diff 算法是在数据变动时,逐层比较新旧虚拟节点,再决定添加或删除或重复使用真实节点,添加 key 可以提高节点的利用率,但同时会增加计算量。
vue 实例在创建的时候会为 data 添加 get、set 方法,data 改变时会触发 set 方法,set 方法通知所有的订阅者,订阅者调用 patch 给真实节点打补丁。
数据更新时:
// Vue.prototype._update
获取真实节点;
获取旧虚拟节点;
if (没有旧虚拟节点) {
patch(真实节点, 新虚拟节点);
} else {
// 对新旧虚拟节点进行 diff 比较
patch(旧虚拟节点, 新虚拟节点);
}
看看 patch 函数是什么样的:
// patch
if (旧虚拟节点不存在) {
创建新真实节点;
} else {
比较新旧虚拟节点是否相同;
if (相同) {
// 递归比较
patchVnode();
} else {
用新虚拟节点创建新真实节点;
用旧虚拟节点删除旧真实节点;
}
}
接下来是 patchVnode 函数:
// patchVnode
if (新旧虚拟节点相同) {
直接返回;
} else {
保存旧虚拟节点引用的旧真实节点;
if (新虚拟节点不是文本节点) {
if (新旧虚拟节点都有子节点且子节点不相等) {
递归执行 updateChildren 函数比较子节点;
} else if (只有新虚拟节点有子节点) {
if (旧虚拟节点为文本) {
旧真实节点置空;
}
插入新的真实节点;
} else if (只有旧虚拟节点有子节点) {
移除旧真实节点的所有子节点;
} else if (旧虚拟节点存在且是文本节点) {
旧真实节点置空;
}
} else if (新虚拟节点的文本内容和旧虚拟节点不一样) {
把旧虚拟节点设置为新虚拟节点文本内容
}
}
diff 算法的核心—— updateChildren 函数:
// updateChildren
旧头: 旧虚拟节点的第一个子节点;
旧尾: 旧虚拟节点的最后一个子节点;
新头: 新虚拟节点的第一个子节点;
新尾: 新虚拟节点的最后一个子节点;
// 遍历新旧虚拟节点的子节点来比较和更新
while (旧头索引 <= 旧尾索引 && 新头索引 <= 新尾索引) {
if (旧头为空) {
旧虚拟节点移除第一个子节点;
} else if (旧尾为空) {
旧虚拟节点移除最后一个子节点;
} else if (旧头新头相同) {
执行 patchVnode 递归下去;
新旧虚拟节点都移除第一个子节点;
} else if (旧尾新尾相同) {
执行 patchVnode 递归下去;
新旧虚拟节点都移除最后一个子节点;
} else if (旧头新尾相同) {
执行 patchVnode 递归下去;
旧头索引向右移动,新尾索引向左移动;
} else if (旧尾新头相同) {
执行 patchVnode 递归下去;
旧尾索引向左移动,新头索引向右移动;
} else {
// 若 4 个虚拟节点都不相同,就利用 key 去对比
用旧的虚拟节点的 key 做 map 映射;
// 循环过程中新虚拟节点的每个子节点都会成为新头
新头的 key 和 map 做对比;
if (key in map) {
if (是相同节点) {
继续进行 patchVnode 操作,再执行 insertBefore 插入相应位置的真实节点;
} else {
创建新的真实节点并插入;
}
} else {
创建新的真实节点并插入;
}
}
}
// 循环结束后,可能存在未处理的虚拟子节点
if (旧头索引 > 旧尾索引) {
表示新虚拟节点的子节点未处理完,遍历剩余的新虚拟节点的子节点,插入到新真实节点中;
} else if (新头索引 > 新尾索引) {
表示旧虚拟节点的子节点未处理完,删除旧真实节点中对应的子节点
}
参考: