欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-12-04 15:27:16
vue2+采用diff算法来进行新旧vnode的对比从而更新dom节点。而通常在我们使用v-for这个指令的时候,vue会要求你给循环列表的每一项添加唯一的key,那么这个...

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列表渲染时究竟起到了什么作用详解整合,希望对大家有所帮助