前言

Vue 中的 key 是用来做什么的?为什么不推荐使用 index 作为 key?常常听说这样的问题,本篇文章带你从原理来一探究竟。

阿里巴巴腾讯前端面试经验,最完整面试真题分享!​www.jianshu.com

示例

以这样一个列表为例:

<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 这个vnodepatch 过程。

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

1. 不是相同节点:

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

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

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

如果新 vnode 是文字 vnode

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

如果新 vnode 不是文字 vnode

如果有新 children 而没有旧 children

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

如果有旧 children 而没有新 children

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

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

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

通过

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

这些变量分别指向旧节点的首尾新节点的首尾

根据这些指针,在一个while 循环中不停的对新旧节点的两端的进行对比,直到没有节点可以对比。

在讲对比过程之前,要讲一个比较重要的函数: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))))
}

它是用来判断节点是否可用的关键函数,可以看到,判断是否是sameVnode ,传递给节点的 key 是关键。

然后我们接着进入diff 过程,每一轮都是同样的对比,其中某一项命中了,就递归的进入 patchVnode 针对单个vnode 进行的过程(如果这个vnode 又有children ,那么还会来到这个diff children 的过程 ):

  1. 旧首节点和新首节点用sameNode 对比。
  2. 旧尾节点和新首节点用sameNode 对比
  3. 旧首节点和新尾节点用sameNode 对比
  4. 旧尾节点和新尾节点用sameNode 对比
  5. 如果以上逻辑都匹配不到,再把所有旧子节点的 key 做一个映射表,然后用新vnodekey去找出在旧节点中可以复用的位置。

然后不停的把匹配到的指针向内部收缩,直到新旧节点有一端的指针相遇(说明这个端的节点都被patch过了)。

在指针相遇以后,还有两种比较特殊的情况:

  1. 有新节点需要加入。如果更新完以后,oldStartIdx > oldEndIdx ,说明旧节点都被 patch完了,但是有可能还有新的节点没有被处理到。接着会去判断是否要新增子节点。
  2. 有旧节点需要删除。如果新节点先patch完了,那么此时会走 newStartIdx > newEndIdx 的逻辑,那么就会去删除多余的旧子节点。

为什么不要以index作为key?

假设我们有这样的一段代码:

    <div id="app"><ul><item:key="index"v-for="(num, index) in nums":num="num":class="`item${num}`"></item></ul><button @click="change">改变</button></div><script src="./vue.js"></script><script>var vm = new Vue({name: "parent",el: "#app",data: {nums: [1, 2, 3]},methods: {change() {this.nums.reverse();}},components: {item: {props: ["num"],template: `<div>{{num}}</div>`,name: "child"}}});</script>

其实是一个很简单的列表组件,渲染出来1 2 3 三个数字。我们先以index 作为key,来跟踪一下它的更新。

我们接下来只关注item列表节点的更新,在首次渲染的时候,我们的虚拟节点列表 oldChildren 粗略表示是这样的:

[{tag: "item",key: 0,props: {num: 1}},{tag: "item",key: 1,props: {num: 2}},{tag: "item",key: 2,props: {num: 3}}
];

在我们点击按钮的时候,会对数组做reverse 的操作。那么我们此时生成的 newChildren列表是这样的:

[{tag: "item",key: 0,props: {
+     num: 3}},{tag: "item",key: 1,props: {
+     num: 2}},{tag: "item",key: 2,props: {
+     num: 1}}
];

发现什么问题没有?key的顺序没变,传入的值完全变了。这会导致一个什么问题?

本来按照最合理的逻辑来说,旧的第一个vnode 是应该直接完全复用新的第三个vnode 的,因为它们本来就应该是同一个vnode,自然所有的属性都是相同的。

但是在进行子节点的diff 过程中,会在旧首节点和新首节点用sameNode对比。 这一步命中逻辑,因为现在新旧两次首部节点key 都是0 了,

然后把旧的节点中的第一个vnode 和 新的节点中的第一个vnode 进行patchVnode 操作。

这会发生什么呢?我可以大致给你列一下:首先,正如我之前的文章props的更新如何触发重渲染?里所说,在进行patchVnode 的时候,会去检查props 有没有变更,如果有的话,会通过props.num = 3 这样的逻辑去更新这个响应式的值,触发dep.notify ,触发子组件视图的重新渲染等一套很重的逻辑。

然后,还会额外的触发以下几个钩子,假设我们的组件上定义了一些dom的属性或者类名、样式、指令,那么都会被全量的更新。

  1. updateAttrs
  2. updateClass
  3. updateDOMListeners
  4. updateDOMProps
  5. updateStyle
  6. updateDirectives

而这些所有重量级的操作(虚拟dom发明的其中一个目的不就是为了减少真实dom的操作么?),都可以通过直接复用第三个vnode 来避免,是因为我们偷懒写了index 作为 key ,而导致所有的优化失效了。

为什么不要用随机数作为key?

<item:key="Math.random()"v-for="(num, index) in nums":num="num":class="`item${num}`"
/>

其实我听过一种说法,既然官方要求一个唯一的key ,是不是可以用Math.random() 作为 key来偷懒?这是一个很鸡贼的想法,看看会发生什么吧。

首先oldVnode 是这样的:

[{tag: "item",key: 0.6330715699108844,props: {num: 1}},{tag: "item",key: 0.25104533240710514,props: {num: 2}},{tag: "item",key: 0.4114769152411637,props: {num: 3}}
];

更新以后是:

[{tag: "item",
+   key: 0.11046018699748683,props: {
+     num: 3}},{tag: "item",
+   key: 0.8549799545696619,props: {
+     num: 2}},{tag: "item",
+   key: 0.18674467938937478,props: {
+     num: 1}}
];

可以看到,key 变成了完全全新的 3 个随机数。

上面说到,diff 子节点的首尾对比如果都没有命中,就会进入key 的详细对比过程,简单来说,就是利用旧节点的 key -> index 的关系建立一个 map 映射表,然后用新节点的key 去匹配,如果没找到的话,就会调用createElm 方法 重新建立 一个新节点。

具体代码在这:

// 建立旧节点的 key -> index 映射表
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);// 去映射表里找可以复用的 index
idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
// 一定是找不到的,因为新节点的 key 是随机生成的。
if (isUndef(idxInOld)) {// 完全通过 vnode 新建一个真实的子节点createElm();
}

也就是说,咱们的这个更新过程可以这样描述:123 -> 前面重新创建三个子组件 -> 321123 -> 删除、销毁后面三个子组件 ->321

发现问题了吧?这是毁灭性的灾难,创建新的组件和销毁组件的成本你们晓得的伐……本来仅仅是对组件移动位置就可以完成的更新,被我们毁成这样了。

作者:ssh 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

总结

经过这样的一段旅行,diff 这个庞大的过程就结束了。

我们收获了什么?

  1. 用组件唯一的id(一般由后端返回)作为它的key ,实在没有的情况下,可以在获取到列表的时候通过某种规则为它们创建一个key ,并保证这个key 在组件整个生命周期中都保持稳定。
  2. 别用index 作为key ,和没写基本上没区别,因为不管你数组的顺序怎么颠倒,index 都是0, 1, 2 这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。
  3. 千万别用随机数作为key ,不然旧节点会被全部删掉,新节点重新创建,你的老板会被你气死。

—————————————————————————————————————

❤️最后

如果本文对你有帮助,就点个赞支持下吧,你的「赞」是我创作的动力。

阿里巴巴腾讯前端面试经验,最完整面试真题分享!​www.jianshu.com

