对比情况
array -> array
- 新旧左侧对比,取出差异数据下标
- 新旧右侧对比,锁定右侧差异位置
- 新旧数据对比,新增少删
例子
参考 上一篇 中的 ArrayToArray.js
前导思考
Vue 的 diff中最重要的就是乱序部分的更新,要确定更新部分的位置,及途中绿框部分。Vue的diff做了几步
- 第一步,首部diff。从新老数据的下标0的位置开始对比,即a开始,对比到c、e发现两者不同,停止首部diff,记录当前差异位置
- 第二部,尾部diff。从新老数据末端开始,即g开始,对比到e、c发现两者不同,停止尾部diff,记录当前差异位置
- 第三部,确立差异数据块,做替换。
代码实现
几个核心参数
- el: 右侧老数据的差异位置
- e2: 右侧新数据的差异位置
- i: 左侧差异位置
头部对比
例子
// (a b) c
// (a b) d e
const prevChildren = [
h('p', { key: 'A' }, 'A'),
h('p', { key: 'B' }, 'B'),
h('p', { key: 'C' }, 'C'),
]
const nextChildren = [
h('p', { key: 'A' }, 'A'),
h('p', { key: 'B' }, 'B'),
h('p', { key: 'D' }, 'D'),
h('p', { key: 'E' }, 'E'),
]
实现
function patchChildren(
n1: any,
n2: any,
container: any,
parentComponent: any,
) {
// other code
if (n2ShapeFlags === ShapeFlags.TEXT_CHILDREN) {
/** array -> text
* 1. 删除子元素
* 2. 重新赋值
*/
unmountChildren(n1Child);
hostSetElementText(el, n2Child);
} else {
patchKeyedChildren(
n1Child,
n2Child,
container,
parentComponent
);
}
}
function patchKeyedChildren(
c1,
c2,
container,
parentComponent,
) {
/** array -> array
* 1. 左侧对比,取出差异目标下标
* 2. 右侧对比,锁定当前差异右侧位置
* 3. 新老对比,新增少删
*/
let e1 = c1.length - 1;
let e2 = c2.length - 1;
let i = 0;
// 对比是否相同数据
function isSameVNode(c1, c2) {
return c1.key === c2.key && c1.type === c2.type;
}
/** 1. 左侧对比,取出左侧差异目标下标
* 循环的边界在两个数组长度内
* 从左到右对比,判断相同,继续向右查询,直至差异结束
*/
while (i <= e1 && i <= e2) {
if (isSameVNode(c1[i], c2[i])) {
// 相同,再深度patch
patch(c1[i], c2[i], container, parentComponent);
} else {
// 有差异,退出循环
break;
}
i++;
}
console.log("左侧差异的位置", i);
}
判断的时候用到了 key ,需要拓展一下对应的函数 vnode.ts
export function createdVNode(type, props?, children?) {
// 将根组件转换为vnode,再将其暴露
const vnode = {
type,
props,
children,
key: props && props.key,
};
return vnode;
}
h.ts
export function h(type, props?, children?) {
return createdVNode(type, props, children);
}
结合图片消化
尾部对比
例子
// a (b c)
// d e (b c)
const prevChildren = [
h('p', { key: 'A' }, 'A'),
h('p', { key: 'B' }, 'B'),
h('p', { key: 'C' }, 'C'),
]
const nextChildren = [
h('p', { key: 'D' }, 'D'),
h('p', { key: 'E' }, 'E'),
h('p', { key: 'B' }, 'B'),
h('p', { key: 'C' }, 'C'),
]
实现
function patchKeyedChildren(
c1,
c2,
container,
parentComponent
) {
// 1. 左侧对比
// other code
/** 2. 右侧对比,锁定右侧差异目标
* 循环的边界在**左侧差异位置i**到**两个数组长度**之间
* 在尾部开始判断,所以取的是对应children长度
* 从右到左对比,判断相同,继续向左查询,直至差异
*/
while (i <= e1 && i <= e2) {
if (isSameVNode(c1[e1], c2[e2])) {
// 相同,再深度patch
patch(
c1[e1],
c2[e2],
container,
parentComponent
);
} else {
// 有差异,退出循环
break;
}
e1--;
e2--;
}
console.log("右侧差异位置-旧的", e1);
console.log("右侧差异位置-新的", e2);
}
结合图片消化
新老数据对比-新增
例子
新数据在尾部
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
const prevChildren = [h('p', { key: 'A' }, 'A'), h('p', { key: 'B' }, 'B')]
const nextChildren = [
h('p', { key: 'A' }, 'A'),
h('p', { key: 'B' }, 'B'),
h('p', { key: 'C' }, 'C'),
h('p', { key: 'D' }, 'D'),
]
新数据在头部
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
const prevChildren = [h('p', { key: 'A' }, 'A'), h('p', { key: 'B' }, 'B')]
const nextChildren = [
h('p', { key: 'C' }, 'C'),
h('p', { key: 'A' }, 'A'),
h('p', { key: 'B' }, 'B'),
]
实现
改写insert,新增了 anchor 入参,因为之前的insert只支持数据插入到最后,不能插入到对应位置,所以我们需要给一个对应的anchor,把数据插入到对应的锚点位置
export function insert(el, parent, anchor) {
/** 根据锚点插入到对应位置
* 1. anchor为null默认插到尾部
* 2. anchor不为空,则插到anchor对应的元素之前
*/
parent.insertBefore(el, anchor || null);
}
实现头部、尾部数据插入
function patchKeyedChildren(
c1,
c2,
container,
parentComponent,
parentAnchor
) {
// 1. 左侧对比
// 2. 右侧对比
// other code
/** 3. 新的比旧的长,添加元素
* 1. 改写insert,支持插入到对应位置
* 2. i 为新老数据左侧的差异位置,e1、e2为数据右侧的差异位置
* 3. i > e1,说明新的比旧的长,需要插入数据
* 4. i > e2,说明新的比旧的短,需要删除数据
*/
const l2 = c2.length - 1;
/** 插入数据
* 1. 左侧 i 大于 e1,说明新数据比旧数据多,要把新数据插入
* 2. 添加范围在新数据长度内
*/
if (i > e1 && i <= e2) {
/** nextPos用来判断插入数据的位置
* 1. nextPos为新数据差异位的后一个元素的锚点位置
* 2. 如果锚点超出新数据children长度,则没有找到对应的锚点元素,则插到尾部
* 3. 如果锚点在新数据children长度范围内,则取到对应的下标元素作为锚点元素,插到对应的位置
*/
const nextPos = e2 + 1;
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
while (i <= e2) {
patch(null, c2[i], container, parentComponent, anchor);
i += 1;
}
}
}
新老数据对比-删除
例子
尾部删除
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
const prevChildren = [
h('p', { key: 'A' }, 'A'),
h('p', { key: 'B' }, 'B'),
h('p', { key: 'C' }, 'C'),
]
const nextChildren = [h('p', { key: 'A' }, 'A'), h('p', { key: 'B' }, 'B')]
头部删除
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
const prevChildren = [
h("p", { key: "A" }, "A"),
h("p", { key: "B" }, "B"),
h("p", { key: "C" }, "C"),
];
const nextChildren = [h("p", { key: "B" }, "B"), h("p", { key: "C" }, "C")];
实现
function patchKeyedChildren(
c1,
c2,
container,
parentComponent,
parentAnchor
) {
// 1. 左侧对比
// 2. 右侧对比
// other code
/** 3. 新的比旧的长,添加元素
* 1. 改写insert,支持插入到对应位置
* 2. i 为新老数据左侧的差异位置,e1、e2为数据右侧的差异位置
* 3. i > e1,说明新的比旧的长,需要插入数据
* 4. i > e2,说明新的比旧的短,需要删除数据
*/
const l2 = c2.length - 1;
/** 插入数据
* 1. 左侧 i 大于 e1,说明新数据比旧数据多,要把新数据插入
* 2. 添加范围在新数据长度内
*/
if (i > e1 && i <= e2) {
// other code
}else if (i > e2 && i <= e1) {
/** 删除数据
* 1. 左侧 i 大于 e2,则新数据比旧数据少,删除对应数据
* 2. 删除范围在旧数据的长度内
*/
while (i <= e1) {
hostRemove(c1[i].el);
i += 1;
}
}
}