要想知道 key 在patch过程的作用,必须知道VNode的生成以及更新真实DOM过程中key的使用
VNode.png
组件生成的VNode
有如下vue组件
<!-- template -->
<ul>
<!-- 这里使用key -->
<li v-for="item in list" :key="item.id">{{ item.value }}</li>
</ul>
// data数据
list = [
{ id: "number", value: "Number" },
{ id: "string", value: "String" },
{ id: "boolean", value: "Boolean" }
]
上面组件生成VNode,key即:key="item.id"中的值,children与数据list是一一对应的。
vnode = {
tag: 'ul',
key: null,
children: [{
tag: 'li',
// 这个key即模板列表中li元素的key
key: 'number',
children: { tag: null, children: 'Number', ... },
... // 其他属性
},{
tag: 'li',
key: 'string',
children: { tag: null, children: 'String', ... }
...
},{
tag: 'li',
key: 'boolean',
children: { tag: null, children: 'Boolean', ... },
...
}],
...
}
这个key将在新旧DOM更新时起到对比作用
如果不使用key的情况
<ul>
<li v-for="item in list">{{ item.value }}</li>
</ul>
生成的VNode如下
// 不使用key
vnode = {
tag: 'ul',
key: null,
children: [{
tag: 'li',
key: '0', // 实际为'|0'竖线(|)+索引拼接而成,这里直接索引,不影响理解
children: { tag: null, children: 'Number', ... },
...
},{
tag: 'li',
key: '1',
children: { tag: null, children: 'String', ... }
...
},{
tag: 'li',
key: '2',
children: { tag: null, children: 'Boolean', ... },
...
}],
...
}
注意:不使用key时,h函数(生成VNode函数)内部会为列表(多子节点)生成默认的key,默认的key由列表数组下标生成。
// 生成key
function normalizeVNodes(children) {
const newChildren = []
// 遍历 children
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (child.key == null) {
// 如果原来的 VNode 没有key,则使用竖线(|)与该VNode在数组中的索引拼接而成的字符串作为key
child.key = '|' + i
}
newChildren.push(child)
}
// 返回新的children
return newChildren
}
通过上面的
VNode可知,模板列表中使用了key将作为该的子节点对应VNode中的key值,如果模板列表中没有使用key,一个简单的key将会被生成。
核心diff - key的使用
我们都知道,当组件状态被修改时,页面上真实DOM需要得到更新,Vue 会尽可能的尝试修复/再利用相同类型元素。
如上面的列表数据发生改变时:
// 顺序改变
list = [
{ id: "string", value: "String" },
{ id: "boolean", value: "Boolean" },
{ id: "number", value: "Number" }, // 下标由 0 --> 2
]
当组件状态改变时,组件会生成新的VNode:
// 新VNode
// 注意新的VNode顺序使用新的数据生成
newVnode = {
tag: 'ul',
key: null,
children: [{
tag: 'li',
key: 'string',
children: { tag: null, children: 'String', ... }
...
},{
tag: 'li',
key: 'boolean',
children: { tag: null, children: 'Boolean', ... },
...
},{ // 下标由 0 --> 2
tag: 'li',
key: 'number',
children: { tag: null, children: 'Number', ... },
...
}],
...
}
组件状态改变时会调用组件的render函数生成新的VNode,通过对比新旧VNode高效更新DOM。现在新旧VNode都已存在,是时候patch真实DOM了,通过对比新旧VNode,查找能复用的节点,添加旧VNode不存在的新节点,或删除新VNode中不需要的节点。
核心Diff针对新旧VNode子节点都是多节点的情况,也就是真实DOM更新前后列表都为多子元素情况,其他新旧VNode单个子节点或无子节点的情况不在这里讨论。
/**
* 新旧VNodeChildren都为多个子节点 patch
* @param { VNode | VNode[] | string } prevChildren 旧子节点VNode列表
* @param { VNode | VNode[] | string } nextChildren 新子节点VNode列表
* @param { Element } container 容器
*/
function patchChildren(prevChildren, nextChildren, container) {
// 遍历新的VNode列表
for (let i = 0; i < nextChildren.length; i++) {
const nextVNode = nextChildren[i];
let find = false; // 是否找到对应旧节点
// 遍历旧的VNode列表
for (let j = 0; j < prevChildren.length; j++) {
const prevVNode = prevChildren[j];
// 对比新旧key,如果找到相同的key则可以直接更新复用DOM
if (nextVNode.key === prevVNode.key) {
find = true;
... // 更新真实DOM 并移动位置
break;
}
}
// 未找到旧节点,说明该节点为新增节点
if (!find) {
... // 挂载新节点
}
// 遍历旧节点,查找是否新VNode列表中是否存在,如果不存在,说明这个节点需要被移除
...
}
}
分析总结
核心是查找新旧VNode的key,以便对旧的DOM更新复用.
-
1、遍历新的
VNode如果在旧VNode中存在相同的key,即认为该节点应该被更新复用如果模板找中不使用
key,此时VNode将会生成默认的key,那么新旧VNode遍历查找key时,会发现新VNode子节点与旧VNode的子节点位置按数组下标一一对应。如上面组件list数据仅顺序有变更,最有效patch应该是移动节点即可,而默认的key将会更新所有的子元素。 -
2、未找到旧节点,说明该节点为新增节点, 直接挂载该新节点即可
-
3、遍历旧节点,查找是否新VNode列表中是否存在,如果不存在,说明这个节点需要被移除
模板不使用
key时,可能会造成本应该移除的节点被复用,应该被复用的节点被删除。如上面组件list删除第二项(id="string")时,新的VNode中key值集合为[0, 1],而旧的VNode中key值集合为[0, 1, 2],对比发现旧VNode中key = 1的虚拟节点(id = "string")被复用,而应该被复用的项key = 2(id = "boolean")反而被移除。