5.2.1 React的调和(Reconciliation)过程
您可以在百度里搜索“深入浅出React和Redux 艾草文学(www.321553.xyz)”查找最新章节!
5.2.1 React的调和(Reconciliation)过程
在装载过程中,React通过render方法在内存中产生了一个树形的结构,树上每一个节点代表一个React组件或者原生的DOM元素,这个树形结构就是所谓的Virtual DOM。React根据这个Virtual DOM来渲染产生浏览器中的DOM树。
在装载过程结束后,用户就可以对网页进行交互,用户操作引发了界面的更新,网页中需要更新界面,React依然通过render方法获得一个新的树形结构Virtual DOM,这时候当然不能完全和装载过程一样直接用Virtual DOM去产生DOM树,不然就和最原始的字符串模板一个做法。而且,在真实的应用中,大部分网页内容的更新都是局部的小改动,如果每个改动都是推倒重来,那样每次都重新完全生成DOM树,性能肯定不可接受。
实际上,React在更新阶段很巧妙地对比原有的Virtual DOM和新生成的Virtual DOM,找出两者的不同之处,根据不同来修改DOM树,这样只需做最小的必要改动。
React在更新中这个“找不同”的过程,就叫做Reconciliation(调和)。
Facebook推出React之处打出的旗号之一就是“高性能”,所以React的Reconci-liation过程必须快速。但是,找出两个树形结构的区别,从计算机科学的角度来说,真的不是一件快速的过程。
按照计算机科学目前的算法研究结果,对比两个N个节点的树形结构的算法,时间复杂度是O(N3),打个比方,假如两个树形结构上各有100节点,那么找出这两个树形结构差别的操作,需要100×100×100次操作,也就是一百万次当量的操作,假如有一千个节点,那么需要相当于进行相当于1000×1000×1000次操作,这是一亿次的操作当量,这么巨大数量的操作在强调快速反应的网页中是不可想象的,所以React不可能采用这样的算法。
React实际采用的算法需要的时间复杂度是O(N),因为对比两个树形怎么着都要对比两个树形上的节点,似乎也不可能有比O(N)时间复杂度更低的算法。
注意
O(N)时间复杂度的算法并不是说一定会有N次指令操作,O(N3)时间复杂度的算法也不是说这个算法一定会执行N的三次方数量的指令操作,时间复杂度只是对一个算法最好和最差情况下需要的指令操作数量级的估量,本书中不仔细介绍时间复杂度的定义,但是读者应该了解通常一个O(N3)时间复杂度的算法意味着不适合于高性能要求的场合。
React采用的算法肯定不是最精准的,但是对于React应对的场景来说,绝对是性能和复杂度的最好折衷,让这个算法发挥作用,还需要开发者一点配合,让我们来看一看React的取属性结果差异算法。
其实React的Reconciliation算法并不复杂,当React要对比两个Virtual DOM的树形结构的时候,从根节点开始递归往下比对,在树形结构上,每个节点都可以看作一个这个节点以下部分子树的根节点。所以其实这个对比算法可以从Virtual DOM上任何一个节点开始执行。
React首先检查两个树形的根节点的类型是否相同,根据相同或者不同有不同处理方式。
1.节点类型不同的情况
如果树形结构根节点类型不相同,那就意味着改动太大了,也不要去费心考虑是不是原来那个树形的根节点被移动到其他地方去了,直接认为原来那个树形结构已经没用,可以扔掉,需要重新构建新的DOM树,原有的树形上的React组件会经历“卸载”的生命周期。
这时候,componentWillUnmount方法会被调用,取而代之的组件则会经历装载过程的生命周期,组件的componentWillMount、render和componentDidMount方法依次被调用。
也就是说,对于Virtual DOM树这是一个“更新”的过程,但是却可能引发这个树结构上某些组件的“装载”和“卸载”过程。
举个例子,在更新之前,组件的结构是这样:
我们想要更新成这样:
那么在做比较的时候,一看根节点原来是div,新的根节点是span,类型就不一样,那么这个算法就认为必须要废掉之前的div节点,包括下面所有的子节点,一切推倒重来,重新构建一个span节点以及其子节点。
很明显,这是一个巨大的浪费,因为div和span的子节点其实是一模一样的Todos组件,顶层的元素实际上不做什么实质的功能,但是仅仅因为类型不同就把本可以重用的Todos组件卸载掉,然后重新再把这个组件装载一遍。
虽然是浪费,但是为了避免O(N3)的时间复杂度,React必须要选择一个更简单更快捷的算法,也就只能采用这种方式。
作为开发者,很显然一定要避免上面这样浪费的情景出现。所以,一定要避免作为包裹功能的节点类型被随意改变,像上面的例子中,把div换成span只会带来没有必要的组件重新装载。
如果React对比两个树形结构的根节点发现类型相同,那么就觉得可以重用原有的节点,进入更新阶段,按照下一小节的步骤来处理。
2.节点类型相同的情况
如果两个树形结构的根节点类型相同,React就认为原来的根节点只需要更新过程,不会将其卸载,也不会引发根节点的重新装载。
这时,有必要区分一下节点的类型,节点的类型可以分为两类:一类是DOM元素类型,对应的就是HTML直接支持的元素类型,比如div、span和p;另一类是React组件,也就是利用React库定制的类型。
对于DOM元素类型,React会保留节点对应的DOM元素,只对树形结构根节点上的属性和内容做一下比对,然后只更新修改的部分。
比如原本的节点用JSX表示是这样:
Hello World
改变之后的JSX表示是这样:
Good Bye
可以看到,二者的差别是div中的文字发生了改变,另外className也发生了变化。style中有一个字段color发生了改变。React可以对比发现这些属性和内容的变化,在操作DOM树上节点的时候,只去修改这些发生变化的部分,让DOM操作尽可能少。
如果属性结构的根节点不是DOM元素类型,那就只可能是React组件类型,那么React做的工作类似,只是React此时并不知道如何去更新DOM树,因为这些逻辑还在React组件之中,React能做的只是根据新节点的props去更新原来根节点的组件实例,引发这个组件实例的更新过程,也就是按照顺序引发下列函数:
·shouldComponentUpdate
·componentWillReceiveProps
·componentWillUpdate
·render
·componentDidUpdate
在这个过程中,如果shouldComponentUpdate函数返回false的话,那么更新过程就此打住,不再继续。所以为了保持最大的性能,每个React组件类必须要重视should-ComponentUpdate,如果发现根本没有必要重新渲染,那就可以直接返回false。
在处理完根节点的对比之后,React的算法会对根节点的每个子节点重复一样的动作,这时候每个子节点就成为它所覆盖部分的根节点,处理方式和它的父节点完全一样。
3.多个子组件的情况
当一个组件包含多个子组件的情况,React的处理方式也非常简单直接。
拿Todo应用中的待办事项列表作为例子,假如最初的组件形态用JSX表示是这样:
在更新之后,用JSX表示是这样:
那么React会发现多出了一个TodoItem,会创建一个新的TodoItem组件实例,这个TodoItem组件实例需要经历装载过程,对于前两个TodoItem实例,React会引发它们的更新过程,但是只要TodoItem的shouldComponentUpdate函数实现恰当,检查props之后就返回false的话,就可以避免实质的更新操作。
上面的例子是在TodoItem序列后面增加一个新的TodoItem实例,接下来我们看另外一个例子,在序列前面增加一个TodoItem实例,就会暴露出一个问题,JSX代码如下:
从直观上看,内容是“Zero”的新加待办事项被插在了第一位,只需要创造一个新的组件TodoItem实例放在第一位,剩下两个内容为“First”和“Second”的TodoItem实例经历更新过程,但是因为props没有改变,所以shouldComponentUpdate可以帮助这两个组件不做实质的更新动作。
可是实际情况并不是这样。如果要让React按照上面我们构想的方式来做,就必须要找出两个子组件序列的不同之处,现有的计算出两个序列差异的算法时间是O(N2),虽然没有树形结构比较的O(N3)时间复杂度那么夸张,但是也不适合一个对性能要求很高的场景,所以React选择看起来很傻的一个办法,不是寻找两个序列的精确差别,而是直接挨个比较每个子组件。
在上面的新TodoItem实例插入在第一位的例子中,React会首先认为把text为First的TodoItem组件实例的text改成了Zero,text为Second的TodoItem组件实例的text改成了First,在最后面多出了一个TodoItem组件实例,text内容为Second。这样操作的后果就是,现存的两个TodoItem实例的text属性被改变了,强迫它们完成一个更新过程,创造出来的新的TodoItem实例用来显示Second。
理想情况下只需要增加一个TodoItem组件,实际上引发了两个TodoItem实例的更新,而且,假设有100个TodoItem实例,那就会引发100个TodoItem实例的更新,这明显就是一个浪费。
看起来的确很傻,但电脑不是人类,一个简单的算法就只能用这种方式处理问题。
当然,React并不是没有意识到这个问题,所以React提供了方法来克服这种浪费,不过需要开发人员在写代码的时候提供一点小小的帮助,这就是key的作用。 深入浅出React和Redux