此文已由作者杨介正授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
第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,即Apple。Virtual 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.在执行某一段函数前,先标记环境变量alreadyBatchingUpdates为true。
2.在执行函数体时,每次遇到setState(partial)时,就将partial推入到对应composition component的pending state队列,并标记这个composition component为dirty。
在函数体结束时,更新所有标记为dirty 的composition component。更新过程中,会直接合并pending state队列里的所有状态,达到一次捆绑更新。
4.2.2 batch update的执行条件
上面说过,只有在环境变量alreadyBatchingUpdates为true的执行环境里,setState才能参与batch update,否则就是立即执行更新操作。那么如何使函数体进入这个batch update的环境呢?在第3章事件机制中我们曾说过,Reactjs事件机制可以在执行event listener前后做一些统一的逻辑操作,这在batch update中尤为重要。Reactjs在执行event listener之前,都会先让这个listener进入到batch update的执行环境,即环境变量alreadyBatchingUpdates为true的环境。那么,在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 tree和Virtual DOM tree的树结构里,children的类型不是List,而是一个Map(Object)。那么,既然是个map,如何确定sibling间的顺序呢。首先,根据Object的特性,所有非整数key的property,在遍历时会根据其插入的顺序出现,各大浏览器都一样。明白了这个之后,我再分步骤为大家介绍下上图的整个比较过程:
1. 在old Virtual DOM tree中,div的children是[p, span]。在生成host component tree的时候,div的children变成了{‘.0’:p host component, ‘.1’: span host component},插入顺序根据children的index。同时,p host component的mountIndex属性被设置成0,span host component的mountIndex属性被设置成1,mountIndex属性的作用是标记siblings的左右关系。
2. 由于new tree顶点节点div和old tree顶点节点div 的tag相同,根据已知的规则,div host component接收new tree的顶点节点,对应的Virtual DOM被替换成new Virtual DOM并执行更新Actual DOM属性操作。这里需要注意,new Virtual DOM tree直接比较的对象是host component tree,host component tree会将比较对象代理给自己对应old Virtual DOM。
3. 接下来开始比较div host component的children。new Virtual DOM tree遍历div Virtual DOM的children{‘.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变为下图:
实际上只是将span和p相互换了一下位置,可是更新过程中会发生什么呢?根据tag不同就销毁并替换之的规则,原先的p host component和span host component都会被销毁,并被重新生成的span host component和p host component替代。显然,这种算法是不合理的,特别在大数据的情况下,会带来很大的性能问题。这时,key的作用就来了。如果你为Virtual DOM设置key属性,那么在生成host component tree和new Virtual DOM tree的时候,children中key由默认的.0/.1/...变为key指定的值。如'$key1','$key2'...,$符号是Reactjs自动加上的前缀,保证key是非整数的。这里我们假设给p Virtual DOM添加key属性k1,为span Virtual DOM添加key属性k2,那么上例的图将会变为
这样,在更新的过程就不会发生销毁p host component和span host component的问题了。但是,这样还不够,因为还没有让p host component和span host component更换位置。这时,mountIndex的作用又来了。更新前,p host component的mountIndex为0,span host component的mountIndex为1。在遍历new div Virtual DOM的children时,会递增当前的遍历计数。如果host component的mountIndex小于这个遍历计数,说明位置需要移动了。如当遍历到new p Virtual DOM时,此时的遍历计数是1,而p host component的mountIndex属性值为0,小于1,所以需要移动到1的位置。span host component的mountIndex值为1,大于遍历计数0,因此不需要移动只需要更新mountIndex为0即可。
第5章 组件的生命周期及性能优化
5.1 组件的生命周期
每个组件都拥有生命周期,分初始化、安装、渲染、更新、卸载5个阶段,每个阶段都对应不同的函数及作用。
5.1.1 初始化
constructor,一般用于设置初始状态。
5.1.2 安装
componentWillMount和componentDidMount,分别在渲染前后调用,一般用于远程请求数据操作。
5.1.3 渲染
render,渲染组件,即将组件的render方法返回的Virtual DOM tree渲染成Actual DOM tree。
5.1.4 更新
一般更新前后又可以分为5个阶段:
5.1.4.1 componentWillReceiveProps(nextProps)
在每次组件接收new Virtual DOM的时候触发,这里的nextProps就是new Virtual DOM的props。因为props往往会被用在组件render返回的Virtual DOM tree 的结构中,所以如果在这里改变nextProps,就会更改在第4个阶段中diff算法的new Virtual DOM tree的结构。
5.1.4.2 shouldComponentUpdate(nextProps, nextState, nextContext)
用于判断组件是否需要更新,如果返回false,则即使通过setState更新状态,组件也不会进入剩下的3个阶段。但注意,此时组件的状态state会更新到最新。
5.1.4.3 componentWillUpdate(nextProps, nextState, nextContext)
在真正更新前执行,同样,这里可以更改nextProps和nextState。但是不要通过setState的方式,这样会让组件的更新进入无限循环。因为setState会将组件重复的标记为dirty,dirty的组件会被执行更新操作。
5.1.4.4 执行更新
前面也提到过,更新操作就是根据组件最新的状态重新生成new Virtual DOM tree,然后与old Virtual DOM tree进行比较,通过diff算法比较,更新host component tree及对应的Actual DOM tree。
5.1.4.5 componentDidUpdate(prevProps, 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,并执行更新相关的操作,即使组件的props和state都没有发生变化。如果我们在shouldComponentUpdate函数里判断一下props和state的变化情况,在它们都没有发生变化的情况将函数直接返回false,将大大减少更新操作中无用的更新。Reactjs提供了一个PureComponent类,如果你的组件继承至它,那么在更新时它将自动帮你判断pros和state的变化。所以,大家在开发过程中,可以尽量让自己的组件继承至React.PureComponent。
5.2.2 清楚setState batch update的条件
只有在环境变量alreadyBatchingUpdates为true的执行环境里,例如event listener里,setState才能参与batch update。其他地方使用setState,请尽量只调用一次。如果你的组件不是继承至PureComponent或者没有在shouldComponentUpdate作判断,更不要在componentWillReceiveProps,shouldComponentUpdate,componentDidUpdate上执行setState操作。
5.2.3在constructor里初始化好状态
不要在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+款云产品!
更多网易技术、产品、运营经验分享请点击。