详解为什么Vue中不要用index作为key(diff算法)

2020-06-16 06:46:48易采站长站整理

前言

Vue 中的 key 是用来做什么的?为什么不推荐使用 index 作为 key?常常听说这样的问题,本篇文章带你从原理来一探究竟。
另外本文的结论对于性能的毁灭是针对列表子元素顺序会交换、或者子元素被删除的特殊情况,提前说明清楚,喷子绕道。

本篇已经收录在 Github 仓库,欢迎 Star:
https://github.com/sl1673495/blogs/issues/39

示例

以这样一个列表为例:


<ul>
<li>1</li>
<li>2</li>
</ul>

那么它的 vnode 也就是虚拟 dom 节点大概是这样的。


{
tag: 'ul',
children: [
{ tag: 'li', children: [ { vnode: { text: '1' }}] },
{ tag: 'li', children: [ { vnode: { text: '2' }}] },
]}

假设更新以后,我们把子节点的顺序调换了一下:


{
tag: 'ul',
children: [
+ { tag: 'li', children: [ { vnode: { text: '2' }}] },
+ { tag: 'li', children: [ { vnode: { text: '1' }}] },
]}

很显然,这里的 children 部分是我们本文 diff 算法要讲的重点(敲黑板)。

首先响应式数据更新后,触发了 渲染 Watcher  的回调函数 vm._update(vm._render())去驱动视图更新,vm._render() 其实生成的就是 vnode,而 vm._update 就会带着新的 vnode 去走触发 __patch__ 过程。

我们直接进入 ul 这个 vnode 的 patch 过程。

对比新旧节点是否是相同类型的节点:

1. 不是相同节点:
isSameNode为false的话,直接销毁旧的 vnode,渲染新的 vnode。这也解释了为什么 diff 是同层对比。

2. 是相同节点,要尽可能的做节点的复用(都是 ul,进入👈)。

会调用src/core/vdom/patch.js下的patchVNode方法。

如果新 vnode 是文字 vnode

就直接调用浏览器的 dom api 把节点的直接替换掉文字内容就好。

如果新 vnode 不是文字 vnode
如果有新 children 而没有旧 children

说明是新增 children,直接 addVnodes 添加新子节点。

如果有旧 children 而没有新 children

说明是删除 children,直接 removeVnodes 删除旧子节点

如果新旧 children 都存在(都存在 li 子节点列表,进入👈)

那么就是我们 diff算法 想要考察的最核心的点了,也就是新旧节点的 diff 过程。

通过


// 旧首节点
let oldStartIdx = 0
// 新首节点
let newStartIdx = 0
// 旧尾节点
let oldEndIdx = oldCh.length - 1
// 新尾节点
let newEndIdx = newCh.length - 1