一、前言
有同学问:能否详细说一下 diff 算法。
简单说:diff 算法是一种优化手段,将前后两个模块进行差异化比较,修补(更新)差异的过程叫做 patch,也叫打补丁。
文章主要解决的问题:
-
1、为什么要说这个 diff 算法?
-
2、虚拟 dom 的 diff 算法
-
3、为什么使用虚拟 dom?
-
4、diff 算法的复杂度和特点?
-
5、vue 的模板文件是如何被编译渲染的?
-
6、vue2.x 和 vue3.x 中的 diff 有区别吗
-
7、diff 算法的源头 snabbdom 算法
-
8、diff 算法与 snabbdom 算法的差异地方?
二、为什么要说这个 diff 算法?
因为 diff 算法是 vue2.x , vue3.x 以及 react 中关键核心点,理解 diff 算法,更有助于理解各个框架本质。
说到「diff 算法」,不得不说「虚拟 Dom」,因为这两个息息相关。
比如:
-
vue 的响应式原理?
-
vue 的 template 文件是如何被编译的?
-
介绍一下 Virtual Dom 算法?
-
为什么要用 virtual dom 呢?
-
diff 算法复杂度以及最大的特点?
-
vue2.x 的 diff 算法中节点比较情况?
等等
三、虚拟 dom 的 diff 算法
我们先来说说虚拟 Dom,就是通过 JS 模拟实现 DOM ,接下来难点就是如何判断旧对象和新对象之间的差异。
Dom 是多叉树结构,如果需要完整的对比两棵树的差异,那么算法的时间复杂度 O(n ^ 3),这个复杂度很难让人接收,尤其在 n 很大的情况下,于是 React 团队优化了算法,实现了 O(n) 的复杂度来对比差异。
实现 O(n) 复杂度的关键就是只对比同层的节点,而不是跨层对比,这也是考虑到在实际业务中很少会去跨层的移动 DOM 元素。
虚拟 DOM 差异算法的步骤分为 2 步:
-
首先从上至下,从左往右遍历对象,也就是树的深度遍历,这一步中会给每个节点添加索引,便于最后渲染差异
-
一旦节点有子元素,就去判断子元素是否有不同
3.1 vue 中 diff 算法
实际 diff 算法比较中,节点比较主要有 5 种规则的比较
-
1、如果新旧 VNode 都是静态的,同时它们的 key 相同(代表同一节点),并且新的 VNode 是 clone 或者是标记了 once(标记 v-once 属性,只渲染一次),那么只需要替换 elm 以及 componentInstance 即可。
-
2、新老节点均有 children 子节点,则对子节点进行 diff 操作,调用 updateChildren,这个 updateChildren 也是 diff 的核心。
-
3、如果老节点没有子节点而新节点存在子节点,先清空老节点 DOM 的文本内容,然后为当前 DOM 节点加入子节点。
-
4、当新节点没有子节点而老节点有子节点的时候,则移除该 DOM 节点的所有子节点。
-
5、当新老节点都无子节点的时候,只是文本的替换
部分源码 https://github.com/vuejs/vue/blob/8a219e3d4cfc580bbb3420344600801bd9473390/src/core/vdom/patch.js#L501 如下:
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
if (oldVnode === vnode) {
return;
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode);
}
const elm = (vnode.elm = oldVnode.elm);
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
} else {
vnode.isAsyncPlaceholder = true;
}
return;
}
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return;
}
let i;
const data = vnode.data;
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, vnode);
}
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode "i");
if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
// 定义了子节点,且不相同,用diff算法对比
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
// 新节点有子元素。旧节点没有
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
// 检查key
checkDuplicateKeys(ch);
}
// 清空旧节点的text属性
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '');
// 添加新的Vnode
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
// 如果旧节点的子节点有内容,新的没有。那么直接删除旧节点子元素的内容
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1);
// 如上。只是判断是否为文本节点
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '');
}
// 如果文本节点不同,替换节点内容
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
}
}
3.2 React diff 算法
在 reconcileChildren 函数的入参中
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
-
workInProgress:作为父节点传入,新生成的第一个 fiber 的 return 会被指向它。
-
current.child:旧 fiber 节点,diff 生成新 fiber 节点时会用新生成的 ReactElement 和它作比较。
-
nextChildren:新生成的 ReactElement,会以它为标准生成新的 fiber 节点。
-
renderLanes:本次的渲染优先级,最终会被挂载到新 fiber 的 lanes 属性上。
diff 的两个主体是:oldFiber(current.child)和 newChildren(nextChildren,新的 ReactElement),它们是两个不一样的数据结构。
部分源码
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
/* * returnFiber:currentFirstChild的父级fiber节点
* currentFirstChild:当前执行更新任务的WIP(fiber)节点
* newChildren:组件的render方法渲染出的新的ReactElement节点
* lanes:优先级相关
* */
&nbs