减少DOM操作的性能开销

简单来说就是复用已有的DOM,因为DOM频繁卸载挂载会导致性能损耗大,这也是为什么要有Diff算法的原因

DOM复用与key的作用

key的作用相当于节点的身份证。

// oldChildren
[
  { type: 'p', children: '1', key: 1 },
  { type: 'p', children: '2', key: 2 },
  { type: 'p', children: '3', key: 3 }
]

// newChildren
[
  { type: 'p', children: '3', key: 3 },
  { type: 'p', children: '1', key: 1 },
  { type: 'p', children: '2', key: 2 }
]

上方的vnode中,如果缺少了key, 则完全无法判断应该如何复用。

Untitled

所以我们认为,当新旧节点的type与key相同时,则可以复用

找到需要移动的元素

如何移动元素

这两个小章节是一起的,就不再分开了

理解下方代码

function patchChildren(n1, n2, container) {
  if (typeof n2.children === 'string') {
    // 省略部分代码
  } else if (Array.isArray(n2.children)) {
    const oldChildren = n1.children
    const newChildren = n2.children

    let lastIndex = 0
    for (let i = 0; i < newChildren.length; i++) {
      const newVNode = newChildren[i]
      let j = 0 // 这里每次都从0开始递归旧节点,好像有问题。同样的类型不应该复用两次才对啊
      for (j; j < oldChildren.length; j++) {
        const oldVNode = oldChildren[j]
        if (newVNode.key === oldVNode.key) { // 递归遍历,如果key相同则往下走。否则继续循环
          patch(oldVNode, newVNode, container) // 复用旧节点,修改内容,以及修改新节点指向的真实DOM
          if (j < lastIndex) {
            // 代码运行到这里,说明 newVNode 对应的真实 DOM 需要移动
            // 先获取 newVNode 的前一个 vnode,即 prevVNode
            const prevVNode = newChildren[i - 1]
            // 如果 prevVNode 不存在,则说明当前 newVNode 是第一个节点,它不需要移动
            if (prevVNode) { // Note: 这个方法其实是前遍历,这个新节点之前的节点应该都是已经排布好了,所以可以直接放到其后面(放到前一个新节点的真实DOM节点的兄弟节点的前面,也就是取代了兄弟节点)
              // 由于我们要将 newVNode 对应的真实 DOM 移动到prevVNode 所对应真实 DOM 后面,
              // 所以我们需要获取 prevVNode 所对应真实 DOM 的下一个兄弟节点,并将其作为锚点
              const anchor = prevVNode.el.nextSibling
              // 调用 insert 方法将 newVNode 对应的真实 DOM 插入到锚点元素前面,
              // 也就是 prevVNode 对应真实 DOM 的后面
              insert(newVNode.el, container, anchor)
            }
          } else {
            lastIndex = j
          }
          break
        }
      }
    }

  } else {
    // 省略部分代码
  }
}

Untitled

  1. 代码中两重遍历,由新节点循环包裹旧节点循环。首先取到新节点p-3, 然后遍历查找旧节点,发现可以复用p-3, 此时lastIndex < j所以lastIndex = j也就是等于2
  2. 然后取新节点p-1进行遍历旧节点,发现可以复用旧节点的p-1, 此时j = 0。满足j < lastIndex的条件。需要将其移动到前一个新节点的后面,也就是p-3的后面(而此时p-1前的节点都已经是排列好了的)
  3. 然后取新节点p-2进行遍历,发现可以复用旧节点p-2, 此时j=1。满足j < lastIndex的条件,需要将其移动到前一个新节点的后面,也就是p-2的后面。此时遍历完成,真实DOM的顺序与新节点顺序一致。

这个排序方法为前循环遍历,当循环到当前节点时,表明当前节点之前的节点已经排好了序列,那么根据顺序,将当前节点排在前一个节点的后边即可。

添加新元素

添加新元素的逻辑也比较简单,就是没有找到type与key相同的元素,则新增一个元素,并将这个元素插到前一个元素的后面即可。这里不做展开。