详解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 相关文章推荐
Jquery数独游戏解析(一)-页面布局
Nov 05 Javascript
jquery 3D 标签云示例代码
Jun 12 Javascript
Javascript进制转换实例分析
May 14 Javascript
基于jquery实现的树形菜单效果代码
Sep 06 Javascript
基于jquery实现的银行卡号每隔4位自动插入空格的实现代码
Nov 22 Javascript
详解堆的javascript实现方法
Nov 29 Javascript
javascript深拷贝的原理与实现方法分析
Apr 10 Javascript
Vue的Class与Style绑定的方法
Sep 01 Javascript
jQuery模拟12306城市选择框功能简单实现方法示例
Aug 13 jQuery
在vue中使用express-mock搭建mock服务的方法
Nov 07 Javascript
node.js监听文件变化的实现方法
Apr 17 Javascript
JS实现烟花爆炸效果
Mar 10 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应用技巧
2008/03/27 PHP
php生成随机数或者字符串的代码
2008/09/05 PHP
PHP与SQL注入攻击防范小技巧
2011/09/16 PHP
php addslashes 利用递归实现使用反斜线引用字符串
2013/08/05 PHP
php stream_get_meta_data返回值
2013/09/29 PHP
PHP中使用循环实现的金字塔图形
2014/11/08 PHP
PHP用PDO如何封装简单易用的DB类详解
2017/07/30 PHP
PHP实现的无限分类类库定义与用法示例【基于thinkPHP】
2018/08/06 PHP
TP - 比RBAC更好的权限认证方式(Auth类认证)
2021/03/09 PHP
用脚本调用样式的几种方法
2006/12/09 Javascript
js计算字符串长度包含的中文是utf8格式
2013/10/15 Javascript
javascript常用函数归纳整理
2014/10/31 Javascript
TypeScript具有的几个不同特质
2015/04/07 Javascript
学习JavaScript设计模式(代理模式)
2015/12/03 Javascript
JavaScript的Ext JS框架中的GridPanel组件使用指南
2016/05/21 Javascript
Bootstrap使用基础教程详解
2016/09/05 Javascript
VueJs与ReactJS和AngularJS的异同点
2016/12/12 Javascript
自带气泡提示的vue校验插件(vue-verify-pop)
2017/04/07 Javascript
基于Vue的文字跑马灯组件(npm 组件包)
2017/05/24 Javascript
微信小程序实现分页加载效果
2020/11/19 Javascript
[02:16]完美世界DOTA2联赛PWL S3 集锦第三期
2020/12/21 DOTA
Python检测网站链接是否已存在
2016/04/07 Python
Python编程pygame模块实现移动的小车示例代码
2018/01/03 Python
python集合比较(交集,并集,差集)方法详解
2018/09/13 Python
python 搭建简单的http server,可直接post文件的实例
2019/01/03 Python
python 将字符串完成特定的向右移动方法
2019/06/11 Python
python实现批量修改服务器密码的方法
2019/08/13 Python
python 多进程并行编程 ProcessPoolExecutor的实现
2019/10/11 Python
Pytorch 实现focal_loss 多类别和二分类示例
2020/01/14 Python
pytorch 修改预训练model实例
2020/01/18 Python
Python文件名匹配与文件复制的实现
2020/12/11 Python
英国高级健康和美容产品零售商:Life and Looks
2019/08/01 全球购物
初中家长寄语
2014/04/02 职场文书
承诺书格式
2014/06/03 职场文书
靠谱准确的求职信
2019/04/02 职场文书
Python机器学习之决策树和随机森林
2021/07/15 Javascript