【vue源码学习】面试官:为何在Vue列表组件中要写key,有什么做用?

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!html

前言

在写Vue.js应用程序时,为何要在列表组件中写key?有什么做用?为何不推荐使用index做为key?这几个问题在面试时,经常被面试官问起,本篇文章将从原理上深刻剖析解惑。前端

本篇文章是该专栏的第二篇文章。往期文章:vue

1、【Vue源码学习】深刻理解watch的实现原理 —— Watcher的实现 (juejin.cn)node

正文

当数据发生改变时,vue是怎么更新结点的? —— diff算法详解

要知道,当咱们修改了某个数据,若是直接渲染到真实dom上会引发整个dom树的重绘和重排,这样会形成很大的开销。那么有没有可能不更新所有的DOM,只更新发生改变的DOM呢?这里就是diff算法的操刀之处!react

先来看一个经典的示例:面试

<ul> 
    <li>1</li> 
    <li>2</li>
</ul>
复制代码

咱们先根据真实DOM生成一颗virtual DOMvirtual DOM就是将真实的DOM的数据抽取出来,以对象的形式模拟树形结构)算法

上述代码的Virtual DOM Tree 大体以下:后端

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

复制代码

为何咱们要使用虚拟dom,而不是直接使用真实的dom呢?api

当数据发生改变时,直接操做真实的dom会引出重绘,在大量修改的状况下,会拉低性能。而使用虚拟dom会更快,经过批量的内存操做(diff算法),找到发生变化的节点,而后再去操做真实的dom,完成视图更新,而后把结果输出到浏览器,在大量修改的状况下性能更优秀,替换效率高。数组

注:VNodeoldVNode都是对象

小思考(欢迎来评论区讨论交流)

严格来讲:

以上所说的好处都只存在于特殊的场景大量修改数据的状况下

而在正常的操做下,并不会有人闲的没事去大量修改数据,有时候仅仅须要修改一两个简单的dom,却要去使用虚拟dom走一遍diff算法的逻辑,在这种场景下,使用虚拟dom的性能还会比直接使用真实dom更优秀吗?

回到示例中:

此时,咱们模拟数据发生修改,即将两个li中的数据就行交换,交换后的vnode以下:

{
    tag: 'ul',
    children: [{
            tag: 'li',
            children: [{
                vnode: {
                    text: '2'
                }
            }]
        },
        {
            tag: 'li',
            children: [{
                vnode: {
                    text: '1'
                }
            }]
        },
    ]
}
复制代码

数据修改后会发生什么?

简单来说:

virtual DOM某个节点的数据改变后会生成一个新的Vnode,而后VnodeoldVnode做对比,发现有不同的地方就直接修改在真实的DOM上,而后使oldVnode的值为Vnode

下面来深刻了解这个过程👇👇👇👇:

为了使后面逻辑更容易理解,这里先来简单回顾一下vue的响应式原理过程

data.png

如上图所示,整个响应式原理的步骤为:

step1:在vnode阶段,数据发生改变

step2:data响应式数据更新

step3:data的改变会通知到观察者Watche。

step4:触发了渲染Watcher的回调函数vm._update(vm._render())去驱动试图更新

图中的整个过程,若是看过第一篇文章 【Vue源码学习】深刻理解watch的实现原理 —— Watcher的实现 (juejin.cn),应该能对这个过程有较为清晰的认识,

其实,在step4中的vm._render()生成的就是vnode,vm._update 会带着新的 vnode 去走 __patch__ 过程。

下面咱们直接进入 ul 这个 vnode 的 patch 过程(对比新旧节点是否为相同类型的节点):

1.png

如图中所示,新旧节点的对比遵循的规则是:同层级比较

经过对新旧vnode的每一层的对应节点,进行下面的比较判断

  • 节点类型是否相同:
    • 若是类型不相同,直接销毁旧的vnode,渲染新的vnode
    • 若是类型相同,尽量的作节点的复用(在示例中,tag都是ul,进入👇👇👇)
      • 新vnode是否是文字vnode:
        • 若是是,直接调用浏览器的dom api 把节点直接替换掉文字内容便可。
        • 若是不是开始对子节点对比(开始对比示例中的li 👇👇👇)
      • 新旧vnode是否都有children
        • 若是oldVnode没有,newVnode有:在原来的dom上添加新的子节点
        • 若是oldVnode有,而newVnode没有:在原来的dom上删除旧子节点
        • oldVnode和newVnode都有(即都存在li子节点列表,下面进入diff的核心,即新旧节点的diff对比环节👇👇👇)

在讲对比过程以前,先来了解源码中这个过程比较重要的函数:sameVnode:

function sameVnode (a, b) { 
        return ( 
            a.key === b.key &&  
                ( 
                    a.tag === b.tag && 
                    a.isComment === b.isComment &&
                    isDef(a.data) === isDef(b.data) &&
                    sameInputType(a, b) 
                )
        )
  }

复制代码

它是用来判断节点是否可复用的关键函数。

回到diff的核心对比过程:

初始状况下,先用四个指针分别指向新旧节点的首尾,而后根据这些指针,在while循环中不停的对新旧节点的两端进行对比,对比后两端的指针不断向内部收缩,直到没有节点能够对比为止。

每一轮的对比过程:

  1. 旧首节点和新首节点用 sameNode 对比。
  2. 旧尾节点和新尾节点用 sameNode 对比
  3. 旧首节点和新尾节点用 sameNode 对比
  4. 旧尾节点和新首节点用 sameNode 对比
  • 若是单个vnode中又有children子列表,那么就回再去走一遍上面的diff children的过程
  • 若是以上有一项命中,就会递归进入patchVnode
  • 若是以上全部逻辑都匹配不到,就会维护一个map表,再将全部旧子节点的key作key。而后再用新vnode的key区找出在就旧节点中能够复用的位置。

key有什么做用?为何要用它?

对整个diff算法逻辑有了大体的了解后,再来思考这个问题就有了大体的方向了:

看到上面提到过的sameNode函数:

能够发现若是传入的vnode的key不相同的话,就能提早结束掉sameNode函数的逻辑,直接断定为false,这样在必定程度上可以提升新旧vnode对比的效率。另外,若是全部的vnode都有属于本身惟一标识的key值,那么在进行新旧vnode对比时,能够直接维护一个map,将旧节点的key做为键,而后使用新vnode的key去map中查找,从而避免复杂的循环。这也是经典的空间换时间的思想,这样整个diff过程会更快。

为何不要使用index做为key?

既然上面说了,key最主要的做用就是用于做为vnode的惟一标识,那么为何不能使用index做为key呢?下面从两个角度来解答:

这里先模拟一个场景,在一个ul中,经过对数据源中的数组[2,5,3,6]进行v-for循环生成多个li,并用数组的index做为每个li的key值:

  1. key:0 , value:2
  2. key:1 , value:5
  3. key:2 , value:3
  4. key:3 , value:6

当咱们对数组进行反转的修改操做时,即数组变为[6,3,5,2]:

不难想到,此时循环生成的每一个新vnode对应的key,value为:

  1. key:0 , value:6
  2. key:1 , value:3
  3. key:2 , value:5
  4. key:3 , value:2

矛盾显而易见,原本按照最合理的逻辑来说,新的第一个vnode彻底能够直接复用旧的第四个vnode,由于它们应该是同一个vnode,全部的数据也是没有变化的。

然而上述修改会致使的后果是:当子节点在进行diff的过程当中,旧首节点和新首节点用sameNode对比,这一步的逻辑会命中,从而进行patchVnode操做,检查props有没有变动,这里天然是变动了,因此会经过_props.num = 3去更新这个值,并触发视图从新渲染等一系列操做。

这意味着本能够直接复用的vnode,却仍是要去进行一系列的从新更新,会产生巨大的性能消耗。而正是由于使用了index做为key,致使diff的全部优化所有失效。

当咱们对数组进行删除操做时,删除后的数组变为[5,3,6]

则新vnode的key,value对应状况为:

  1. 被删除了
  2. key:0 , value:5
  3. key:1 , value:3
  4. key:2 , value:6

下面来走一边diff的逻辑,这关键函数sameNode看来,它感知不到子组件内部的实现,从上述的sameNode函数中就能看到,sameNode只会只会经过判断key、 tag是否有data的存在(不关心内部具体的值)是不是注释节点是不是相同的input type,来判断是否能够复用这个节点。

因此此时在diff过程当中,旧的1,2,3和新的2,3,4彻底相同,直接复用,最后旧vnode多出了一个4,就会把旧的4删掉。这样的话,原本咱们只是应该把旧的1删掉,结果把旧的4删掉了。

因而可知,使用index做为key,一旦数据发生变化,会给咱们带来毁灭性的错误。

总结

回到问题自己:

  • key有什么做用?

    key能够用来作列表组件的惟一标识符,能够提升diff算法逻辑的效率,主要体如今:一、若是新旧vnodekey值不一样,sameNode函数会直接断定为不可服用。二、有了key,能够直接维护一个map,将旧子节点的key做为键,再用新vnode的key去map中寻找更新的位子,可以加快查找的过程。

  • 为何不能用index做为key? 由于若是循环的数组发生改变,如反转、删除操做,新vnode的key依然是按0,1,2的顺序排列下来的,会致使vue复用错误的节点,走到patchVnode才会发现,又会去一遍数据更新驱动试图更新的操做,diff的全部优化都会失效。

结语

本篇文章主要是介绍了diff过程,以及为何列表组件中要用key? 为何不能用index做为key? 这两个问题的缘由。

本文收录在vue源码学习专栏下,本专栏也是基于笔者本身的学习理念:学习要有输入和输出。而写做的,做为该阶段笔者学习vue源码的输出,但愿一样能给你答疑解惑。让读这篇文章的你有所收获,既是这次分享最大的意义。感谢你的关注!!!

若是有不清楚的地方或者发现文章的错误也欢迎各位在评论区讨论和指出。

参考文献:

但愿与你共同进步!!!

相关文章
相关标签/搜索