从 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 执行删除操作,移除老节点



目录
相关文章
|
1天前
|
设计模式 JavaScript 前端开发
Vue源码学习需要哪些工具和技能
【4月更文挑战第20天】学习Vue源码需具备的工具与技能:VS Code或WebStorm作为代码编辑器,Node.js与npm管理依赖,Git操作仓库。基础包括JavaScript、ES6+语法、前端知识(HTML/CSS/浏览器原理)及Vue基础知识。进阶则需源码阅读理解能力,调试技巧,熟悉设计模式和架构思想。学习方法强调系统学习、实践与持续关注Vue最新动态。
17 8
|
1天前
|
JavaScript 前端开发 编译器
Vue 源码学习路线
【4月更文挑战第20天】探索Vue源码涉及响应式系统、虚拟DOM、模板编译等核心概念。先掌握Vue基础知识、JavaScript(ES6+)和前端工程化。从源码入口文件开始,研究响应式、虚拟DOM、模板编译、实例方法、全局API及生命周期。理解编译器和渲染器工作原理,实践编写Vue插件,参与开源项目,阅读相关文章教程,持续关注Vue最新动态。这是一个循序渐进、需要耐心和实践的过程。
7 1
|
1天前
|
JavaScript 前端开发
鼠标监视 | 拖拽方块 | Vue
鼠标监视 | 拖拽方块 | Vue
5 0
|
2天前
|
JavaScript
Vue控制是否禁用disabled属性
Vue控制是否禁用disabled属性
7 1
|
2天前
|
JavaScript 前端开发
Vue鼠标悬浮切换图片
Vue鼠标悬浮切换图片
8 0
|
2天前
|
JavaScript 前端开发 内存技术
Vue入门:构建你的第一个Vue应用程序
【4月更文挑战第22天】Vue.js 入门教程:安装 Node.js 和 npm,使用 Vue CLI (`npm install -g @vue/cli`) 创建项目,选择预设或自定义配置。在 `src/components/` 创建 `HelloWorld.vue` 组件,显示数据属性。在 `App.vue` 中引入并注册组件,启动开发服务器 (`npm run serve`) 预览。开始你的 Vue 之旅!
|
2天前
|
JavaScript 前端开发 UED
动画效果:给 Vue 应用添加过渡和动画
【4月更文挑战第22天】Vue 框架提供强大的过渡和动画功能,增强用户体验,创建流畅的用户界面。通过CSS动画、设置过渡属性和自定义过渡,开发者能实现多样化效果。同时,结合组件状态、关注性能并测试优化,确保在不同设备上运行良好,打造引人入胜的应用交互。
|
5天前
|
JavaScript
Ant design Vue 父子组件传递(代码案例--不懂的地方可以私信本博主)
Ant design Vue 父子组件传递(代码案例--不懂的地方可以私信本博主)
10 0
|
5天前
|
JavaScript 前端开发
ant design vue 配置菜单外部打开
ant design vue 配置菜单外部打开
12 0
|
5天前
|
JavaScript
ant design vue 在列表中使用插槽 例如当性别为0的时候在列表中我想显示男
ant design vue 在列表中使用插槽 例如当性别为0的时候在列表中我想显示男
7 0