抛砖引玉

React通过引入Virtual DOM的概念,极大地防止有效的Dom操作,已使咱们的页面的构建效率提到了极大的晋升。然而如何高效地通过比照新旧Virtual DOM来找出真正的Dom变动之处同样也决定着页面的性能,React用其非凡的diff算法解决这个问题。Virtual DOM+React diff的组合极大地保障了React的性能,使其在业界有着不错的性能口碑。diff算法并非React独创,React只是对diff算法做了一个优化,但却是因为这个优化,给React带来了极大的性能晋升,不禁让人感叹React创造者们的智慧!接下来咱们就探索一下React的diff算法。

传统diff算法

在文章结尾咱们提到React的diff算法给React带来了极大的性能晋升,而之前的React diff算法是在传统diff算法上的优化。上面咱们先看一下传统的diff算法是什么样子的。

传统diff算法通过循环递归对节点进行顺次比照,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。

O(n^3) 到底有多可怕呢?这意味着如果要展现 1000 个节点,就要顺次执行上十亿次 的比拟,这种指数型的性能耗费对于前端渲染场景来说代价太高了。而React却这个diff算法工夫复杂度从O(n^3)降到O(n)。O(n^3)到O(n)的晋升有多大,咱们通过一张图来看一下。

从下面这张图来看,React的diff算法所带来的晋升无疑是微小无比的。接下来咱们再看一张图:

从1979到2011,30多年的工夫,才将工夫复杂度搞到O(n^3),而React从开源到当初不过区区几年的工夫,却一下子干到O(n),到这里不禁再次膜拜一下React的创造者们。
那么React这个牛逼的diff算法是如何做到的呢?

React diff原理

后面咱们讲到传统diff算法的工夫复杂度为O(n^3),其中n为树中节点的总数,随着n的减少,diff所消耗的工夫将出现爆炸性的增长。react却利用其非凡的diff算法做到了O(n^3)到O(n)的飞跃性的晋升,而实现这一壮举的法宝就是上面这三条看似简略的diff策略:

  • Web UI中DOM节点跨层级的挪动操作特地少,能够忽略不计
  • 领有雷同类的两个组件将会生成类似的树形构造,领有不同类的两个组件将会生成不同的树形构造
  • 对于同一层级的一组子节点,它们能够通过惟一 id 进行辨别

在下面三个策略的根底上,React 别离将对应的tree diff、component diff 以及 element diff 进行算法优化,极大地晋升了diff效率。

tree diff

基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比拟,两棵树只会对同一档次的节点进行比拟。

既然 DOM 节点跨层级的挪动操作少到能够忽略不计,针对这一景象,React只会对雷同层级的 DOM 节点进行比拟,即同一个父节点下的所有子节点。当发现节点曾经不存在时,则该节点及其子节点会被齐全删除掉,不会用于进一步的比拟。这样只须要对树进行一次遍历,便能实现整个 DOM 树的比拟。参考React实战视频解说:进入学习

策略一的前提是Web UI中DOM节点跨层级的挪动操作特地少,但并没有否定DOM节点跨层级的操作的存在,那么当遇到这种操作时,React是如何解决的呢?

接下来咱们通过一张图来展现整个处理过程:

A 节点(包含其子节点)整个被挪动到 D 节点下,因为 React 只会简略地思考同层级节点的地位变换,而对于不 同层级的节点,只有创立和删除操作。当根节点发现子节点中 A 隐没了,就会间接销毁 A;当 D 发现多了一个子节点 A,则会创 建新的 A(包含子节点)作为其子节点。此时,diff 的执行状况:create A → create B → create C → delete A。

由此能够发现,当呈现节点跨层级挪动时,并不会呈现设想中的挪动操作,而是以 A 为根节点的整个树被从新创立。这是一种影响React性能的操作,因而官网倡议不要进行 DOM 节点跨层级的操作。

在开发组件时,保持稳定的 DOM 构造会有助于性能的晋升。例如,能够通过 CSS 暗藏或显示节点,而不是真正地移
除或增加 DOM 节点。

component diff

