开局一张图,不充钱也能精通Reactjs内部实现原理(2)

勿忘初心2018-10-26 12:01

此文已由作者杨介正授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。


第4章 Batch update diff算法原理

组件更新就是根据最新的组件状态state重新生成new Virtual DOM tree,然后与old Virtual DOM tree进行比较,根据diff算法规则更新host component tree及对应的Actual DOM tree。下图为整个更新的原理图,我们从上到下说明一下整个更新过程。


4.1.1初始化状态

组件的状态信息保存在state对象里,初始状态为{fruit: ‘Apple’}。如图,Virtual DOM树在创建时,将Virtual DOM p的属性fruit设置为state.fruit,AppleVirtual DOM为:

React.createElement(‘p’,{fruit: this.state.fruit})

对应的Actual DOM会拥有fruit=’Apple’attribute


4.1.2触发更新

组件在event listener里或者其他任何一个地方调用setState方法时,对应的composition component会被标记为dirty。如果在setState里更新了组件状态,则对应的composition component会执行更新操作。这里,我们将状态更新成了{fruit: ‘Pear’}


4.1.3执行更新



composition component会根据新的state重新生成一颗Virtual DOM树,然后让old Virtual DOM树从顶点节点开始依次和new Virtual DOM树比较。如果两个节点的type不一样,则直接unmount对应的host component,并用新的Virtual DOM生成的host component替换之,并且用新生成的Actual DOM替换老的Actual DOM;如果两个节点的type相同,key也相同,则host component receive这个new Virtual DOM替换掉old Virtual DOM,并更新对应的Actual DOM属性。

4.2 batch update


4.2.1 batch update的作用及基本原理


请看下面代码:

this.setState({a:1, b:2});
this.setState({a:1, c:3});

当执行以上代码时,每调用一次setState都会触发组件更新操作,两次更新操作的最终结果是组件的状态更新为state={a:1,b:2,c:3}。为了提高更新效率,Reactjs通过batch update的方式将多次更新合并在一起,通过一次捆绑更新使组件的状态达到最新state={a:1,b:2,c:3}batch update的基本原理比较简单:

    1.在执行某一段函数前,先标记环境变量alreadyBatchingUpdatestrue

    2.在执行函数体时,每次遇到setStatepartial)时,就将partial推入到对应composition componentpending state队列,并标记这个composition componentdirty

在函数体结束时,更新所有标记为dirty composition component。更新过程中,会直接合并pending state队列里的所有状态,达到一次捆绑更新。


4.2.2 batch update的执行条件

上面说过,只有在环境变量alreadyBatchingUpdatestrue的执行环境里,setState才能参与batch update,否则就是立即执行更新操作。那么如何使函数体进入这个batch update的环境呢?在第3章事件机制中我们曾说过,Reactjs事件机制可以在执行event listener前后做一些统一的逻辑操作,这在batch update中尤为重要。Reactjs在执行event listener之前,都会先让这个listener进入到batch update的执行环境,即环境变量alreadyBatchingUpdatestrue的环境。那么,在event listener函数体里的一切setState操作都会参与进batch update

实现的原理也很简单,将函数作为参数传入batch update函数即可。因此,不是随随便便的setState都可以参与到batch update,一般只有在event listener等具有batch update执行环境的setState才可以。

4.3 diff算法原理

在执行更新操作时,需要比较新老两颗Virtual DOM树,高效的diff算法至关重要,这一节为大家介绍下Reactjs的高效diff算法原理。


如图,在host component treeVirtual DOM tree的树结构里,children的类型不是List,而是一个MapObject)。那么,既然是个map,如何确定sibling间的顺序呢。首先,根据Object的特性,所有非整数keyproperty,在遍历时会根据其插入的顺序出现,各大浏览器都一样。明白了这个之后,我再分步骤为大家介绍下上图的整个比较过程:

