React框架使用的目的,就是为了维护状态,更新视图。html
为何会说传统DOM操做效率低呢?当使用document.createElement()建立了一个空的Element时,会须要按照标准实现一大堆的东西,以下图所示。此外,在对DOM进行操做时,若是一不留神致使回流,性能可能就很难保证了。前端
相比之下,JS对象的操做却有着很高的效率,经过操做JS对象,根据这个用 JavaScript 对象表示的树结构来构建一棵真正的DOM树,正是React对上述问题的解决思路。以前的文章中能够看出,使用React进行开发时, DOM树是经过Virtual DOM构造的,而且,React在Virtual DOM上实现了DOM diff算法,当数据更新时,会经过diff算法计算出相应的更新策略,尽可能只对变化的部分进行实际的浏览器的DOM更新,而不是直接从新渲染整个DOM树,从而达到提升性能的目的。在保证性能的同时,使用React的开发人员就没必要再关心如何更新具体的DOM元素,而只须要数据状态和渲染结果的关系。node
传统的diff算法经过循环递归来对节点进行依次比较还计算一棵树到另外一棵树的最少操做,算法复杂度为O(n^3),其中n是树中节点的个数。尽管这个复杂度并很差看,可是确实一个好的算法,只是在实际前端渲染的场景中,随着DOM节点的增多,性能开销也会很是大。而React在此基础之上,针对前端渲染的具体状况进行了具体分析,作出了相应的优化,从而实现了一个稳定高效的diff算法。react
diff算法有以下三个策略:git
DOM节点跨层级的移动操做发生频率很低,是次要矛盾;github
拥有相同类的两个组件将会生成类似的树形结构,拥有不一样类的两个组件将会生成不一样的树形结构,这里也是抓前者放后者的思想;算法
对于同一层级的一组子节点,经过惟一id进行区分,即没事就warn的key。
基于各自的前提策略,React也分别进行了算法优化,来保证总体界面构建的性能。数组
两棵树只会对同一层次的节点进行比较,忽略DOM节点跨层级的移动操做。React只会对相同颜色方框内的DOM节点进行比较,即同一个父节点下的全部子节点。当发现节点已经不存在,则该节点及其子节点会被彻底删除掉,不会用于进一步的比较。这样只须要对树进行一次遍历,便能完成整个DOM树的比较。由此一来,最直接的提高就是复杂度变为线型增加而不是原先的指数增加。浏览器
值得一提的是,若是真的发生跨层级移动(以下图),例如某个DOM及其子节点进行移动挂到另外一个DOM下时,React是不会机智的判断出子树仅仅是发生了移动,而是会直接销毁,并从新建立这个子树,而后再挂在到目标DOM上。从这里能够看出,在实现本身的组件时,保持稳定的DOM结构会有助于性能的提高。事实上,React官方也是建议不要作跨层级的操做。所以在实际使用中,比方说,咱们会经过CSS隐藏或显示某些节点,而不是真的移除或添加DOM节点。其实一旦接受了React的写法,就会发现前面所说的那种移动的写法几乎不会被考虑,这里能够说是React限制了某些写法,不过遵照这些实践确实会使得React有更好的渲染性能。若是真的须要有移动某个DOM的状况,或许考虑考虑尽可能用CSS3来替代会比较好吧。框架
关于这一部分的源码,首先须要提到的是,React是如何控制“层”的。在许多源码阅读的文章里(搜到的讲的比较细的通常都是两三年前啦),都是说用一个updateDepth或者某种控制树深的变量来记录跟踪。事实上就目前版原本看,已经不是这样了(若是我没看错…)。ReactDOMComponent .updateComponent方法用来更新已经分配并挂载到DOM上的DOM组件,并在内部调用ReactDOMComponent._updateDOMChildren。而ReactDOMComponent经过_assign将ReactMultiChild.Mixin挂到原型上,得到ReactMultiChild中定义的方法updateChildren(事实上还有updateTextContent等方法也会在不一样的分支里被使用,React目前已经对这些情形作了不少细化了)。ReactMultiChild包含着diff算法的核心部分,接下来会慢慢进行梳理。到这里咱们暂时没必要再继续往下看,能够注意prevChildren和nextChildren这两个变量,固然removedNodes、mountImages也是意义比较明显且很重要的变量:
prevChildren和nextChildren都是ReactElement,也就是virtual DOM,从它们的$$typeof: Symbol(react.element)就可看出;removedNodes保存删除的节点,mountImages则是保存对真实DOM的映射,或者能够理解为要挂载的真实节点,这些变量会随着调用栈一层层往下做为参数传下去并被修改和包装。
而控制树的深度的方法就是靠传入nextNestedChildrenElements,把整个树的索引一层层递归的传下去,同时传入prevChildren这个虚拟DOM,进入_reconcilerUpdateChildren方法,会在里面经过flattenChildren方法(固然里面还有个traverse方法)来访问咱们的子树指针nextNestedChildrenElements,获得与prevChildren同层的nextChildren。而后ReactChildReconciler.updateChildren就会将prevChildren、nextChildren封装成ReactDOMComponent类型,并进行后续比较和操做。
至此,同层比较叙述结束,后面会继续讨论针对组件的diff和对元素自己的diff。
参考官方文档及其余资料,能够讲组件间的比较策略总结以下:
若是是同类型组件,则按照原策略继续比较virtual DOM树;
若是不是,则将该组件判断为dirty component,而后整个unmount这个组件下的子节点对其进行替换;
对于同类型组件,virtual DOM可能并无发生任何变化,这时咱们能够经过shouldCompoenentUpdate钩子来告诉该组件是否进行diff,从而提升大量的性能。
这里能够看出React再次抓了主要矛盾,对于不一样组件但结构类似的情形再也不去关注,而是对相同组件、类似结构的情形进行diff算法,并提供钩子来进一步优化。能够说,对于页面结构基本没有变化的状况,确实是有着很大的优点。
这一节算是diff算法最核心的部分,我会尝试着对算法的思想进行分析,并结合本身的demo来增进理解。
例子很简单,是一个涉及到新集合中有新加入的节点且老集合存在须要删除的节点的情形。以下图所示。
也就是说,经过点击来控制文字和数字的显示与消失。这种JSX能够说是太经常使用了。正好借学习diff算法的机会,来看看就这种最基本的结构,React是怎么作的。
首先先在ReactMultiChild中的_updateChildren中打上第一个debugger。
断点以前的代码会获得prevChildren和nextChildren,他们通过处理会从ReactElement数组变成一个奇怪的对象,key为“.0”、“.1”这样的带点序号(这里不妨先多说一句,这是React为一个个组件们默认分配的key,若是这里我强行设置一个key给h2h3标签,那么它就会拥有如’$123’这样的key),值为ReactDOMComponent 组件,前面写初次渲染的文章中提到过ReactDOMComponent就是最终渲染到DOM以前的那一环。而在本demo中,prevChildren存放着“哈哈哈的h1标签”和“142567的h3标签”,而nextChildren存放着“哈哈哈的h1标签”和“你好啊的h2标签”。
先不看若干index变量,看到for循环的in写法,便可明白是在遍历存放了新的ReactDOMComponent的对象,而且经过hasOwnProperty来过滤掉原型上的属性和方法。接着各自拿到同层节点的第一个,并对两者进行比较。若是相同,则enqueue一个moveChild方法返回的type为MOVE_EXISTING的对象到updates里,即把更新放入一个队列,moveChild也就是移动已有节点,可是是否真的移动会根据总体diff算法的结果来决定(本例固然是没移动了),而后修改若干index量;不然,就会计算一堆index(这里实际上是算法的核心,此处先不细说),而后再次enqueue一个update,事实上是一个type属性为INSERT_MARKUP的对象。对于本例而言,h1标签不变,则会先来一个MOVE_EXISTING对象,而后h3变h2,再来一个INSERT_MARKUP,而后经过ReactReconciler.getHostNode根据nextChild获得真实DOM。
这个for-in结束后,则是会把须要删除的节点用enqueue的方法继续入队unmount操做,这里this._unmountChild返回的是REMOVE_NODE对象,至此,整个更新的diff流程就走完了,而updates保存了所有的更新队列,最终由processQueue来挨个执行更新。
那么细节在哪里?慢慢来。
首先,React为同层节点比较提供了若干操做。早期版本有INSERT_MARKUP、MOVE_EXISTING、REMOVE_NODE这三个增、移、删操做,如今又加入了SET_MARKUP和TEXT_CONTENT这俩操做。
INSERT_MARKUP,新的component类型(nextChildren里的)不在老集合(prevChildren)里,便是全新的节点,须要对新节点执行插入操做;
MOVE_EXISTING,在老集合有新component类型,且element是可更新的类型,这种状况下prevChild===nextChild,就须要作移动操做,能够复用之前的DOM节点。
REMOVE_NODE,老component类型在新集合里也有,但对应的element不一样则不能直接复用和更新,须要执行删除操做;或者老component不在新集合里的,也须要执行删除操做。
全部的操做都会经过enqueue来入队,把更新细节隐藏,而如何判断作出何种更新操做,则是diff算法之所在。咱们回到前面的代码从新再看,并分状况讨论其中的原理。
首先对新集合的节点(nextChildren)进行in循环遍历,经过惟一的key(这里是变量name,前面提到过nextChildren和prevChildren是以对象的形式存储ReactDOMComponent的)能够取得新老集合中相同的节点,若是不存在,prevChildren即为undefined。根据图中代码,若是存在相同节点,也即prevChild === nextChild,则进行移动操做,但在移动前须要将当前节点在老集合中的位置与 lastIndex 进行比较,见moveChild函数,以下图:
if (child._mountIndex < lastIndex),则进行节点移动操做,不然不执行该操做。这是一种顺序优化手段,lastIndex一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),若是新集合中当前访问的节点比lastIndex大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其余节点的位置,所以不用添加到差别队列中,即不执行移动操做,只有当访问的节点比lastIndex小时,才须要进行移动操做。
图是直接拷来的…画那么好我就不重复画轮子了。仍是源码,就按上面的图来说。
源码中会开始对nextChildren(即新的节点状态 对象形式)进行遍历,而且对象自己是以键值对的形式存储这些节点的状态。首先,key=’b’时,经过prevChildren[name]的方式(name即为key)取老集合节点中是否存在key为b的节点,显然,若是存在,则取得,不存在,则为undefined。而后,判断是否相等。当咱们两个key值相同的B节点被断定相等时,enqueue一个’ MOVE_EXISTING’操做。这一操做内部会做以下判断:
child即为prevChild,也就是判断B._mountIndex < lastIndex,lastIndex是prevChildren最近访问的最新index,初始为0(其实由于这些个children都是对象,因此index更多的是计数而非下标)。这里,B._mountIndex=1,lastIndex为0,因此不作移动操做更新。而后更新lastIndex,以下图所示:
咱们知道prevChild就是B,则prevChild._mountIndex如前所示为1,因此lastIndex更新为1,这样lastIndex就能够记录着prevChildren中最后访问的那个的序号。再而后,更新B的位置为信集合中的位置:
nextIndex随着nextChildren中遍历的子元素递增,此时为1,也就是说,把B的挂载位置设置为0,就至关于告诉B你的位置从1移动到了0。
最后更新nextIndex,准备为下一个放在位置1的元素准备序号。这里getHostNode方法会返回一个真正的DOM,它主要是给enqueue使用,能够理解为开始执行更新队列时能让React知道这些更新的节点要放到的DOM的位置。
第二轮,重新集合取到A,判断到老集合中存在相同节点,一样是对比位置来判断是否进行移动操做。只不过,这一次A._mountIndex=0,lastIndex在上一轮更新为1,知足child._mountIndex<lastIndex的条件,因而enqueue移动操做。
其中toIndex就是nextIndex,目前为1,很正确嘛。而后继续更新lastIndex为1,并更新A._mountIndex=1,而后后续基本一致。
剩下两轮判断,不出上述情形。在此再也不细表。
仍是拿了大佬的图,哈哈。这里其实就是更完整的情形,也就会涉及到整个代码流程,固然也并不复杂。
首先,仍是重新集合先取到B,判断出老集合中有B,因而本轮与上面的第一轮就同样了(同一段代码嘛)。
第二轮,重新集合取到E,可是老集合中不存在,因而走入新流程:
讲白了,就是enqueue来建立节点到指定位置,而后更新E的位置,并nextIndex++来进入下一个节点的执行。
第三轮,重新集合取到C,C在老集合中有,可是判断以后并不进行移动操做,继续各类更新而后进入下一个节点的判断。
第四轮,重新集合中取到A,A也存在,因此enqueue移动操做。
至此,diff已经完成,这以后会对removedNodes进行循环遍历,这个对象是在this._reconcilerUpdateChildren就对比新老集合获得的。
这样一来,新集合中不存在的D也就被清除了。总体上看,是先建立,后删除的方式。
Ok,差很少啦,diff算法的核心就是这么回事啦。
经过diff策略,将算法从O(n^3)简化为O(n)
分层求异,对tree diff进行优化
分组件求异,相同类生成类似树形结构、不一样类生成不一样树形结构,对component diff进行优化
设置key,对element diff进行优化
尽可能保持稳定的DOM结构、避免将最后一个节点移动到列表首部、避免节点数量过大或更新过于频繁
官方文档Keys should be stable, predictable, and unique. Unstable keys (like those produced by Math.random() will cause many component instances and DOM nodes to be unnecessarily recreated, which can cause performance degradation and lost state in child components.