React Diff流程
前提
在React的render阶段的beginWorl方法中对于update的组件会通过reconcileChildFibers方法将当前组件与该组件在上次更新时对应的Fiber节点进行比较(即diff算法),将比较的结果生成新的Fiber节点,即创建workInProgress fiber的过程。
其中,在某一时刻,一个DOM节点最多会存在4个节点与它相关:
- current fiber,页面中正在显示的DOM对应的Fiber节点
- workInProgress fiber,内存中正在构建的Fiber节点
- DOM节点本身
- JSX对象,ClassComponent或者FunctionComponent的返回值
diff算法的本质就是对比current fiber和JSX对象,然后生成workInProgress fiber。
DIff的瓶颈
因为diff操作本身会带来性能损耗,即算法复杂度过高,所以React的应对方法是预设三个限制:
- 只对同级元素进行diff
- 两个不同类型的元素会产出不同的fiber树
- 设置key属性
Diff的实现
reconcileChildFibers方法中对同级的节点针对节点的数量会分为两类进行处理:单节点的DIff和多节点的Diff。
单节点的Diff
首先React会判断前后元素的key的值是否相等,如果相等则会继续比较type的值是否相等,如果都相等则表示可以复用current fiber节点部分属性来创建workInProgress fiber节点。
如果前后元素的key相等,但是type的值不相等,那么会将current fiber以及它的兄弟fiber节点都标记为删除。
如果前后元素的key不相等,那么将current fiber节点标记删除。
然后接着移动sibling指针继续比较current fiber的兄弟fiber节点与当前元素进行比较,重复上述的过程。
多节点的diff
多节点的diff会经历两轮遍历:
- 第一轮遍历处理更新的节点
- 第二轮遍历处理剩下不属于更新的节点
第一轮遍历
主要流程还是会通过遍历current fiber和当前同级元素进行key和type的比较。
- 如果key和type都相同,则会复用current fiber节点来创建workInProgress fiber节点
- 如果key相同,但type不同,则会重新创建workInProgress fiber节点
- 如果key不同,则直接退出第一轮遍历
如果newChildren遍历完,也会退出第一轮遍历。
第二轮遍历
在经过第一轮遍历之后会出现四种情况:
- 情况一:newChildren 和 oldFiber 同时遍历完了
- 情况二:newChildren 没有遍历完,oldFiber 遍历完了
- 情况三:newChildren 遍历完了,oldFiber 没有遍历完了
- 情况四:newChildren 和 oldFiber 都没有遍历完
针对情况 1 和 情况 3 oldFiber 遍历完的情况,将剩余的 newChildren 标记为插入。
针对情况 2 newChildren 遍历完的情况,将剩下的 oldFiber 标记为删除。
针对最为复杂的情况 4 要做的是处理移动的节点。而对于移动的节点可能进行的操作是新增和删除。
哪如果判断一个节点在更新前后是否发生了移动呢?找到一个参照物,然后比较当前元素和该参照物在更新前后的相对位置是否发生了变化。
为此 React 定义了两个关键变量 oldIndex 和 lastPlacedIndex。
- oldIndex 表示通过遍历 newChildren 拿到的该元素对应 oldFiber 中该节点的 index
- lastPlacedIndex 表示 oldFiber 当中最后一个可复用元素的 index
这个根据 newChildren 遍历到的元素查找对应 oldFiber 的过程是:先将剩余的 oldFiber 都保存在变量名为为 existingChildren 的 map 数据结构。oldFiber 的 key 为 key,oldFiber 为 value。
然后通过遍历到的 newChildren 元素的 key 属性找到对应的 oldFiber 节点,再根据两者的 type 是否相同决定是否复用该 oldFiber。如果相同则复用 current fiber 创建 workInProgress fiber 节点。如果不相同则重新创建 workInProgress fiber 节点,然后标记为插入。
lastPlacedIndex 的初始值为 0。通过比较 oldIndex 和 lastPlacedIndex 的大小就能判断当前元素的位置是否发生了移动。
如果 oldIndex >= lastPlacedIndex,表示当前元素和参照物的相对位置没有发生变化, 所以可以复用该元素对应的 current fiber 节点来创建 workInProgress fiber。然后将 oldIndex 赋值给 lastPlacedIndex,继续遍历下一个元素。
而如果 oldIndex < lastPlacedIndex,则表示当前元素和参照物的相对位置发生了变化,则将该元素的 current fiber 标记为删除,然后重新创建 workInProgress fiber 并标记插入。
(举例:oldFiber 中有两个节点:fiber 1 和 fiber 2。newChildren 有两个元素:element 2 和 element 1。 因为 newChildren 的遍历顺序是从前往后的,也就是说会先遍历element 1,再遍历element 2。element 2 对应 oldFiber 中 fiber 2 的 index 为 1,可以复用 fiber 2节点。然后赋值 lastPlacedIndex = oldIndex = 1。继续遍历 element 1,element 1 对应的 oldFibr 中 fiber 1 的 index 为 0,oldIndex < lastPlacedIndex,说明 element 1 在更新前后的位置发生了变化,所以将 fiber 1 标记删除,根据 element 1 重新创建的 workInProgress fiber 标记为插入)
直到遍历完 newChildren,再遍历 existingChildren 将剩下的 oldFiber 都标记为删除。
结束 diff 算法。
标签:oldFiber,current,fiber,React,遍历,newChildren,Diff,节点 From: https://www.cnblogs.com/rocenjs/p/18357605