1. old Virtual DOM tree中,divchildren[p, span]。在生成host component tree的时候,divchildren变成了{‘.0’:p host component, ‘.1’: span host component},插入顺序根据childrenindex。同时,p host componentmountIndex属性被设置成0span host componentmountIndex属性被设置成1mountIndex属性的作用是标记siblings的左右关系

2. 由于new tree顶点节点divold tree顶点节点div tag相同,根据已知的规则,div host component接收new tree的顶点节点,对应的Virtual DOM被替换成new Virtual DOM并执行更新Actual DOM属性操作。这里需要注意,new Virtual DOM tree直接比较的对象是host component treehost component tree会将比较对象代理给自己对应old Virtual DOM

3. 接下来开始比较div host componentchildrennew Virtual DOM tree遍历div Virtual DOMchildren{‘.0’: p, ‘.1’: span}。因为.0.1都不是整数,会根据插入的顺序出现,因此首先会比较p Virtual DOM,然后再比较span Virtual DOM

4. 比较p Virtual DOM时,由于tag相同,所以p host component接收new p Virtual DOM,将对应的Actual DOM的属性fruit更新为Pear

5. 比较span Virtual DOM时,由于tag相同,所以span host component接收new span Virtual DOM,对应的Actual DOM没有属性需要更新。

从以上的比较过程中,还看不出将children的类型设为map的好处。这里,还需要引入一个key的概念,用来自定义children中的key。假设new Virtual DOM tree变为下图:

 

实际上只是将spanp相互换了一下位置,可是更新过程中会发生什么呢?根据tag不同就销毁并替换之的规则,原先的p host componentspan host component都会被销毁,并被重新生成的span host componentp host component替代。显然,这种算法是不合理的,特别在大数据的情况下,会带来很大的性能问题。这时,key的作用就来了。如果你为Virtual DOM设置key属性,那么在生成host component treenew Virtual DOM tree的时候,childrenkey由默认的.0/.1/...变为key指定的值。如'$key1','$key2'...$符号是Reactjs自动加上的前缀,保证key是非整数的。这里我们假设给p Virtual DOM添加key属性k1,span Virtual DOM添加key属性k2,那么上例的图将会变为


这样,在更新的过程就不会发生销毁p host componentspan host component的问题了。但是,这样还不够,因为还没有让p host componentspan host component更换位置。这时,mountIndex的作用又来了。更新前,p host componentmountIndex0span host componentmountIndex1。在遍历new div Virtual DOMchildren时,会递增当前的遍历计数。如果host componentmountIndex小于这个遍历计数,说明位置需要移动了。如当遍历到new p Virtual DOM时,此时的遍历计数是1,而p host componentmountIndex属性值为0,小于1,所以需要移动到1的位置。span host componentmountIndex值为1,大于遍历计数0,因此不需要移动只需要更新mountIndex0即可。


第5章 组件的生命周期及性能优化


5.1 组件的生命周期

每个组件都拥有生命周期,分初始化、安装、渲染、更新、卸载5个阶段,每个阶段都对应不同的函数及作用。


5.1.1 初始化

constructor,一般用于设置初始状态。

 

5.1.2 安装

componentWillMountcomponentDidMount,分别在渲染前后调用,一般用于远程请求数据操作。

 

5.1.3 渲染

render,渲染组件,即将组件的render方法返回的Virtual DOM tree渲染成Actual DOM tree

 

5.1.4 更新

一般更新前后又可以分为5个阶段:


5.1.4.1 componentWillReceivePropsnextProps 

在每次组件接收new Virtual DOM的时候触发,这里的nextProps就是new Virtual DOMprops。因为props往往会被用在组件render返回的Virtual DOM tree 的结构中,所以如果在这里改变nextProps,就会更改在第4个阶段中diff算法的new Virtual DOM tree的结构。


5.1.4.2 shouldComponentUpdatenextProps, nextState, nextContext

用于判断组件是否需要更新,如果返回false,则即使通过setState更新状态,组件也不会进入剩下的3个阶段。但注意,此时组件的状态state会更新到最新。

