从 vue 源码看问题 —— vue 中如何进行 patch ?(三)

简介: 从 vue 源码看问题 —— vue 中如何进行 patch ?

updateChildren()

/*
   diff 过程:
     diff 优化:
               1. 四种假设:
                          newStart === oldStart
                          newEnd === oldEnd
                          newStart === oldEnd
                          newEnd === oldStart
               2. 假设新老节点开头结尾有相同节点的情况: 
                - 一旦命中假设,就避免了一次循环,以提高执行效率
                - 如果没有命中假设,则执行遍历,从老节点中找到新开始节点
                  找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
     如果老节点先于新节点遍历结束,则剩余的新节点执行新增节点操作
     如果新节点先于老节点遍历结束,则剩余的老节点执行删除操作,移除这些老节点
  */
  function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 老节点的开始索引
    let oldStartIdx = 0
    // 新节点的开始索引
    let newStartIdx = 0
    // 老节点的结束索引
    let oldEndIdx = oldCh.length - 1
    // 老节点的第一个子节点
    let oldStartVnode = oldCh[0]
    // 老节点的最后一个子节点
    let oldEndVnode = oldCh[oldEndIdx]
    // 新节点的结束索引
    let newEndIdx = newCh.length - 1
    // 新节点的第一个子节点
    let newStartVnode = newCh[0]
    // 新节点的最后一个子节点
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    // removeOnly 是一个特殊的标志,仅由 <transition-group> 使用,
    // 以确保被移除的元素在离开转换期间保持在正确的相对位置
    const canMove = !removeOnly
    if (process.env.NODE_ENV !== 'production') {
      // 检查新节点的 key 是否重复
      checkDuplicateKeys(newCh)
    }
    // 遍历新老两组节点,只要有一组遍历完(开始索引超过结束索引)则跳出循环
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        // 如果节点被移动,在当前索引上可能不存在,检测这种情况,如果节点不存在则调整索引
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        // 如果节点被移动,在当前索引上可能不存在,检测这种情况,如果节点不存在则调整索引
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 老开始节点和新开始节点是同一个节点,执行 patch
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // patch 结束后老开始和新开始的索引分别加 1,开始下一个节点
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 老结束和新结束是同一个节点,执行 patch
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // patch 结束后老结束和新结束的索引分别减 1,开始下一个节点
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 老开始和新结束是同一个节点,执行 patch
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // 处理被 transtion-group 包裹的组件时使用
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // patch 结束后老开始索引加 1,新结束索引减 1,开始下一个节点
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 老结束和新开始是同一个节点,执行 patch
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // patch 结束后,老结束的索引减 1,新开始的索引加 1,开始下一个节点
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 如果上面的四种假设都不成立,则通过遍历找到新开始节点在老节点中的位置索引
        // 找到老节点中每个节点 key 和 索引之间的关系映射:
        // 如 oldKeyToIdx = { key1: idx1, ... }
        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)) {
            // 如果这两个节点是同一个,则执行 patch
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // patch 结束后将该老节点置为 undefined
            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]
      }
    }
    // 走到这里,说明老节点或者新节点被遍历完了
    if (oldStartIdx > oldEndIdx) {
      // 老节点被遍历完了,新节点有剩余,则说明这部分剩余的节点是新增的节点,然后添加这些节点
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      // 新节点被遍历完了,老节点有剩余,说明这部分的节点被删掉了,则移除这些节点
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }
复制代码

checkDuplicateKeys()

// 检查一组元素的 key 是否重复 
  function checkDuplicateKeys(children) {
    const seenKeys = {}
    for (let i = 0; i < children.length; i++) {
      const vnode = children[i]
      const key = vnode.key
      if (isDef(key)) {
        if (seenKeys[key]) {
          warn(
            `Duplicate keys detected: '${key}'. This may cause an update error.`,
            vnode.context
          )
        } else {
          seenKeys[key] = true
        }
      }
    }
  }
复制代码

addVnodes()

// 在指定索引范围(startIdx —— endIdx)内添加节点
  function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
      createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
    }
  }
复制代码

createKeyToOldIdx()

// 得到指定范围(beginIdx —— endIdx)内节点的 key 和 索引之间的关系映射 => { key1: idx1, ... }
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
}
复制代码

findIdxInOld()

// 找到新节点(vnode)在老节点(oldCh)中的位置索引 
  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
    }
  }
