详解key在Vue列表渲染时究竟起到了什么作用


Posted in Javascript onApril 20, 2019

Vue2+采用diff算法来进行新旧vnode的对比从而更新DOM节点。而通常在我们使用v-for这个指令的时候,Vue会要求你给循环列表的每一项添加唯一的key,那么这个key在渲染列表时究竟起到了什么作用呢?

在解释这一点之前,你最好已经了解Vue的diff算法的具体原理是什么。

Vue2更新真实DOM的操作主要是两种:创建新DOM节点并移除旧DOM节点和更新已存在的DOM节点,这两种方式里创建新DOM节点的开销肯定是远大于更新或移动已有的DOM节点,所以在diff中逻辑都是为了减少新的创建而更多的去复用已有DOM节点来完成DOM的更新。

在新旧vnode的diff过程中,key是判断两个节点是否为同一节点的首要条件:

// 参见Vue2源码 core/vdom/patch.js

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)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

值得注意的是,如果新旧vnode的key值都未定义的话那么两个key都为undefined,a.key === b.key 是成立的

接下来是在updateChildren方法中,这个方法会对新旧vnode进行diff,然后将比对出的结果用来更新真实的DOM

// 参见Vue2源码 core/vdom/patch.js

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  ...
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      ...
    } else if (isUndef(oldEndVnode)) {
      ...
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      ...
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      ...
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      ...
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      ...
    } else {
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) { // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // same key but different element. treat as new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  ...
}

设置key的可以在diff中更快速的找到对应节点,提高diff速度

在updateChildren方法的while循环中,如果头尾交叉对比没有结果,即oldStartVnode存在且oldEndVnode存在且新旧children首尾四个vnode互不相同的条件下,会根据newStartVnode的key去对比oldCh数组中的key,从而找到相应oldVnode

首先通过createKeyToOldIdx方法创建一个关于oldCh的map

if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