5.1.4.3 componentWillUpdatenextProps, nextState, nextContext

在真正更新前执行,同样,这里可以更改nextPropsnextState。但是不要通过setState的方式,这样会让组件的更新进入无限循环。因为setState会将组件重复的标记为dirtydirty的组件会被执行更新操作。


5.1.4.4 执行更新

前面也提到过,更新操作就是根据组件最新的状态重新生成new Virtual DOM tree,然后与old Virtual DOM tree进行比较,通过diff算法比较,更新host component tree及对应的Actual DOM tree


5.1.4.5 componentDidUpdateprevProps, prevState, prevContext

在真正更新后执行,同样也不要用setState方法。当然,除非你可以在shouldComponentUpdate方法进行合理的判断,使组件无法进入无限更新状态。

    

在组件真正执行更新前,由于最新的state还没有被更新进this.state,所以大家在第1到第3阶段的函数里,大家不要去用this.state属性。

 

5.1.5 卸载

componentWillUnMount,组件被卸载后执行,一般在这里作一些资源的清理工作,比如定时器。


5.2 性能优化

上面分析了这么多原理,其中一个目的就是用Reactjs时少踩吭,并写出高性能的应用。


5.2.1 利用好shouldComponentUpdate

默认情况下,当组件的父组件进入更新状态,那么组件就会接收到一个new Virtual DOM,并执行更新相关的操作,即使组件的propsstate都没有发生变化。如果我们在shouldComponentUpdate函数里判断一下propsstate的变化情况,在它们都没有发生变化的情况将函数直接返回false,将大大减少更新操作中无用的更新。Reactjs提供了一个PureComponent类,如果你的组件继承至它,那么在更新时它将自动帮你判断prosstate的变化。所以,大家在开发过程中,可以尽量让自己的组件继承至React.PureComponent

5.2.2 清楚setState batch update的条件

只有在环境变量alreadyBatchingUpdatestrue的执行环境里,例如event listener里,setState才能参与batch update。其他地方使用setState,请尽量只调用一次。如果你的组件不是继承至PureComponent或者没有在shouldComponentUpdate作判断,更不要在componentWillReceiveProps,shouldComponentUpdate,componentDidUpdate上执行setState操作。


5.2.3constructor里初始化好状态

不要在componentWillMount或者componentDidMount等地方初始化状态,这样会让组件无故多更新1次。


5.2.4请尽量给siblings Virtual DOM提供不重复的key

如果Virtual DOM发生了移动或者添加,如果没有key值会大大增加diff算法的开销。所以在条件允许下,大家可以尽量给Virtual DOM提供key值。key值得实现原理,大家可以回到4.3 节回顾一下。


第6章 服务端渲染原理


写到这一章的时候,我觉得自己已经被掏空了,我就用一句话结束这一章吧。客服端渲染,Virtual DOM tree host component tree 映射成 Actual DOM tree;服务端则是映射成Actual DOM tree 对应html字符串。就是这么简单,意不意外?惊不惊喜?


结语

写这篇文章,主要是想告诉大家Reactjs的实现原理和思想,并不在关注它的代码实现,因为实现的方式千万种。希望大家通过熟知原理,平时在用Reactjs的时候能够游刃有余,大胆的改变状态,大胆的使用setState,大胆的在各个生命周期函数里做你想做的。而不用对官方文档的中出现的“可能”、“有时候”、“不一定”、“不可预测的结果”等模糊字眼有所顾忌。最后还是客套话:这篇文章不仅仅写的时候时间仓促,我还要告诉大家一个更坏的消息:这篇文章不仅仅是我编的,更可怕的是我还没有用过Reactjs。因为最近有一个项目要迁移到Reactjs,为了少踩吭,为了让代码更高效,我才来研究一下它的源码的。因此,文章中如果说得不对的地方,请大家多多指正,最好是popo直接联系我hzyangjiezheng,谢谢。


网易云免费体验馆,0成本体验20+款云产品! 

更多网易技术、产品、运营经验分享请点击