Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
106 lines (80 sloc) 3.8 KB

2.2.2.1 列表渲染 v-for 的 key

前言

列表数据在变动时,对应的列表 DOM 树会随着更新。而往往我们更新列表数据只是更新其中某一项,如果整个列表重绘显然效率太低。为了让这里的 DOM 树重绘次数更少,需要引入 key 的概念 (官方文档),每个节点都有一个唯一的 key 标记,在 patch 节点 updateChildren 时,复用相同 key 的 DOM 元素。

前边说到,我们要引入新的语法糖需要做几个步骤的改造,这里引入的 key 语法比较简单,所以这篇文章就不啰嗦这个改造过程,我们的重点在 patch 算法如何优化,让 DOM 树重绘次数更少。

patch(oldVnode, newVnode) 算法的代码在 core/vdom/patch.js

1. 如何判断 VNode 是否是同一个节点

上边提到,在 diff 两个新旧 VNode 时,我们需要先判定这两个 VNode 是否可以复用,这样我们才能复用他们映射的 Dom 元素,避免创建新的 Dom 和 移除旧的 Dom。

之前我们仅仅通过 VNode 的标签名来判定这两个 VNode 是否一致,现在我们需要加入一个 key 的标记:

// core/vdom/patch.js
function sameVnode (vnode1, vnode2) {
  return (
    vnode1.key === vnode2.key && // 加入 key 判断
    vnode1.tag === vnode2.tag &&
    !vnode1.data === !vnode2.data
  )
}

2. 在 updateChildren 时如何复用同个 key 的VNode

之前的 updateChildren 逻辑在最后头尾都没相同元素的时候,直接创建一个新的 DOM 插入 parentElm。

// core/vdom/patch.js
function updateChildren (parentElm, oldCh, newCh, removeOnly) {
  // blabla..

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx]
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // blabla..
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // blabla..
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      // blabla..
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      // blabla..
    } else {
      // 我们需要从 oldCh 中找到同 key 的元素,复用它
      // 接下来我们通过示意图来讨论这里的逻辑
    }
  }
  // blabla..
}

为了复用相同 key 的VNode 节点的 Dom 元素,我们来讨论一下改造流程:

###2.1 创建旧children的 key 索引表 oldKeyToIdx

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
}

oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

2.2 从索引表里边找当前 key 可以复用的 VNode

idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null

如果找不到可以复用的 VNode 就走回之前的逻辑:

if (isUndef(idxInOld)) {
  createElm(newStartVnode, parentElm, oldStartVnode.elm)
  newStartVnode = newCh[++newStartIdx]
}

如果找到可以复用的 VNode,那么通过 patchVnode 复用旧 Dom,然后把 Dom 移动到新的位置。

elmToMove = oldCh[idxInOld] // 找到同key的元素

patchVnode(elmToMove, newStartVnode) // 先patch这个节点
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm) // 然后开始移动
newStartVnode = newCh[++newStartIdx]

代码整理

src源码新增了72行,总共1443行,查看分支2.2.2.1代码查看2.2.2.1新增代码