专栏首页前端充电站深入浅出 Vue 中的 key 值

深入浅出 Vue 中的 key 值

从前篇文章说起

前几天我写了一篇文章,sortable.js——Vue 数据更新问题 ,当时自己只是从数据的强制刷新角度去分析,而且并没找到真正的“元凶”。

很感谢有人帮我指出,可能是 Vuekey 值,导致数据渲染不正确的。由此,我做了进一步的尝试。

key 的一个错误使用——使用 index 作为 key

不知道你在写 v-for 的时候,会不会直接使用 index 作为它的 key 值,是的,我承认我会,不得不说,这真的不是一个好习惯。

根据上篇文章,我们还是用 sortable.js 作为例子讨论。以下是核心代码,其中 arrData 的值为 [1,2,3,4]

  1. <div id="sort">
  2. <div v-for="(item,index) in arrData" :key="index" >
  3. <div>{{item}}</div>
  4. </div>
  5. </div>
  1. mounted () {
  2. let el = document.getElementById('sort')
  3. var sortable = new Sortable(el, {
  4. onEnd: (e) => {
  5. const tempItem = this.arrData.splice(e.oldIndex, 1)[0]
  6. this.arrData.splice(e.newIndex, 0, tempItem)
  7. }
  8. })
  9. }

当然一开始的时候,数据渲染肯定是没有问题的

好了,我们来看下以下的操作:

可以看到,我将3拖到2上面的时候,下面的数据变成了 1342,但是上面视图的还是1234。然后我第四位置拖到第三位置的时候,下面的数据也是生效的,但是上面的数据似乎全部错乱了。很好,我们重现了案发现场。

接着我改了绑定的 key 值,因为这里的例子比较特殊,我们就认为 item 的值都不相同

  1. <div id="sort">
  2. <div v-for="(item,index) in arrData" :key="item" >
  3. <div>{{item}}</div>
  4. </div>
  5. </div>

再看效果:

是的,这个时候数据就完全跟视图同步了。

为什么?

先看官方文档中 key 的一句介绍

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

之所以会造成上面渲染错误的情况,是因为我们的 key 值不是独特的,比如上面的 key 值,在调整数组顺序后就每一项原来的 key 值都变了,所以导致了渲染错误。

我们先来得出一个结论,index 作为 key 值是有隐患的,除非你能保证 index 始终能够能够作为一个唯一的标识

key 值到底有什么用

vue2.0 之后,我们不写 key 的话,就会报 warning,那也就是说官方是希望我们写 key 值的,那么 key 到底在 vue 中扮演了什么样的角色?

不使用 key 可以提高性能么 答案是,是的!可以!

先看官方解释:

如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

比如现在有一个数组 [1,2,3,4]变成了[2,1,3,4],那么没有 key 的值会采取一种“就地更新策略”,见下图。它不会移动元素节点的位置,而是直接修改元素本身,这样就节省了一部分性能

而对于有 key 值的元素,它的更新方式如下图所示。可以看到,这里它对 DOM 是移除/添加的操作,这是比较耗性能的。

竟然不带 key 性能更优,为何还要带 key 先来看一个例子,核心代码如下,这里模仿一个切换 tab 的功能,也就是切换的tab1 是1,2,3,4。tab2 是 5,6,7,8。其中有设置了一个点击设置第一项字体色为红色的功能。

那么当我们点击tab1将字体色设置成红色之后,再切换到 tab2,我们预期的结果是我们第一项字体的初始颜色而不是红色,但是结果却还是红色。

  1. <div id="sort">
  2. <button @click="trunToTab1">tab1</button>
  3. <button @click="trunToTab2">tab2</button>
  4. <div v-for="(item, index) in arrData">
  5. <div @click="clickItem(index)" class="item">{{item}}</div>
  6. </div>
  7. </div>
  1. trunToTab1 () {
  2. this.arrData = [1,2,3,4]
  3. },
  4. trunToTab2 () {
  5. this.arrData = [5,6,7,8]
  6. },
  7. clickItem () {
  8. document.getElementsByClassName('item')[0].style.color = 'red'
  9. }

这就超出了我们的预期了,也就是官方文档所说的,默认模式指的就是不带 key 的状态,对于依赖于子组件状态或者临时 DOM 状态的,这种模式是不适用的。

这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。

我们来看带上 key 之后的效果

这就是官方文档之所以推荐我们写 key 的原因,根据文档的介绍,如下:

使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。 它也可以用于强制替换元素/组件而不是重复使用它。当你遇到如下场景的时候它可能会很有用:

  • 完整地触发组件的生命周期钩子
  • 触发过渡

那么 Vue 底层 key 值到底是怎么去做到以上的功能?我们就得聊聊 diff 算法以及虚拟 DOM 了。

key 在 diff 算法中的作用

这里我们不谈 diff 算法的具体,只看 key 值在其中的作用。( diff 算法有机会我们再聊)

vue 源码中

  1. if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  2. idxInOld = isDef(newStartVnode.key)
  3. ? oldKeyToIdx[newStartVnode.key]
  4. : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

我们整理一下代码块:

  1. // 如果有带 key
  2. if (isUndef(oldKeyToIdx)) {
  3. // 创建 index 表
  4. oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
  5. }
  6. if (isDef(newStartVnode.key)) {
  7. // 有 key ,直接从上面创建中获取
  8. idxInOld = oldKeyToIdx[newStartVnode.key]
  9. } else {
  10. // 没有key, 调用 findIdxInOld
  11. idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
  12. }

那么最主要还是 createKeyToOldIdxfindIdxInOld 两个函数的比较,那么他们做了什么呢?

  1. function createKeyToOldIdx (children, beginIdx, endIdx) {
  2. let i, key
  3. const map = {}
  4. for (i = beginIdx; i <= endIdx; ++i) {
  5. key = children[i].key
  6. if (isDef(key)) map[key] = i
  7. }
  8. return map
  9. }
  1. function findIdxInOld (node, oldCh, start, end) {
  2. for (let i = start; i < end; i++) {
  3. const c = oldCh[i]
  4. if (isDef(c) && sameVnode(node, c)) return i
  5. }
  6. }

我们可以看到,如果我们有 key 值,我们就可以直接在 createKeyToOldIdx 方法中创建的 map 对象中根据我们的 key 值,直接找到相应的值。没有 key 值,则需要遍历才能拿到。相比于遍历,映射的速度会更快。

key 值是每一个 vnode 的唯一标识,依靠 key,我们可以更快的拿到 oldVnode 中相对应的节点。

参考

第 1 题:写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?

https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/1

解析vue2.0的diff算法

https://github.com/aooy/blog/issues/2

本文分享自微信公众号 - 前端杂货铺(gh_9013e08e595a),作者:GpingFeng

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【前端进阶】深入浅出浏览器事件循环【内附练习题】

    我们看一个很经典的图,这张图基本可以概括了事件循环(该图来自演讲—— 菲利普·罗伯茨:到底什么是Event Loop呢?| 欧洲 JSConf 2014[1])...

    GopalFeng
  • 不靠谱的 console

    作为一名前端, console 估计会时时刻刻陪伴我们,其实各个端都会有输出变量的值的方法,以便调试,这里我指的 console 单纯指的是前端中的 conso...

    GopalFeng
  • 前端踩坑系列《四》

    问题描述 关于性能,确实是一个前端程序员应该特别注意的问题,我这个问题其实算是冰山一角。平时我们在使用动画的时候,可能经常使用 transition 做动画,但...

    GopalFeng
  • redis

    1)Redis:REmote DIctionary Server(远程字典服务器)

    用户2337871
  • 网银安全控件问题

    网银的密码输入控件是通过直接读键盘设备IO获取的输入。需要使用驱动级的键盘模拟输入技术才能输进去。

    周小董
  • Redis学习笔记 -- 2

    Redis 字符串数据类型的相关命令用于管理 redis 字符串值,基本语法如下:

    用户1637228
  • Redis常用命令整理

    端碗吹水
  • Redis系列(十一)redis命令全集

    总的来说,Redis 是一个基于内存的高性能的键值型数据库,也就是常说的 NoSQL, 可以用来作为数据库或者缓存。并且支持多种数据结构,包括字符串,散列,列表...

    呼延十
  • redis

    (redis.memcache(内存数据库,高速缓存),mongodb(文档数据库))

    Dean0731
  • Redis工作中常用命令,看这一篇就够了

    jvm我们讲了两篇文章,为了不让大家学习疲劳,我们几个技术穿插着来讲,我们今天讲讲Redis的各种命令,这篇会把大家日常需要用到的命令全都列出来,满足你们的日常...

    公众号 IT老哥

扫码关注云+社区

领取腾讯云代金券