React 是基于组件构建利用的,对于组件间的比拟所采取的策略也是十分简洁、高效的。

  • 如果是同一类型的组件,依照原策略持续比拟 Virtual DOM 树即可。
  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
  • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变动,如果可能确切晓得这点,那么就能够节俭大量的 diff 运算工夫。因而,React容许用户通过shouldComponentUpdate()来判断该组件是否需进行diff算法剖析,然而如果调用了forceUpdate办法,shouldComponentUpdate则生效。

接下来咱们看上面这个例子是如何实现转换的:

转换流程如下:

当组件D变为组件G时,即便这两个组件构造类似,一旦React判断D和G是不同类型的组件,就不会比拟二 者的构造,而是间接删除组件D,从新创立组件G及其子节点。尽管当两个组件是不同类型但构造类似时,diff会影响性能,但正如React官网博客所言:不同类型的组件很少存在类似DOM树的状况,因而这种极其因素很难在理论开发过程中造成重大的影响。

element diff

当节点处于同一层级时,diff 提供了 3 种节点操作,别离为 INSERT_MARKUP (插入)、MOVE_EXISTING (挪动)和 REMOVE_NODE (删除)。

  • INSERT_MARKUP :新的组件类型不在旧汇合里,即全新的节点,须要对新节点执行插入操作。
  • **MOVE_EXISTING :旧汇合中有新组件类型,且 element 是可更新的类型,generateComponentChildren 已调用
    receiveComponent ,这种状况下 prevChild=nextChild ,就须要做挪动操作,能够复用以前的 DOM 节点。**
  • **REMOVE_NODE :旧组件类型,在新汇合里也有,但对应的 element 不同则不能间接复用和更新,须要执行删除操作,或者
    旧组件不在新汇合里的,也须要执行删除操作。**

旧汇合中蕴含节点A、B、C和D,更新后的新汇合中蕴含节点B、A、D和C,此时新旧汇合进行diff差异化比照,发现B!=A,则创立并插入B至新汇合,删除旧汇合A;以此类推,创立并插入A、D和C,删除B、C和D。


咱们发现这些都是雷同的节点,仅仅是地位产生了变动,但却须要进行繁冗低效的删除、创立操作,其实只有对这些节点进行地位挪动即可。React针对这一景象提出了一种优化策略:容许开发者对同一层级的同组子节点,增加惟一 key 进行辨别。 尽管只是小小的改变,性能上却产生了天翻地覆的变动!咱们再来看一下利用了这个策略之后,react diff是如何操作的。

通过key能够精确地发现新旧汇合中的节点都是雷同的节点,因而无需进行节点删除和创立,只须要将旧汇合中节点的地位进行挪动,更新为新汇合中节点的地位,此时React 给出的diff后果为:B、D不做任何操作,A、C进行挪动操作即可。

具体的流程咱们用一张表格来展示一下:

index节点oldIndexmaxIndex操作
0B10oldIndex(1)>maxIndex(0),maxIndex=oldIndex,maxIndex变为1
1A01oldIndex(0)<maxIndex(1),节点A挪动至index(1)的地位
2D31oldIndex(3)>maxIndex(1),maxIndex=oldIndex,maxIndex变为3
3C23oldIndex(2)<maxIndex(3),节点C挪动至index(3)的地位
  • index: 新汇合的遍历下标。
  • oldIndex:以后节点在老汇合中的下标。
  • maxIndex:在新汇合拜访过的节点中,其在老汇合的最大下标值。

操作一栏中只比拟oldIndex和maxIndex:

  • 当oldIndex>maxIndex时,将oldIndex的值赋值给maxIndex
  • 当oldIndex=maxIndex时,不操作
  • 当oldIndex<maxIndex时,将以后节点挪动到index的地位

下面的例子仅仅是在新旧汇合中的节点都是雷同的节点的状况下,那如果新汇合中有新退出的节点且旧汇合存在 须要删除的节点,那么 diff 又是如何比照运作的呢?