复制代码

invokeCreateHooks()

/*
    调用各个模块的 create 方法,比如创建属性的、创建样式的、指令的等等,
    然后执行组件的 mounted 生命周期方法
  */ 
  function invokeCreateHooks(vnode, insertedVnodeQueue) {
    for (let i = 0; i < cbs.create.length; ++i) {
      cbs.create[i](emptyNode, vnode)
    }
    // 组件钩子
    i = vnode.data.hook // Reuse variable
    if (isDef(i)) {
      if (isDef(i.create)) i.create(emptyNode, vnode)
      // 调用组件的 insert 钩子,执行组件的 mounted 生命周期方法
      if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
    }
  }
复制代码

createChildren()

// 创建所有子节点,并将子节点插入父节点,形成一棵 DOM 树
  function createChildren(vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
      // children 是数组,表示是一组节点
      if (process.env.NODE_ENV !== 'production') {
        // 检测这组节点的 key 是否重复
        checkDuplicateKeys(children)
      }
      // 遍历这组节点,依次创建这些节点然后插入父节点,形成一棵 DOM 树
      for (let i = 0; i < children.length; ++i) {
        createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
      }
    } else if (isPrimitive(vnode.text)) {
      // 说明是文本节点,创建文本节点,并插入父节点
      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
    }
  }
复制代码

总结

Vue 中的 patch 都做了些什么?Vuepatch 负责组件的 首次渲染后续更新销毁组件

  • 如果老的 VNode 是真实元素,则表示首次渲染,创建整棵 DOM 树,并插入 body,然后移除老的模版节点
  • 如果老的 VNode 不是真实元素,并且新的 VNode 也存在,则表示更新阶段,执行 patchVnode
  • 首先是全量更新所有的属性
  • 如果新老 VNode 都有孩子,则递归执行 updateChildren,进行 diff 过程
  • 老的 VNode 没孩子,如果新的 VNode 有孩子,则新增这些新孩子节点
  • 如果老的 VNode 有孩子,新的 VNode 没孩子,则删除这些老孩子节点
  • 如果不符合上面的几种,说明属于更新文本节点
  • 如果新的 VNode 不存在,老的 VNode 存在,则调用 destroy 销毁老节点

diff 过程结合 DOM 特点的优化?

  • 同层比较(降低时间复杂度)深度优先(递归)
  • 根据 DOM 节点做了四种假设,假设新老 VNode 的开头结尾存在相同节点
  • 如果命中假设,就可避免一次循环,降低了 diff 时间复杂度,提高执行效率
  • 如果没有命中假设,则执行遍历,从老的 VNode 中找到新的 VNode 的开始节点
  • 找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
  • 如果老的 VNode 先于新的 VNode 遍历结束,则剩余的新的 VNode 执行新增节点操作
  • 如果新的 VNode 先于老的 VNode 遍历结束,则剩余的老的 VNode 执行删除操作,移除老节点



目录
相关文章
|
20小时前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的画师约稿平台附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的画师约稿平台附带文章源码部署视频讲解等
22 8
|
20小时前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的档案管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的档案管理系统附带文章源码部署视频讲解等
26 8
|
20小时前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的儿童性教育网站附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的儿童性教育网站附带文章源码部署视频讲解等
12 5
|
20小时前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的高校学生饮食推荐系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的高校学生饮食推荐系统附带文章源码部署视频讲解等
12 0
|
20小时前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的高校毕业与学位资格审核系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的高校毕业与学位资格审核系统附带文章源码部署视频讲解等
14 2
|
21小时前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的智慧养老院管理系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的智慧养老院管理系统附带文章和源代码部署视频讲解等
5 0
基于ssm+vue.js+uniapp小程序的智慧养老院管理系统附带文章和源代码部署视频讲解等
|
21小时前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp小程序的电子病历管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp小程序的电子病历管理系统附带文章源码部署视频讲解等
7 0
|
9月前
|
JavaScript 容器
【Vue源码解析】mustache模板引擎
【Vue源码解析】mustache模板引擎
35 0
|
11月前
|
JavaScript 前端开发
vue源码解析之mustache模板引擎
vue源码解析之mustache模板引擎
68 0
|
11月前
|
JavaScript
01 - vue源码解析之vue 数据绑定实现的核心 Object.defineProperty()
01 - vue源码解析之vue 数据绑定实现的核心 Object.defineProperty()
62 0