/ javascript

React Diff 源码分析

本文主要按照目前React V16.4.1来介绍

Fiber介绍

What is “React Fiber”?
Fiber is the new reconciliation engine in React 16. Its main goal is to enable incremental rendering of the virtual DOM.
官方解释,fiber是React 16中新的reconciliation(diff的逻辑)引擎。它的主要目的是使虚拟DOM能够进行增量渲染。

React之前渲染相关的任务是连续的,现在React的任务则是由一系列Fiber的更新组成的,因此React可以在多个帧中断断续续的更新Fiber,最后commit变化。从某种意义上来说,这些任务单元就是 Fiber。一个Fiber代表了一个任务单元.

Fiber节点的数据结构

具体的数据结构在https://github.com/facebook/react/blob/v16.4.1/packages/react-reconciler/src/ReactFiber.js#L72代码中。这里罗列几个比较重要的属性。

{
    tag: TypeOfWork, // fiber的类型
    
    key: null, // 子元素独一无二的id
    type: any, // 对应的组件类型
    
    alternate: Fiber|null, // 在fiber更新时克隆出的镜像fiber,对fiber的修改会标记在这个fiber上

    return: Fiber|null, // 指向fiber树中的父节点
    child: Fiber|null, // 指向第一个子节点
    sibling: Fiber|null, // 指向兄弟节点

    nextEffect: Fiber | null, // 单链表结构,方便遍历fiber树上有副作用的节点
    pendingWorkPriority: PriorityLevel, // 标记子树上待更新任务的优先级

}

所有Fiber节点构成了一颗树。这棵树在数据结构上是通过单链表的形式构成的,Fiber节点上的chlid和sibling属性分别指向了这个节点的第一个子节点和相邻的兄弟节点。这样就可以遍历整个Fiber树了。

React 16中Diff核心代码

diff代码定位

reconcileChildren实现的就是所谓的的Virtul DOM diff,reconcileChildren是一个入口,但真正的入口是reconcileChildrenAtExpirationTime这个函数。
snipaste_2018-08-02_00-51-25
如果currentnull,表示为初次渲染,调用mountChildFibers创建子节点的Fiber实例。current不为null,则调用reconcileChildFibers,这个方法会diff出effect list。

reconcileChildrenAtExpirationTime这个函数里调用了两个功能相似的函数:mountChildFibersInPlacereconcileChildFibers在源码中我们发现,这两个函数其实是同一个函数,只不过传入的参数不同。
snipaste_2018-08-02_01-15-34

ChildReconciler内部有很多工具函数,最终返回的函数叫reconcileChildFibers,这个函数实现了对子fiber节点的reconciliation。下面我们看reconcileChildFibers函数的实现。reconcileChildFibers这个函数,会根据newChild的类型调用不同的方法。

  • 如果是newChild是一个对象,即单个元素,调用reconcileSingleElement函数,比较key和type,如果相同,复用fiber,删除多余的元素(currentFirstChildsibling)。
  • 如果是string或number,调用reconcileSingleTextNode,删除多余元素,返回text类型的新Fiber节点
  • 如果是array,调用reconcileChildrenArray,后面细讲。
  • 如果是空,deleteRemainingChildren删除老的子元素。

核心代码 reconcileChildrenArray

React16的diff算法采用从链表头部开始比较的算法,是层次遍历,算法是建立在一个节点的插入、删除、移动等操作都是在节点树的同一层级中进行的,核心就是如何diff两个子节点数组。

看一下代码,https://github.com/facebook/react/blob/v16.4.1/packages/react-reconciler/src/ReactChildFiber.js#L728

从头部遍历。第一次遍历新数组,新老index都++,比较新老数组哪些元素是一样的,(通过updateSlot,比较key),如果是同样的就update。

第一次遍历完了,如果新数组遍历完了,那就可以把老数组中剩余的fiber删除了。链接

如果老数组完了新数组还没完,那就把新数组剩下的都插入。链接

如果这些情况都不是(新老数组长度一致),就把所有老数组元素按key放map里,然后遍历新数组,插入老数组的元素,这是移动的情况。链接

最后再删除没有被上述情况涉及的元素(也就是老数组中有新数组中无的元素)链接

下面通过MutationObserver [https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver] 观察上述逻辑:

这里渲染一个可输入的数组。
1

当第一种情况,新数组遍历完了,老数组剩余直接删除(12345→1234 删除 5):

新数组没完,老数组完了(1234→1234567 插入 567):

移动的情况,即之前就存在这个元素,后续只是顺序改变(123 → 4321 插入4,移动2 1):

最后删除没有涉及的元素。