multiusb key找不到驱动程序_尤大-Vue中为什么不建议用index作为key?相关推荐

  1. 在react或vue中,for循环用Index作为key值是好还是坏呢?

    React 与 vue的基本实现原理是通过对比两次虚拟dom的不同 然后直接操作不同的dom 当以数组的下标index作为key值时  其中一个元素发生了变化 就有可能导致所有元素的key值发生改变 ...

  2. 在虚拟DOM和Diff算法中为什么不建议使用index作为key值?

    key是什么 key是虚拟DOM的一个重要标识,在DIff算法中,key对我们的性能也有着至关重要到的作用,有key就可以优化Diff算法,提高渲染性能: key在页面更新的时候做了什么 1.首先,当 ...

  3. thymealf如何实现传单个变量给html_梦回2013,看尤大vue的第一行代码,如何用30行代码实现vue(超简洁,适合初学者)...

    非非非标题党,干货预警!!! 介绍 大家好,我是清池交友 app 开发日记,记录清池交友 app 开发中学习过程和踩坑日记,伪全栈[1] 技术栈:前端 js,vue,uniapp,后端 java 尤大 ...

  4. vue 给checkbox 赋值_浅谈vue中关于checkbox数据绑定v-model指令的个人理解

    vue.js为开发者提供了很多便利的指令,其中v-model用于表单的数据绑定很常见, 下面是最常见的例子: {{msg}} js里data初始化数据 new Vue({ el: "#myA ...

  5. sm总线控制器找不到驱动程序_细说嵌入式系统下的驱动程序设计

    嵌入式系统驱动程序的开发有别于WIndows或Linux.后者除了必须了解新设备的硬件特性,把控制硬件的程序尽快完成之外,还需要设法让驱动程序符合Windows或Linux的规定(大且复杂的架构).但 ...

  6. sm总线控制器找不到驱动程序_技术 | 基于CAN总线的伺服电机通信控制

    1.引言 CAN(ControllerArea Network)总线是一种有效支持分布式控制或实时控制的串行通信局域网络,由于其高性能.高可靠性.实时性好以及独特的设计,已广泛应用于控制系统中的各检测 ...

  7. pci串行端口找不到驱动程序_科普:PCI-E插槽都有哪些样子?

    主板上的扩展插槽曾经是多种多样的,例如曾经非常流行的组合就是PCI插槽搭配AGP插槽,其中AGP插槽主要用在显卡上,而PCI插槽的用途则更广一些,不仅有用在显卡上,还能用于扩展其它设备,如网卡.声卡. ...

  8. adb interface找不到驱动程序_打印机驱动程序无法使用怎么办 打印机驱动安装方法【介绍】...

    打印机 在我们的办公区域实用性是非常强的,不仅可以提高办公的效率,而且还能节省用户时间,是一款非常不错的办公小助手.长时间使用, 打印机驱动程序无法使用 ,怎么回事呢?用户一定要做出详细了解,是否需要 ...

  9. sm总线控制器找不到驱动程序_【KHGEARS钧兴谐波 | 新品】埃斯顿发布总线伺服驱动系统 ProNet Summa...

    高工机器人CEO圈群招募中,欢迎感兴趣的朋友们加微信号:13632944360入群:添加微信时请备注单位-姓名-职务,通过审核后我们将邀请进群. 2019 年 3 月 6 日,以"创新设计 ...

最新文章

  1. 剑指offer java版(三)
  2. 算法-------寻找旋转排序数组中的最小值
  3. 在oracle 11gr2 grid独占模式下,如何使oracle数据库实例伴随OHAS的启动而启动?
  4. 深度学习与自然语言处理之四:卷积神经网络模型(CNN)
  5. 利用openssl进行base64的编码与解码
  6. Unix网络编程卷1源代码使用
  7. Android开发入门基础
  8. Windows Media Player 播放.WMV文件破解许可证
  9. XRD分析软件安装及使用
  10. (转)司徒正美:如何挑选适合的前端框架?
  11. 递归回溯求解数独 C++实现方法
  12. html在线人数统计代码,做一个简单的网站统计和在线人数统计
  13. Android EditText 只能输入数字
  14. [python爬虫小实战2]根据用户输入关键词爬取今日头条图集,并批量下载图片
  15. 奥斯卡大乌龙,然而人工智能也预测《爱乐之城》为最佳影片
  16. 计算机体系结构 第一章 计算机系统结构的基础知识(2)
  17. word文档doc格式转换成docx
  18. Android统一推送联盟成立
  19. 小闫陪你入门 Java (三)
  20. rpm软件安装冲突:conflicts with

热门文章

  1. Hive 通过关闭CBO (Cost based Optimizer) 来优化特定的SQL执行
  2. 设计文档的内容(概要设计,需求分析,详细设计)
  3. 可分离卷积及深度可分离卷积详解
  4. Was8.5静默安装完整步骤
  5. 知识点8--Docker镜像的秘密
  6. vue 中的indexof_vue的这段排序代码看着不是很懂, p = p.name.indexOf(searchperson)!== -1到底是什么意思...
  7. Linux 把内存当做硬盘来使用
  8. php 如何判断手机(m端)和电脑(pc端)
  9. 机器人示教编程与离线编程的选择
  10. React-hooks-ts-antd项目 使用阿里图标库中的图标