react和vue作为两个目前影响力最大的两套框架,在性能提升方面,都采用了虚拟dom这种理念,在内存中运行,而不会马上改变dom结构。再一个就是使用了diff算法,但是直接diff两棵树会有问题,考虑到一个树上的节点可以复用,以及可以增加或者删除,会导致时间复杂度过大O(n*n*n)
怎么解决?
react和vue都是默认同级比较的,这样就只需要考虑新增以及删除节点的操作复杂度O(n),极大的提高了效率。
当然,vue和react的具体实现还是不同的,vue在diff的时候调用patch函数给dom打补丁,而且当节点元素的type和key相同时,如果className不同的化,vue也会选择直接销毁,而react则会修改属性。但是,还有个不同,那就时列表渲染的时候,vue会采用双指针的方式比较,而react则是设置lastPlacedIndex这个值作为参考,效率上面比vue要稍微差一些?但是为什么呢?
因为react里面的fiber对象时通过单向链表的形式存在的,所以不能用数组里面的优化手段,双指针。
那么就进入正题了,react如何diff?
diff的实质就是对比显示在页面上的current fiber树和JSX对象生成一个workInProgress fiber树的过程。
在源码中diff的入口函数是:reconcilerChildFibers(),这个函数会判断传入的参数的类型,也就是newChild的类型。
1.单节点diff。对应newChild对象为Object,string,number类型的时候。则执行reconcilerSingleElement()。在函数里面会先比较key,再比较type这是个重点,也是react的优化操作。如果key不同,则给child标记删除,若key相同,比较type,如果type不同,则直接删除child及其兄弟节点,因为唯一的key相同但是type不同,其他的就无需比较。type相同,则复用type。
2.多节点diff。对应的newChild类型为Array类型,同级有多个节点。执行reconcilerChildArray()。
这个阶段分两轮遍历,第一轮遍历更新,第二轮遍历增删的情况。(其实这也是react团队在大量的人机交互之后总结出来的,更新的频率比增删的更高)。
第一轮遍历:遍历newChildren,遇到第一个key不同的场景直接跳出遍历,key相同但是type不同则标记DELETION。直到新旧children被遍历完。
第二轮更新:增删直接就根据打好的标记执行相应的操作就好了,重头戏是移动。那react里面针对移动是一个什么算法呢?
// 之前
abcd
// 之后
badc
首先确定移动的参照点:lastPlacedIndex。
这个单词的意思可以理解为最新的移动后的元素的位置索引。如上图,在第二轮遍历时,b此时在之前的位置索引为2,因此lastPlacedIndex=1,移动到a,a的位置索引为0<1,则a向后移动,到d,d的位置索引为3>1,改变lastPlacedIndex的值=3,不移动,最后c的索引为2<3,向后移动c。完成移动过程。这里还有个亮点就是:第一轮没有遍历完的oldChild会被映射为Map结构,key是key,value就是对应的fiber。方便查找。
记录自己的学习,如果还能帮到你就更好了。如果有不正确的地方还请批评指正!