最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 我用index作为key也没啥问题啊

    正文概述 掘金(Fatty)   2021-08-24   432

    前言

    所有熟悉 Vue 技术栈的小伙伴,都知道在列表渲染的场景下,不能使用 indexrandom 作为 key

    也有很多小伙伴在面试的时候会被面试官比较详细的追问,假如使用 index 作为 key 会有什么问题?假如使用 random 作为 key 会有什么问题?假如使用一个唯一不变的 id 作为 key 有什么好处呢?

    这道题目,表面上看起来是考察我们对同级比较过程中 diff 算法的理解,唯一不变的 key 可以帮助我们更快的找到可复用的 VNode,节省性能开销,使用 index 作为 key 有可能造成 VNode 错误的复用,从而产生 bug ,而使用 random 作为 key 会导致VNode 始终无法复用,极大的影响性能。

    这么回答有问题么?没有问题。

    但是假如这道题目满分100,我只能给你99分。

    还有 1分,涉及到 Vue 更新流程中的一点点细节,若不理清,可能在实际的业务场景中给我们造成困扰。

    啥困扰呢?

    举个栗子

    直奔主题,看一段代码,index 作为 key ,假如我们删除某一条,结果会是啥呢?

    <template>
      <div id="app">
        <div v-for="(item, index) in data" :key="index">
          <Child />
          <button @click="handleDelete(index)">删除这一行</button>
        </div>
      </div>
    </template>
    ​
    <script>
    ​
    export default {
      name: "App",
      components: {
        Child: {
          template: '<span>{{name}}{{Math.floor(Math.random() * 1000)}}</span>',
          props: ['name']
        }
      },
      data() {
        return {
          data: [
            { name: "小明" },
            { name: "小红" },
            { name: "小蓝" },
            { name: "小紫" },
          ]
        };
      },
      methods: {
        handleDelete(index) {
          this.data.splice(index, 1);
        },
      }
    };
    </script>
    

    看结果

    我用index作为key也没啥问题啊

    可以观察到,虽然我们删除的不是最后一条,但最终却是最后一条被删除了,看起来很奇怪,但是假如你了解过 Vuediff 流程,这个结果应该是可以符合你的预期的。

    diff

    大段的列源码,会增加我们的理解负担,所以我把 Vue更新流程简化成一张图:

    我用index作为key也没啥问题啊

    通常来讲,我们说 Vuediff 流程,指的就是 patchVnode ,其中 updateChildren 就是我们说的同层比较,其实就是比较新旧两个 Vnode 数组。

    Vue 会声明四个指针变量,分别记录新旧 Vnode 数组的首尾索引,通过首尾索引指针的移动,根据新头旧头、新尾旧尾、旧头新尾、旧尾新头的顺序,依次比较新旧 Vnode ,若不能命中 sameVnode,则将oldVnode.key 维护成一个 map, 继续查询是否包含newVnode.key ,若命中 sameVnode ,则递归执行 patchVnode。若最终无法命中,说明无可复用的 Vnode ,创建新的 dom 节点。

    newVnode 的首尾指针先相遇,说明 newVnode 已经遍历完成,直接移除 oldVnode 多余部分,若 oldVnode 的首尾指针先相遇,说明 oldVnode 已经遍历完成,直接新增 newVnode 的多余部分。

    这种直接的文字描述会显得比较苍白,所以我给大家准备了个动画

    第一步:

    我用index作为key也没啥问题啊

    第二步:

    我用index作为key也没啥问题啊

    第三步:

    我用index作为key也没啥问题啊

    第四步:

    我用index作为key也没啥问题啊

    第五步:

    我用index作为key也没啥问题啊

    第六步:

    我用index作为key也没啥问题啊

    理论上,只要你滑动的足够快,这几张图就可以动起来?

    使用 index 作为 key 会有什么问题

    上面我们讲,判断新旧 Vnode 是否可以复用,取决于 sameNode 方法,这个方法非常简单,就是比对 Vnode 的部分属性,其中 key 是最关键的因素

    function sameVnode (a, b) {
        return (
          a.key === b.key &&
          a.asyncFactory === b.asyncFactory && (
            (
              a.tag === b.tag &&
              a.isComment === b.isComment &&
              isDef(a.data) === isDef(b.data) &&
              sameInputType(a, b)
            ) || (
              isTrue(a.isAsyncPlaceholder) &&
              isUndef(b.asyncFactory.error)
            )
          )
        )
      }
    

    我们再回到上面的栗子,看看是哪里出了问题

    上面代码生成的 VNode 大约是这样的:

    [
      {
        tag: 'div',
        key: 0,
        children: [
          {
            tag: VueComponent, 
            elm: 408, // 这个Vnode对应的真实dom是408
          },
          {
            tag: 'button'
          }
        ]
      },
      {
        tag: 'div',
        key: 1,
        children: [
          {
            tag: VueComponent,
            elm: 227, // 这个Vnode对应的真实dom是227
          },
          {
            tag: 'button'
          }
        ]
      }
      ...
    ]
    

    我们删除第一条数据,新的 VNode 大约是这样的:

    [
      {
        tag: 'div',
        key: 0,
        children: [
          {
            tag: VueComponent,
            elm: 227, // 这个Vnode对应的真实dom是227
          },
          {
            tag: 'button'
          }
        ]
      },
      {
        tag: 'div',
        key: 1,
        children: [
          {
            tag: VueComponent,
            elm: 324, // 这个Vnode对应的真实dom是324
          },
          {
            tag: 'button'
          }
        ]
      }
      ...
    ]
    

    我们人肉逻辑 一下这两个 Vnode 数组,由于 key 都是0,所以比较第一条的时候,就会命中 sameNode ,导致错误复用,然后 updateChildren ,子节点的 Vnode 依然会命中 sameVnode ,同理,第二、三条均会命中 sameVnode ,而直接错误复用其关联的真实 dom 节点,所以我们明明删除的是第一条,UI表现却是最后一条被删除了。

    那么到这里就结束了么?

    当然没有,因为很多小伙伴在刚接触 Vue 的时候,也用过 index 作为 key ,部分牛逼的项目甚至已经上线了,似乎也没人来找麻烦

    why?

    为什么我用 index 作为 key 没出现问题

    如果我把代码改成这样,再删除某一条,会是什么结果呢?

    <template>
      <div id="app">
        <div v-for="(item, index) in data" :key="index">
          <Child :name="`${item.name}`" />
          <button @click="handleDelete(index)">删除这一行</button>
        </div>
      </div>
    </template>
    

    看结果

    我用index作为key也没啥问题啊

    法克,我们明明把 Vue更新流程捋清楚了,用 index 作为 key 会导致 Vnode 错误复用啊,怎么这里表现却正常了呢?

    我们再看一下更新流程简化图:

    我用index作为key也没啥问题啊

    组件类型的 Vnode ,在 patchVnode 的过程中会执行 prePatch 钩子函数,给组件的 propsData 重新赋值,从而触发 setter ,假如 propsData 的值有变化,则会触发 update ,重新渲染组件

    我们可以再人肉逻辑 一下,这次我们删除的是第二条,因为key 一致,新的 Vnode 数组依然会复用旧的 Vnode 数组的前三条,第一条 Vnode 是正确复用,组件的 propsData 未发生变化,不会触发 update ,直接复用其关联的真实 dom 节点,但是第二条 Vnode 是错误复用,但是组件的 propsData 发生变化,由小红变成了小蓝,触发了 update ,组件重新渲染,因此我们看到其实连 random 都发生了变化,第三条同理。

    呼~

    到这里,总算是搞明白了,我可真是个小机灵鬼

    那么到这里就结束了么?

    其实还没有,比如我们再改一下代码

    <template>
      <div id="app">
        <div v-for="(item, index) in data" :key="index">
          <span>{{item.name}}</span>
          <button @click="handleDelete(index)">删除这一行</button>
        </div>
      </div>
    </template>
    

    看结果

    我用index作为key也没啥问题啊

    这次我们没有组件类型 Vnode ,不会执行 prePatch,为啥表现还是正常的呢?

    再观察一下上面的更新流程图,文本类型的 Vnode ,新旧文本不同的时候是会直接覆盖的。

    到这里,我们已经完全明白,列表渲染的场景下,为什么推荐使用唯一不变的 id 作为 key了。抛开代码规范不谈,即使某些场景下,问题并未以 bug 的形式暴露出来,但是不能复用、或者错误复用 Vnode ,都会导致组件重新渲染,这部分的性能包袱还是非常沉重的!

    最后的1分

    纸上得来终觉浅,绝知此事要躬行

    我第一次读完 Vue2 源码的时候,以为自己已经清晰的明白了这部分知识,直到团队里的小伙伴拿着一个纯文本类型的列表来质问我

    不得已仔细 debug 了一遍更新流程,才算解开了心中疑惑,补上了这 1分 的缺口

    引用如下:

    Vue2源码

    Vue技术揭秘


    起源地下载网 » 我用index作为key也没啥问题啊

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元