简单的学习一下 vue3.0 中的 diff 过程
const patchChildren = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized = false) => {
const c1 = n1 && n1.children;
const prevShapeFlag = n1 ? n1.shapeFlag : 0;
const c2 = n2.children;
const { patchFlag, shapeFlag } = n2;
if (patchFlag === -2 /* BAIL */) {
optimized = false;
}
// fast path
// 这个 patchFlag 是在 编译的时候 生成的,对于节点来说,一般都是 0
if (patchFlag > 0) {
if (patchFlag & 128 /* KEYED_FRAGMENT */) {
// this could be either fully-keyed or mixed (some keyed some not)
// presence of patchFlag means children are guaranteed to be arrays
patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
return;
}
else if (patchFlag & 256 /* UNKEYED_FRAGMENT */) {
// unkeyed
patchUnkeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
return;
}
}
// children has 3 possibilities: text, array or no children.
// 子节点有三种可能:文本子节点,数组子节点,或者没有子节点
if (shapeFlag & 8 /* TEXT_CHILDREN */) { // 当前的子节点是 文本节点的时候
// text children fast path
if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) { // 如果之前的子节点是 数组的话,就直接把之前的节点 给清除
unmountChildren(c1, parentComponent, parentSuspense);
}
if (c2 !== c1) { // 如果 之前的子节点 和新的子节点不相同,直接使用 新的 文本替换以前的子节点
hostSetElementText(container, c2);
}
}
else {
if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) { // 如果之前的节点是 数组节点的话
// prev children was array
if (shapeFlag & 16 /* ARRAY_CHILDREN */) { // 如果当前节点也是 数组节点的话,就进入 diff 的过程
// two arrays, cannot assume anything, do full diff
patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
else { // 如果 当前的节点 不是 文本节点,也不是数组节点,那么就是空节点,删除 老的 节点
// no new children, just unmount old
unmountChildren(c1, parentComponent, parentSuspense, true);
}
}
// 现在的情况就是 之前的节点 要么是 文本节点,或者为空
// 而新的节点 要么是 数组,要么是空
else {
// 新的节点是 数组 或者 为空节点
if (prevShapeFlag & 8 /* TEXT_CHILDREN */) { // 如果 之前的 节点是文本节点,那么 直接清除
hostSetElementText(container, '');
}
// mount new if array
if (shapeFlag & 16 /* ARRAY_CHILDREN */) { // 现在以前的子节点已经是空了,如果新的节点是数组,生成新的节点
mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
}
}
};
- 获取子节点和子节点的类型,新节点 n2 和 旧节点 n1 中,一共有三种类型的子节点:文本节点,数组节点,空节点
- 如果 新节点是 文本节点,而 老节点 是数组节点的话,就删除老姐点,这样老节点就是空,然后插入文本节点
- 如果 新节点是 文本节点,老节点 是文本节点,比较新旧节点 文本 是否一致,否就 替换文本内容
- 如果 老节点是数组节点,而新节点也是数组节点的话,进入 patchKeyedChildren ,也就是 diff 的过程
- 如果 老节点是数组节点,而新节点是空节点 的话,进入 删除老节点
- 这样比较下来,剩下的情况就是 : 之前的节点 要么是 文本节点,或者为空,而新的节点 要么是 数组,要么是空
- 所以如果之前的节点是文本节点,删除 老节点的文本内容
- 如果 新节点是数组,就把新节点添加到 dom 树上去
- 最后就剩下新节点是空节点,不做任何操作(在第7点已经把老节点删除了)
function normalizeVNode(child) {
// 如果 当前节点为空,创建注释 vnode
if (child == null || typeof child === 'boolean') {
// empty placeholder
return createVNode(Comment);
}
// 如果 当前节点 是数组,创建 一个 Fragment 节点,并把子节点 传入
else if (isArray(child)) {
// fragment
return createVNode(Fragment, null, child);
}
// 如果当前节点是一个 对象,那么 就是 vnode
else if (typeof child === 'object') {
// 如果存在 el属性,说明已经 经历了 从 vnode 转变为 element 挂载的过程,就克隆一份
return child.el === null ? child : cloneVNode(child);
}
// 最后就是一个 基础类型的节点了,数字 或者 文本
else {
return createVNode(Text, null, String(child));
}
}
function cloneIfMounted(child) {
// 如果存在 el属性,说明已经 经历了 从 vnode 转变为 element 挂载的过程,就克隆一份
return child.el === null ? child : cloneVNode(child);
}
function isSameVNodeType(n1, n2) {
return n1.type === n2.type && n1.key === n2.key;
}
这里 的 是否是 相同节点对比于以前是有了很大的变化的,偏向于 react 的方向变化,看下面的 代码就是以前Vue2.x的 sameNode
// 两个 vnode 是否相同
function sameVnode(a, b) {
return (
// 需要注意的是 null === null 为 true
// 也就是说,如果 没有设置 key 的话,那么就默认为 两个节点的key 是一致的
// 对比于 react 的diff ,这里的判断 多出了 tag、comment 和 input 的判断
// 而 react 则着重于 $$type 的判断
// 这里 react 明显的 对于 jsx 的分类 更倾向于 自己的判断,而 vue 则是 倾向于 用户的输入
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
// 是否是 同一个类型的 input 标签,这个很好理解
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
// 是否是 相同类型的 input 节点,比如 radio,text,checkbox等
function sameInputType(a, b) {
if (a.tag !== 'input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0;
// 新子节点的长度
const l2 = c2.length;
// 旧子节点的 排尾
let e1 = c1.length - 1; // prev ending index
// 薪子节点的 排尾
let e2 = l2 - 1; // next ending index
// 从头开始 遍历 新旧节点
// 首先 获取 当前 index 下的子节点,然后 对新节点 获取 对应的 vnode
while (i <= e1 && i <= e2) {
const n1 = c1[i];
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i]) // 如果新节点经历了挂载的阶段,就克隆一份
: normalizeVNode(c2[i])); // 否则就创建一个 新节点的 vnode
// 如果是相同的节点,也就是 type 和 key 相同
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized);
}
else {
// 如果有不同的,立刻结束循环
break;
}
i++;
}
// 从尾遍历 新旧节点,过程和上面几乎一样
while (i <= e1 && i <= e2) {
const n1 = c1[e1];
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2])
: normalizeVNode(c2[e2]));
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized);
}
else {
break;
}
e1--;
e2--;
}
// 3. common sequence + mount
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
// 遇到 类似于 旧节点为空,而新节点 依旧存在值 的情况下,
// 也就是 旧节点 比较完毕了
// 插入新节点
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1;
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
while (i <= e2) {
patch(null, (c2[i] = optimized
? cloneIfMounted(c2[i])
: normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG);
i++;
}
}
}
// 4. common sequence + unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
// 新节点比较完毕了
// 删除 多余的 旧节点
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true);
i++;
}
}
// 5. unknown sequence
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
// 最后,也就是 最普遍的情况,也就是 当前的 新旧节点都有剩余
else {
const s1 = i; // 旧节点比较的开始索引
const s2 = i; // 新节点比较的开始索引
const keyToNewIndexMap = new Map();
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i])
: normalizeVNode(c2[i]));
// 当 key 存在的时候,为 新节点 建立 一个 key 和 当前节点 index 对应关系的 Map
if (nextChild.key != null) {
// 如果 key 重复了,报个 错
if ( keyToNewIndexMap.has(nextChild.key)) {
warn(`Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.`);
}
keyToNewIndexMap.set(nextChild.key, i);
}
}
// 5.2 loop through old children left to be patched and try to patch
// matching nodes & remove nodes that are no longer present
let j;
let patched = 0;
// 新节点还有没有 遍历 到的长度
const toBePatched = e2 - s2 + 1;
let moved = false;
// used to track whether any node has moved
// 用来标记是否 有节点被移动了
let maxNewIndexSoFar = 0;
// 这个数组存储新子序列中的元素在旧子序列节点的索引,用于确定最长稳定子序列
//
// 遍历这个数组,填上默认字符 0
const newIndexToOldIndexMap = new Array(toBePatched);
for (i = 0; i < toBePatched; i++)
newIndexToOldIndexMap[i] = 0;
// 遍历剩下的旧节点
for (i = s1; i <= e1; i++) {
const prevChild = c1[i];
// 所有的新节点都被 patch 了,这里就是把以前的节点删除
if (patched >= toBePatched) {
// all new children have been patched so this can only be a removal
unmount(prevChild, parentComponent, parentSuspense, true);
continue;
}
let newIndex;
// 如果老的节点 存在 key
if (prevChild.key != null) {
// 在之前建立的 Map 中 获取 和老节点 的 key 相同的 index
newIndex = keyToNewIndexMap.get(prevChild.key);
}
else {
// key-less node, try to locate a key-less node of the same type
// 如果 没有设置 key 的话,就只能去遍历新节点 然后获取 相同类型,且没有设置 key 的新节点了
for (j = s2; j <= e2; j++) {
if (newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j])) {
newIndex = j;
break;
}
}
}
// 如果不存在 可以复用的节点,就删除 当前 的老节点
if (newIndex === undefined) {
unmount(prevChild, parentComponent, parentSuspense, true);
}
else {
// 更新新子序列中的元素在旧子序列中的索引,这里加 1 偏移,是为了避免 i 为 0 的特殊情况
newIndexToOldIndexMap[newIndex - s2] = i + 1;
// 标记 移动的节点
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
}
else {
moved = true;
}
// 更新节点
patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, optimized);
patched++;
}
}
// 5.3 move and mount
// generate longest stable subsequence only when nodes have moved
// 移动和挂载新节点
// 仅当节点移动时生成最长上升子序列
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR;
j = increasingNewIndexSequence.length - 1;
// looping backwards so that we can use last patched node as anchor
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i;
const nextChild = c2[nextIndex];
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
// 有移动的节点就会在 重新设定 偏移的 index,
// 所以依旧为 0 的话,说明当前节点没有发生移动,就是没有在 旧节点 中找到对应的节点
// 挂载 新节点
if (newIndexToOldIndexMap[i] === 0) {
// mount new
patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG);
}
// 如果需要移动的话
else if (moved) {
// move if:
// There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence
// 或者当前没有最长上升子序列
// 当前的节点 不在 最长上升子序列当中
// 对节点进行移动
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, 2 /* REORDER */);
}
else {
j--;
}
}
}
}
};
- 从头遍历新节点和旧节点,如果有相同节点,更新节点,遇到第一个不一样的,结束循环
- 从尾遍历新节点和旧节点,如果有相同节点,更新节点,遇到第一个不一样的,结束循环
- 如果旧节点 比较完毕了,插入新节点
- 如果新节点 比较完毕了,删除旧节点
- 给新节点 建立一个
的 Map 对象 - 使用 老节点 的key 来查找,如果存在 相同节点,就 patch
- 如果不存在 相同节点,把老节点删除
- 最后再 移动和挂载新节点(这一块有点没看懂,以后再研究研究)
这里就总结一下 Vue 源码中 的 diff 和 vue3.0 的区别,以及部分 react 的 diff 的区别
- 首先 vue3.0 的 diff 很明显 向 react 的 diff 靠拢,取消了 vue2.0 中的 排头 和排尾 ,排尾 和排头的比较
- 比较 也是采用了 一个 循环遍历,遇到第一个不一样的就 退出循环
- 与 比较中 与 react 不同的,还多了一次 从后往前面的遍历,算是 vue2.0 的精华遗产了
- 然后 vue2.0 建立的
是一个 Object ,而 react 和 vue3.0 中的是 Map ,所以现在 在 3.0 中使用 对象作为 key 也是可以的了(在 object 中 使用 对象作为 key 会自动变成 [object Object],所有 的key 都会重复)- 最后执行 移动和挂载新节点