function createKeyToOldIdx (children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

这个map中将所有定义了key的oldVnode在数组中的index值作为键值,它的key作为键名存储起来,然后赋给oldKeyToIdx

idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

function findIdxInOld (node, oldCh, start, end) {
  for (let i = start; i < end; i++) {
    const c = oldCh[i]
    if (isDef(c) && sameVnode(node, c)) return i
  }
}

如果newStartVnode的key存在的话,就去oldKeyToIdx中寻找相同key所对应的index值,这样就能拿到跟newStartVnode的key相同的oldVnode在oldCh数组中的index,即得到了与newStartVnode对应的oldVnode。如果找不到的话,那么idxInOld就为undefined。

而如果newStartVnode并没有设置key,则通过findIdxInOld方法遍历oldCh来获取与newStartVnode互为sameVnode的oldVnode,返回这个oldVnode在oldCh数组的index。(前面介绍过,Vue在更新真实DOM时倾向于真实DOM节点的复用,所以在这里还是会选择去找对应的oldVnode,来更新已有的DOM节点)

这时候设置key的好处就显而易见了,有key存在时我们可以通过map映射快速定位到对应的oldVnode然后进行patch,没有key值时我们需要遍历这个oldCh数组然后去一一进行比较,相比之下肯定是key存在时diff更高效。

接下来就是更新DOM的过程,如果oldCh[idxInOld]存在且与newStartVnode互为sameVnode存在则先更新再移动,否则创建新的element

if (isUndef(idxInOld)) { // New element
  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
  vnodeToMove = oldCh[idxInOld]
  if (sameVnode(vnodeToMove, newStartVnode)) {
    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
    oldCh[idxInOld] = undefined
    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
  } else {
    // same key but different element. treat as new element
    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
  }
}

那么设置key值就一定能提高diff效率吗?

答案是否定的

`<div v-for="i in arr">{{ i }}</div>`

// 如果我们的数组是这样的
[1, 2, 3, 4, 5]

// 它的渲染结果是这样的
`<div>1</div>` // key: undefined
`<div>2</div>` // key: undefined
`<div>3</div>` // key: undefined
`<div>4</div>` // key: undefined
`<div>5</div>` // key: undefined

// 将它打乱
[4, 1, 3, 5, 2]

// 渲染结果是这样的 期间只发生了DOM节点的文本内容的更新
`<div>4</div>` // key: undefined
`<div>1</div>` // key: undefined
`<div>3</div>` // key: undefined
`<div>5</div>` // key: undefined
`<div>2</div>` // key: undefined


// 如果我们给这个数组每一项都设置了唯一的key
[{id: 'A', value: 1}, {id: 'B', value: 2}, {id: 'C', value: 3}, {id: 'D', value: 4}, {id: 'E', value: 5}]

// 它的渲染结果应该是这样的
`<div>1</div>` // key: A
`<div>2</div>` // key: B
`<div>3</div>` // key: C
`<div>4</div>` // key: D
`<div>5</div>` // key: E

// 将它打乱
[{id: 'D', value: 4}, {id: 'A', value: 1}, {id: 'C', value: 3}, {id: 'E', value: 5}, {id: 'B', value: 2}]

// 渲染结果是这样的 期间只发生了DOM节点的移动
`<div>4</div>` // key: D
`<div>1</div>` // key: A
`<div>3</div>` // key: C
`<div>5</div>` // key: E
`<div>2</div>` // key: B

我们给数组设置了key之后数组的diff效率真的变高了吗?

并没有,因为在简单模板的数组渲染中,新旧节点的key都为undefined,根据sameVnode的判断条件,这些新旧节点的key、tag等属性全部相同,所以在sameVnode(oldStartVnode, newStartVnode)这一步的时候就已经判定为对应的节点(不再执行头尾交叉对比),然后直接进行patchVnode,根本没有走后面的那些else。每一次循环新旧节点都是相对应的,只需要更新其内的文本内容就可以完成DOM更新,这种原地复用的效率无疑是最高的。

而当我们设置了key之后,则会根据头尾交叉对比结果去执行下面的if else,进行判断之后还需要执行insertBefore等方法移动真实DOM的节点的位置或者进行DOM节点的添加和删除,这样的查找复用开销肯定要比不带key直接原地复用的开销要高。

Vue文档中对此也进行了说明:

当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。

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

建议尽可能在使用 v-for 时提供 key,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。

所以,简单列表的渲染可以不使用key或者用数组的index作为key(效果等同于不带key),这种模式下性能最高,但是并不能准确的更新列表项的状态。一旦你需要保存列表项的状态,那么就需要用使用唯一的key用来准确的定位每一个列表项以及复用其自身的状态,而大部分情况下列表组件都有自己的状态。

总结

key在列表渲染中的作用是:在复杂的列表渲染中快速准确的找到与newVnode相对应的oldVnode,提升diff效率

以上所述是小编给大家介绍的key在Vue列表渲染时究竟起到了什么作用详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
javascript的原生方法获取数组中的最大(最小)值
Dec 19 Javascript
javascript parseInt() 函数的进制转换注意细节
Jan 08 Javascript
jQuery拖动div、移动div、弹出层实现原理及示例
Apr 08 Javascript
原生javascript实现简单的datagrid数据表格
Jan 02 Javascript
JavaScript通过字典进行字符串翻译转换的方法
Mar 19 Javascript
AngularJS向后端ASP.NET API控制器上传文件
Feb 03 Javascript
JS组件Bootstrap dropdown组件扩展hover事件
Apr 17 Javascript
ES6中的箭头函数实例详解
Apr 06 Javascript
正则表达式基本语法及表单验证操作详解【基于JS】
Apr 07 Javascript
深入浅析ES6 Class 中的 super 关键字
Oct 20 Javascript
BootStrap TreeView使用实例详解
Nov 01 Javascript
JavaScript实现异步图像上传功能
Jul 12 Javascript
函数式编程入门实践(一)
Apr 20 #Javascript
vue路由对不同界面进行传参及跳转的总结
Apr 20 #Javascript
详解Vue+Element的动态表单,动态表格(后端发送配置,前端动态生成)
Apr 20 #Javascript
详解vue使用$http服务端收不到参数
Apr 19 #Javascript
ajaxfileupload.js实现上传文件功能
Apr 19 #Javascript
AjaxFileUpload.js实现异步上传文件功能
Apr 19 #Javascript
读懂CommonJS的模块加载
Apr 19 #Javascript
You might like
php session和cookie使用说明
2010/04/07 PHP
浅析PHP程序设计中的MVC编程思想
2014/07/28 PHP
Codeigniter检测表单post数据的方法
2015/03/21 PHP
深入理解PHP JSON数组与对象
2016/07/19 PHP
PHP基于正则批量替换Img中src内容实现获取缩略图的功能示例
2017/06/07 PHP
PHP利用Cookie设置用户30分钟未操作自动退出功能
2017/07/03 PHP
javascript类继承机制的原理分析
2009/09/12 Javascript
扩展jQuery 键盘事件的几个基本方法
2009/10/30 Javascript
jquery对ajax的支持介绍
2013/12/10 Javascript
javascript基本类型详解
2014/11/28 Javascript
jQuery前端框架easyui使用Dialog时bug处理
2014/12/05 Javascript
JavaScript实现向setTimeout执行代码传递参数的方法
2015/04/16 Javascript
基于jquery实现瀑布流布局
2020/06/28 Javascript
用canvas 实现个图片三角化(LOW POLY)效果
2016/02/18 Javascript
实例解析jQuery中proxy()函数的用法
2016/05/24 Javascript
AngularJS基础 ng-hide 指令用法及示例代码
2016/08/01 Javascript
bootstrap制作jsp页面(根据值让table显示选中)
2017/01/05 Javascript
vue省市区三联动下拉选择组件的实现
2017/04/28 Javascript
详解webpack与SPA实践之开发环境搭建
2017/12/18 Javascript
在AngularJs中设置请求头信息(headers)的方法及不同方法的比较
2018/09/04 Javascript
15分钟上手vue3.0(小结)
2020/05/20 Javascript
JS实现简易贪吃蛇游戏
2020/08/24 Javascript
vue+swiper实现左右滑动的测试题功能
2020/10/30 Javascript
python安装oracle扩展及数据库连接方法
2017/02/21 Python
Flask框架Jinjia模板常用语法总结
2018/07/19 Python
Django Rest framework之认证的实现代码
2018/12/17 Python
python控制nao机器人身体动作实例详解
2019/04/29 Python
python操作openpyxl导出Excel 设置单元格格式及合并处理代码实例
2019/08/27 Python
Python sklearn库实现PCA教程(以鸢尾花分类为例)
2020/02/24 Python
CSS3模块的目前的状况分析
2010/02/24 HTML / CSS
跑步爱好者一站式服务网站:Jack Rabbit
2016/09/01 全球购物
adidas官方旗舰店:德国运动用品制造商
2017/11/25 全球购物
和平主题的演讲稿
2014/01/12 职场文书
大学新生军训自我鉴定
2014/03/18 职场文书
诉讼财产保全担保书
2014/05/20 职场文书
公司前台接待岗位职责
2015/04/03 职场文书