更新element的children-双端对比 diff 算法
- 双端对比
- 通过双端对比,把左边,右边缩小范围,将中间乱序的位置找出来
- 找出来以后,
- 老的有,新的没有,删除老的
- 老的没有,新的有,创建新的
- 老的,新的都有,但顺序发生改变,按照新的调整顺序
- 为什么进行双端对比?
- 操作 dom 性能消耗大,比较昂贵,很多时候,新旧节点开头和结尾都是不动的,仅仅是中间位置进行了更改,双端对比减少了dom操作
- 为了简化算法,遍历所有节点是 O(n) 的时间复杂度,双端对比,减少了 n
- 有哪些情况呢?
- 仅双端对比部分
- 左侧对比 左侧有相同的部分,比如: a b c < === > a b d e
- 右侧对比 右侧有相同的部分,比如: a b c < === > d e b c
- 新的比老的多
- a b < === > a b c
- c a b < === > a b
- 老的比新的多
- a b c < === > a b
- a b c < === > b c
- 中间对比
- 老的有,新的没有,删除老的
- 老的没有,新的有,创建新的
- 老的,新的都有,但顺序发生改变,按照新的调整顺序
- 初始化文件
js
复制代码
// main.js index.html 省略
// App.js
import {
h,
ref
} from "../../lib/guide-mini-vue.esm.js";
import {
ArrayToArray
} from "./ArrayToArray.js";
export const App = {
name: "App",
render() {
return h("div", {},
[
h('p', {}, '主页'),
h(ArrayToArray)
]
)
},
setup() {
return {}
},
}
// ArrayToArray.js
import {
h,
ref
} from '../../lib/guide-mini-vue.esm.js'
// 1. 左侧的对比
// (a b ) c
// (a b ) d e
const preChildren = [
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'),
]
export const ArrayToArray = {
name: "ArrayToArray",
setup() {
const isChange = ref(false);
window.isChange = isChange;
return {
isChange,
}
},
render() {
const self = this
return h('div', {}, self.isChange === false ? preChildren : nextChildren)
}
}
- 双端对比实现
- 对比左侧
js
复制代码
// 这里对比的是 abc ---- abde
function patchChildren(n1, n2, container, parentComponent) {
const { shapeFlag: prevShapeFlag, children: c1 } = n1
const { shapeFlag: nextShapeFlag, children: c2 } = n2
if (nextShapeFlag & shapeFlags.TEXT_CHILDREN) {
// 这里的 TEXT_CHILDREN 这种情况,没有过多说明,即使没有说明,默认也包含这种情况,这块是优化完毕的代码
if (prevShapeFlag & shapeFlags.ARRAY_CHILDREN) {
unmountChildren(n1.children)
}
if (c1 !== c2) {
hostSetElementText(container, c2)
}
} else {
// 这里是 转成了数组类型的结构
if (prevShapeFlag & shapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, "")
mountChildren(c2, container, parentComponent)
} else { // ✅ 其余位置上一章内容
// array to array
patchKeyedChildren(c1, c2, container, parentComponent)
}
}
}
function isSameVNodeType(n1, n2) { // ✅ 这里判断是否是同一个节点
return n1.type === n2.type && n1.key === n2.key
}
function patchKeyedChildren(c1, c2, container,parentComponent) { // ✅ 通过这几个下标来实现左侧对比范围划分
let i = 0 // 左侧的初始下标
let e1 = c1.length - 1 // 右侧的初始下标 (新节点)
let e2 = c2.length - 1 // 右侧的初始下标 (旧节点)
while (i <= e1 && i <= e2) { // 左侧下标始终小于右侧的下标
if (isSameVNodeType(c1[i], c2[i])) {
patch(c1, c2, container, parentComponent)
} else {
break;
}
i++
}
console.log(i);
}
- 对比右侧
js
复制代码
// 右侧的对比的数据
const preChildren = [
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'),
]
js
复制代码
// 初始化时, 老节点第一个是 A,新节点第一个是 D,第一个值不一样,所以 i 直接为0,不再改变
while(i <= e1 && i <= e2) {
let n1 = c1[e1]
let n2 = c2[e2]
if(isSameVNodeType(n1, n2)){
patch(n1, n2, container, parentComponent)
} else {
break
}
e1--
e2--
}
- 新的比老的多
js
复制代码
// a b < === > a b c
const preChildren = [
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'),
]
// c a b < === > a b
const preChildren = [
h('p', { 'key': 'C' }, 'C'),
h('p', { 'key': 'A' }, 'A'),S
h('p', { 'key': 'B' }, 'B'),
]
const nextChildren = [
h('p', { 'key': 'A' }, 'A'),
h('p', { 'key': 'B' }, 'B'),
h('p', { 'key': 'C' }, 'C'),
]
js
复制代码
if(i > e1) {
if(i <= e2) {
const nextPos = i + 1
const anchor = nextPos < c2.length ? c2[nextPos].el : null
while(i <= e2) { // 这里是说新节点比老节点多很多个,所以要逐个进行添加
patch(null, c2[i], container, parentComponent, anchor)
i++
}
}
}
// run time-dom/index.ts
export function insert(child, parent, ancher) { // ✅ 这里新增 ancher,所以涉及到 insert 方法的,如 patch 方法都增加了这个参数,很多位置,不做列举
// parent.append(el)
parent.insertBefore(child, ancher || null )
}
- 老的比新的多
js
复制代码
// a b c < === > a b
const preChildren = [
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
const preChildren = [
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'),
]
js
复制代码
if (i > e1) {
// 省略 新的比老的多
} else if (i > e2) { // 老的比新的多
while (i <= e1) {
hostPatchRemove(c1[i].el)
i++
}
}