What -- 什么是 Diff 算法(1)
对于需要 update 的组件,React 会将当前组件与该组件上一次更新后渲染的 Fiber 做比较,这个比较用到的方法,就是 Diff 算法。 Diff 算法就是更新过程中,比对待更新组件与现有组件差别,处理差别的过程 Diff 算法后,会跟生成一些处理 effectTag,然后 React 会根据这些 tag 更新出新的组件
和 Diff 算法相关的概念词语
对于视图上一个已有的 DOM 节点
,在 Diff 算法中会有以下概念与其相关
- current Fiber: 当前 DOM 节点对于的 fiber 节点,是 DOM 节点的 fiber 描述
- workInProgress Fiber: 如果 DOM 节点将要在本次更新中渲染到页面中,那么 workInProgress Fiber 就是将要替换的 DOM 节点对应的 fiber 节点。也就是更新后的 DOM 对应的 fiber ,但是现在还处于 fiber 节点
- DOM 节点: 处于视图中,描述某一次页面的节点
- JSX 对象: ClassComponent 的 render 方法返回的结果,或者 FunctionComponent 的调用结果;JSX 对象和 DOM 节点是关联关系,是描述 DOM 节点响应信息的对象。
Diff 算法就是比对 JSX 对象和 current Fiber,最后生成 workInprogress Fiber 的过程,这个过程也是 reconcile 过程 workInprogress Fiber 最后会被渲染成新的 DOM,这个就是直接渲染。
Why -- 为什么要用 Diff 算法 / 为什么需要特定的 Diff 算法,其他的方法有啥缺点吗?
Diff 算法就是比对 currentFiber 树与 jsx 对象,最后生成 workInprogress 树的过程,其中 jsx 对象也相当于是 dom 树的映射
- 完全比对两个树的算法,时间复杂度要达到 , 这个复杂度太高,需要耗费的资源太大,所以归纳常用特性后, React 决定对比对做限制条件,进而降低复杂度,而这些限制条件下的比对算法,就是 Diff 算法。
How1 -- Diff 算法是怎样的 / 有限制的算法中的限制是什么
总原则
- 只对同级元素进行 Diff。如果一个 DOM 节点在更新前后跨越了层级,那么 React 会在 WIP Fiber 树对应的位置删除旧节点树,然后再新的位置重新构建
- 两个不同类型的元素会产生不同的树。DOM 节点更改了它的标签,对应于 fiber 就是类型发现了变化,例如
div -> p
,那么会先删除 div 对应的整个子树,然后再在对应的位置新增一个 p 树 - 使用 key prop 来暗示那些元素在不同的渲染下能保持稳定
理解
Q: 首先我们要定位,什么时候才叫 DOM 节点跨越了层级,怎么判断更新前后两个元素是同一个元素呢?
- 如果有 key,且 key 唯一,那么就可以按照 key 来判断更新前后的节点是哪一组两个
- 如果没有 key,那么就按照位置算,比方说
div > p a p
变成了div > a p p
,如果没有 key 标志,就会先删除 p,然后再对应生成一个 a;然后再删除 a,对应位置生成 p
Q: key 的作用
- 大力出奇迹,没有 key 的时候,直接同一级对比,一旦类型不对,直接删除全部,重建就好
- 如果有 key 了,那么每一个 jsx 对应的节点就需要遍历 current Fiber 同一层级的所有的节点,找出 key 相同的那个节点,然后再看看是删除还是更新;
Q: 为什么说 key 不一定提升效率
- 虽然 key 可以避免位置转移的更新,不效率的使用删除新增的方法进行更新
- 但是存在 key 的时候,每一个节点都需要遍历所有的同层级节点,找到对应的节点,这个遍历过程本来就是一种资源消耗,如果在叶子节点中,那么删除新增的方法,会比遍历查找后更新,耗费更少的资源。
- 当然 React 是推荐使用 key 的,因为统计来看,一次性删除新增那么大的子树,比遍历查找耗费的资源多的多。
How2 -- Diff 是如何实现的
概述
- Diff 算法通过 DFS 的方式 JSX 节点的 child 去和同一层级的 fiber 进行对比,然后返回对应的结果
- 首先判断 child 的类型来处理,child 可能是对象、字符串、数组等等
- Diff 算法有两种: 单节点 Diff 和多节点 Diff
- 单节点就是 child 只有一个,这个时候 child 可能是 string/number,统一当文档处理;如果是对象,则就是一个组件,那么就再次分类了
- REACT_ELEMENT_TYPE 普通组件的处理
- REACT_PORTAL_TYPE 入口组件的出来
- REACT_LAZY_TYPE xxx
- 多节点就是 child 是数组或者类数组,那么就需要更复杂的比对方法了
function reconcileChildFibers(
returnFiber, // 父 fiber
currentFirsetChild, // 当前层级的第一个 child,
newChild, // jsx 的一个节点对应的对象
) {
// 如果
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
// object 类型,可能是 REACT_ELEMENT_TYPE 或者是 REACT_PORTAL_TYPE 或者 REACT_LAZY_TYPE
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
//todo
case REACT_PORTAL_TYPE:
//todo
case REACT_LAZY_TYPE:
//todo
}
}
// 使用单节点文本的diff 算法
if (typeof newChild === 'string' || typeof newChild === 'number') {
// 调用 reconcileSingleTextNode 处理文本节点
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
// 数组
if (isArray(newChild)) {
// 数组
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 类数组
if (getIteratorFn(newChild)) {
// Interator,和数组类似
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 其他情况,直接删除节点直接新增
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
各个 Diff 算法的实现
未完待续...
小结
- Diff 算法发送在 React 的 reconcile 阶段,通过比对 jsx 对象和当前展示在视同中的 dom 对应的 currentFiber 做比对和处理的方法。
- 因为完全比对两课树的复杂度太高,耗费资源太多,所以根据不为人知的统计后,设计了一种限制性的比对处理方法,就是 Diff 方法了
- key 是 Diff 方法中很重要的一个属性,关乎在多节点 Diff 中使用哪种方式处理节点,但是使用 key 并不一定就是优化性能了。
- Diff 有三大规范:同层级比对,不同类型节点删除,key的使用
- 比对过程中,给定 jsx child 是对象数组形式,而同层级 fiber 是一种链式结构,通过 sibling 链接对应的兄弟 fiber。