index节点oldIndexmaxIndex操作
0B10oldIndex(1)>maxIndex(0),maxIndex=oldIndex,maxIndex变为1
1E-1oldIndex不存在,增加节点E至index(1)的地位
2C21oldIndex(2)>maxIndex(1),maxIndex=oldIndex,maxIndex变为2
3A02oldIndex(0)<maxIndex(2),节点A挪动至index(3)的地位
注:最初还须要对旧汇合进行循环遍历,找出新汇合中没有的节点,此时发现存在这样的节点D,因而删除节点D,到此 diff 操作全副实现。

同样操作一栏中只比拟oldIndex和maxIndex,然而oldIndex可能有不存在的状况:

  • oldIndex存在

    1. 当oldIndex>maxIndex时,将oldIndex的值赋值给maxIndex
    2. 当oldIndex=maxIndex时,不操作
    3. 当oldIndex<maxIndex时,将以后节点挪动到index的地位
  • oldIndex不存在

    1. 新增以后节点至index的地位


当然这种diff并非白璧无瑕的,咱们来看这么一种状况:

理论咱们只需对D执行挪动操作,然而因为D在旧汇合中的地位是最大的,导致其余节点的oldIndex < maxIndex,造成D没有执行挪动操作,而是A、B、C全副挪动到D节点前面的景象。针对这种状况,官网倡议:

在开发过程中,尽量减少相似将最初一个节点挪动到列表首部的操作。当节点数量过大或更新操作过于频繁时,这在肯定水平上会影响React的渲染性能。

因为key的存在,react能够精确地判断出该节点在新汇合中是否存在,这极大地提高了diff效率。咱们在开发过中进行列表渲染的时候,若没有加key,react会抛出正告要求开发者加上key,就是为了进步diff效率。然而加了key肯定要比没加key的性能更高吗?咱们再来看一个例子:

当初有一汇合[1,2,3,4,5],渲染成如下的样子:<div>1</div><div>2</div><div>3</div><div>4</div><div>5</div>---------------当初咱们将这个汇合的程序打乱变成[1,3,2,5,4]。1.加key<div key='1'>1</div>             <div key='1'>1</div>     <div key='2'>2</div>             <div key='3'>3</div>  <div key='3'>3</div>  ========>  <div key='2'>2</div>  <div key='4'>4</div>             <div key='5'>5</div>  <div key='5'>5</div>             <div key='4'>4</div>  操作:节点2挪动至下标为2的地位,节点4挪动至下标为4的地位。2.不加key<div>1</div>             <div>1</div>     <div>2</div>             <div>3</div>  <div>3</div>  ========>  <div>2</div>  <div>4</div>             <div>5</div>  <div>5</div>             <div>4</div>  操作:批改第1个到第5个节点的innerText---------------如果咱们对这个汇合进行增删的操作改成[1,3,2,5,6]。1.加key<div key='1'>1</div>             <div key='1'>1</div>     <div key='2'>2</div>             <div key='3'>3</div>  <div key='3'>3</div>  ========>  <div key='2'>2</div>  <div key='4'>4</div>             <div key='5'>5</div>  <div key='5'>5</div>             <div key='6'>6</div>  操作:节点2挪动至下标为2的地位,新增节点6至下标为4的地位,删除节点4。2.不加key<div>1</div>             <div>1</div>     <div>2</div>             <div>3</div>  <div>3</div>  ========>  <div>2</div>  <div>4</div>             <div>5</div>  <div>5</div>             <div>6</div> 操作:批改第1个到第5个节点的innerText---------------通过下面这两个例子咱们发现:因为dom节点的挪动操作开销是比拟低廉的,没有key的状况下要比有key的性能更好。

通过下面的例子咱们发现,尽管加了key进步了diff效率,然而未必肯定晋升了页面的性能。因而咱们要留神这么一点:

对于简略列表页渲染来说,不加key要比加了key的性能更好

依据下面的状况,最初咱们总结一下key的作用:

  • 精确判断出以后节点是否在旧汇合中
  • 极大地缩小遍历次数

利用实际

页面指定区域刷新

当初有这么一个需要,当用户身份变动时,以后页面从新加载数据。猛一看过来感觉非常简单,没啥难度的,只有在componentDidUpdate这个生命周期里去判断用户身份是否产生扭转,如果产生扭转就从新申请数据,于是就有了以下这一段代码:

import React from 'react';import {connect} from 'react-redux';let oldAuthType = '';//用来存储旧的用户身份@connect(  state=>state.user)class Page1 extends React.PureComponent{  state={    loading:true  }  loadMainData(){    //这里采纳了定时器去模仿数据申请    this.setState({      loading:true    });    const timer = setTimeout(()=>{      this.setState({        loading:false      });      clearTimeout(timer);    },2000);  }  componentDidUpdate(){    const {authType} = this.props;    //判断以后用户身份是否产生了扭转    if(authType!==oldAuthType){      //存储新的用户身份      oldAuthType=authType;      //从新加载数据      this.loadMainData();    }  }  componentDidMount(){    oldAuthType=this.props.authType;    this.loadMainData();  }  render(){    const {loading} = this.state;    return (      <h2>{`页面1${loading?'加载中...':'加载实现'}`}</h2>    )  }}export default Page1;

看上去咱们仅仅通过加上一段代码就实现了这一需要,然而当咱们页面是几十个的时候,那这种办法就显得顾此失彼了。哪有没有一个很好的办法来实现这个需要呢?其实很简略,利用react diff的个性就能够实现它。对于这个需要,实际上就是心愿以后组件能够销毁在从新生成,那怎么能力让其销毁并从新生成呢?通过下面的总结我发现两种状况,能够实现组件的销毁并从新生成。

  • 当组件类型产生扭转
  • 当key值发生变化
    接下来咱们就联合这两个特点,用两种办法去实现。

第一种:引入一个loading组件。切换身份时设置loading为true,此时loading组件显示;切换身份实现,loading变为false,其子节点children显示。

<div className="g-main">{loading?<Loading/>:children}</div> 

第二种:在刷新区域加上一个key值就能够了,用户身份一扭转,key值就产生扭转。

<div className="g-main" key={authType}>{children}</div>

第一种和第二种取舍上,集体倡议的是这样子的:

如果须要申请服务器的,用第一种,因为申请服务器会有肯定等待时间,退出loading组件能够让用户有感知,体验更好。如果是不须要申请服务器的状况下,选用第二种,因为第二种更简略实用。

更加不便地监听props扭转

针对这个需要,咱们喜爱将搜寻条件封装成一个组件,查问列表封装成一个组件。其中查问列表会接管一个查问参数的属性,如下所示:

import React from 'react';import {Card} from 'antd';import Filter from './components/filter';import Teacher from './components/teacher';export default class Demo2 extends React.PureComponent{  state={    filters:{      name:undefined,      height:undefined,      age:undefined    }  }  handleFilterChange=(filters)=>{    this.setState({      filters    });  }  render(){    const {filters} = this.state;    return <Card>      {/* 过滤器 */}      <Filter onChange={this.handleFilterChange}/>       {/* 查问列表 */}      <Teacher filters={filters}/>    </Card>  }}

当初咱们面临一个问题,如何在组件Teacher中监听filters的变动,因为filters是一个援用类型,想监听其变动变得有些简单,好在lodash提供了比拟两个对象的工具办法,使其简略了。然而如果前期给Teacher加了额定的props,此时你要监听多个props的变动时,你的代码将变得比拟难以保护。针对这个问题,咱们仍旧能够通过key值去实现,当每次搜寻时,从新生成一个key,那么Teacher组件就会从新加载了。代码如下:

import React from 'react';import {Card} from 'antd';import Filter from './components/filter';import Teacher from './components/teacher';export default class Demo2 extends React.PureComponent{  state={    filters:{      name:undefined,      height:undefined,      age:undefined    },    tableKey:this.createTableKey()  }  createTableKey(){    return Math.random().toString(36).substring(7);  }  handleFilterChange=(filters)=>{    this.setState({      filters,      //从新生成tableKey      tableKey:this.createTableKey()    });  }  render(){    const {filters,tableKey} = this.state;    return <Card>      {/* 过滤器 */}      <Filter onChange={this.handleFilterChange}/>       {/* 查问列表 */}      <Teacher key={tableKey} filters={filters}/>    </Card>  }}

即便前期给Teacher退出新的props,也没有问题,只需拼接一下key即可:

 <Teacher key={`${tableKey}-${prop1}-${prop2}`} filters={filters} prop1={prop1} prop2={prop2}/>

react-router中Link问题

先看一下demo代码:

import React from 'react';import {Card,Spin,Divider,Row,Col} from 'antd';import {Link} from 'react-router-dom';const bookList = [{  bookId:'1',  bookName:'三国演义',  author:'罗贯中'},{  bookId:'2',  bookName:'水浒传',  author:'施耐庵'}]export default class Demo3 extends React.PureComponent{  state={    bookList:[],    bookId:'',    loading:true  }  loadBookList(bookId){    this.setState({      loading:true    });    const timer = setTimeout(()=>{      this.setState({        loading:false,        bookId,        bookList      });      clearTimeout(timer);    },2000);  }  componentDidMount(){    const {match} = this.props;    const {params} = match;    const {bookId} = params;    this.loadBookList(bookId);  }  render(){    const {bookList,bookId,loading} = this.state;    const selectedBook = bookList.find((book)=>book.bookId===bookId);    return <Card>      <Spin spinning={loading}>        {          selectedBook&&(<div>            <img width="120" src={`/static/images/book_cover_${bookId}.jpeg`}/>            <h4>书名:{selectedBook?selectedBook.bookName:'--'}</h4>            <div>作者:{selectedBook?selectedBook.author:'--'}</div>          </div>)        }        <Divider orientation="left">关联图书</Divider>        <Row>          {            bookList.filter((book)=>book.bookId!==bookId).map((book)=>{              const {bookId,bookName} = book;              return <Col span={6}>                <img width="120" src={`/static/images/book_cover_${bookId}.jpeg`}/>                <h4><Link to={`/demo3/${bookId}`}>{bookName}</Link></h4>              </Col>            })          }        </Row>      </Spin>    </Card>  }}

通过演示gif,咱们看到了地址栏的地址曾经产生扭转,然而并没有咱们设想中那样从新走一遍componentDidMount去申请数据,这阐明咱们的组件并没有实现销毁并从新生成这么一个过程。解决这个问题你能够在componentDidUpdate去监听其扭转:

  componentDidUpdate(){    const {match} = this.props;    const {params} = match;    const {bookId} = params;    if(bookId!==this.state.bookId){      this.loadBookList(bookId);    }  }

后面咱们说过如果是前期须要监听多个props的话,这样子前期保护比拟麻烦.同样咱们还是利用key去解决这个问题,首页咱们能够将页面封装成一个组件BookDetail,并且在其外层再包裹一层,再去给BookDetail加key,代码如下:

import React from 'react';import BookDetail from './bookDetail';export default class Demo3 extends React.PureComponent{  render(){    const {match} = this.props;    const {params} = match;    const {bookId} = params;    return <BookDetail key={bookId} bookId={bookId}/>  }}

这样的益处是咱们代码构造更加清晰,后续拓展新性能比较简单。

结语:

  • React的高效得益于其Virtual DOM+React diff的体系。diff算法并非react独创,react只是在传统diff算法做了优化。但因为其优化,将diff算法的工夫复杂度一下子从O(n^3)降到O(n)。
  • React diff的三大策略:

    • Web UI中DOM节点跨层级的挪动操作特地少,能够忽略不计。
    • 领有雷同类的两个组件将会生成类似的树形构造,领有不同类的两个组件将会生成不同的树形构造。
    • 对于同一层级的一组子节点,它们能够通过惟一 id 进行辨别。
  • 在开发组件时,保持稳定的 DOM 构造会有助于性能的晋升。
  • 在开发过程中,尽量减少相似将最初一个节点挪动到列表首部的操作。
  • key的存在是为了晋升diff效率,但未必肯定就能够晋升性能,记住简略列表渲染状况下,不加key要比加key的性能更好。
  • 懂得借助react diff的个性去解决咱们理论开发中的一系列问题。