React 源码 - diff算法

157 阅读3分钟

一个DOM节点在某一时刻最多会有4个节点和他相关

  1. current Fiber:页面上DOM节点对应的Fiber节点
  2. workInProgress Fiber:本次更新中DOM节点对应的Fiber节点
  3. DOM节点本身
  4. JSX对象函数组件类组件的调用结果

Diff算法的本质是对比 1 和 4 生成 2 的过程

1、整体流程

Reactdiff三个策略

  • 只对同级元素进行Diff
  • 两个不同类型元素会产生出不同的树
    • 如果元素由div变为p
    • 销毁div及其子孙节点,并新建p及其子孙节点
  • 通过 key 属性来维护哪些子元素在不同的渲染下能保持稳定

2、Diff分为两类

  • 单节点:newChild类型为objectnumberstring
  • 多节点:newChild类型为Array
  • 特点:整个diff过程,就是 currentFirstChild 和 element 做对比,返回新的fiber的过程
单节点diff

key相同

  • type相同时,节点可复用
  • type不同时,将childsibing的fiber都标记删除 (不能被复用)

key不同

  • 仅将childfiber标记删除(交换位置时可能被复用)
多节点diff

第一轮遍历:处理更新的节点

  • 遍历newChildren,将newChildren[i]oldFiber比较
  • 三种情况:
    • key相同type相同:复用
    • key相同type不同:oldFiber标记删除、继续遍历
    • key不同:直接跳出,可能属于交换位置

第二轮遍历:处理剩下的不属于更新的节点

  • newChildrenoldFiber同时遍历完 --> 结束
  • newChildren没遍历完 --> 新增
  • oldFiber没遍历完 --> 删除
  • newChildrenoldFiber都没遍历完(交换位置)
    • 由于有节点改变了位置,所以不能再用位置索引i对比前后的节点
    • 将所有还未处理的oldFiber存入以key为key,oldFiber为value的Map
    • 遍历剩余的newChildren,通过newChildren[i].key就能在existingChildren中找到key相同的oldFiber

3、案例

比如父节点下有 A、B、C、D 四个子节点,那渲染出的 vdom 就是这样的:

经过 reconcile 之后,会变成这样的 fiber 结构:

那如果再次渲染的时候,渲染出了 A、C、B、E 的 vdom,这时候怎么处理呢?

再次渲染出 vdom 的时候,也要进行 vdom 转 fiber 的 reconcile 阶段,但是要尽量能复用之前的节点。 那怎么复用呢?一一对比下不就行了?先把之前的 fiber 节点放到一个 map 里,key 就是节点的 key:

然后每个新的 vdom 都去这个 map 里查找下有没有可以复用的,找到了的话就移动过来,打上更新的 effectTag:

这样遍历完 vdom 节点之后,map 里剩下一些,这些是不可复用的,那就删掉,打上删除的 effectTag;如果 vdom 中还有一些没找到复用节点的,就直接创建,打上新增的 effectTag。

这样就实现了更新时的 reconcile,也就是上面的 diff 算法。其实核心就是找到可复用的节点,剩下的旧节点删掉,新节点新增。这样就完成了新的 fiber 结构的创建,也就是 reconcile 的过程。

比如上面那个例子,第一轮遍历就是这样的:

一一对比新的 vdom 和 老的 fiber,发现 A 是可以复用的,那就创建新 fiber 节点,打上更新标记。

C 不可复用,所以结束第一轮遍历,进入第二轮遍历。

把剩下的 老 fiber 节点放到 map 里,然后遍历新的 vdom 节点,从 map 中能找到的话,就是可复用,移动过来打上更新的标记。

遍历完之后,剩下的老 fiber 节点删掉,剩下的新 vdom 新增。

这样就完成了更新时的 reconcile 的过程。