vue2-patch流程分析

简介: 我们在上篇文章分析了虚拟节点的创建及渲染流程,其中也有简单分析了 patch 过程,但是对于新旧节点的对比逻没有去仔细分析,所以我们打算好好梳理下 patch 的整个流程。

我们在上篇文章分析了虚拟节点的创建及渲染流程,其中也有简单分析了 patch 过程,但是对于新旧节点的对比逻没有去仔细分析,所以我们打算好好梳理下 patch 的整个流程。


PATCH入口


有了上篇文章的基础,我们简单再过过入口就行了,首先在 updateComponent 函数中调用 vm._update


Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  vm._vnode = vnode
  if (!prevVnode) {
    // 首次渲染的入参为真实节点
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // 更新的入参为组件的上次虚拟节点
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
}
复制代码

不难找到 __patch__ 的最终调用

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
复制代码


我们接下来的的重点则是分析 createPatchFunction,并注意此处的传参 nodeOpsmodules


首次PATCH


我们先来分析首次渲染的情况

export const emptyNode = new VNode('', {}, [])
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  const { modules, nodeOps } = backend
  // 节点数据更新函数添加到cbs
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 首次渲染将走到这里
        if (isRealElement) {
          // 创建新的空节点作为oldVnode
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }
        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
        // destroy old node
        if (isDef(parentElm)) {
          debugger
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}
复制代码


首次渲染的时候,会执行 createElm,将 vnode 作为参数传递,我们看看 createElm 逻辑


function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // 注意这边如果含有ownerArray参数的时候会执行个浅拷贝的过程
  // 我的理解是为了避免vnode在下面的代码中被我们注入其它属性污染
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }
  vnode.isRootInsert = !nested // for transition enter check
  // 组件逻辑暂不分析
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      // 会执行个检查逻辑检查标签的合法性
      if (isUnknownElement(vnode, creatingElmInVPre)) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }
    // 调用createElement创建真实节点
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)
    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      // 创建子节点
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        // 执行节点更新函数
        // vnode数据更新节点样式及属性、事件等
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      // 插入节点
      insert(parentElm, vnode.elm, refElm)
    }
  } else if (isTrue(vnode.isComment)) {
    // 注释节点
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    // 文本节点
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}
复制代码


可以看到 createElm 的逻辑并不复杂,其实就是执行 createElement -> createChildren -> invokeCreateHooks -> insert 的流程。我们再来看看 createChildren


function createChildren (vnode, children, insertedVnodeQueue) {
  // 先检查 `key` 是否有重复
  // 也是我们经常看到的警告
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(children)
    }
    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)))
  }
}
复制代码


createChildren 的主要逻辑就是检查 key 是否有重复,然后递归调用 createElm,最后执行 nodeOps.appendChild 添加节点到父节点。

最后在 patch 中返回 vnode.elm 完成 patch


更新PATCH


我们再来看看数据更新的时候执行的 patch

return function patch (oldVnode, vnode, hydrating, removeOnly) {
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }
  let isInitialPatch = false
  const insertedVnodeQueue = []
  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      // 更新主要执行的是patchVnode逻辑
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    }
  }
}
复制代码


我们来看看 patchVnode 的代码

function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // 如果节点数据相同则不同更新
  if (oldVnode === vnode) {
    return
  }
  // 浅拷贝
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
  }
  // 赋值elm
  const elm = vnode.elm = oldVnode.elm
  // ...
  // 略过异步组件及静态节点逻辑
  let i
  const data = vnode.data
  // 组件的prepatch hook
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }
  // 新旧子节点数组
  const oldCh = oldVnode.children
  const ch = vnode.children
  if (isDef(data) && isPatchable(vnode)) {
    // 更新节点属性样式等
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    // 组件hook相关
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }
  // 执行子节点对比逻辑
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      // 1. 新旧子节点数组都存在则调用updateChildren
      // updateChildren也就是vue中比较出名的diff算法
      // 值得深入研究一番
      // 因为以前分析过这边就不再分析了
      // 感兴趣可以前往https://juejin.cn/post/6927177789426630663查看
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      // 2. 只存在新子节点数组
      // 判断key属性重复警告
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(ch)
      }
      // 将textContent设置为空
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      // 执行addVnodes添加子节点
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      // 3. 只存在旧子节点数组
      // 直接移除旧子节点
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      // 4. 无新旧子节点 只存在text
      // 将textContent设置为空
      nodeOps.setTextContent(elm, '')
    }
  // 5. 文本节点则直接设置文本即可
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
  // 触发组件的postpatch钩子略过
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}
复制代码

patchVnode 的逻辑也很清晰,先调用更新函数 update 更新节点的属性样式等,再通过节点属性及新旧节点的子节点数组对比来更新节点,执行 addVnodes 添加节点或 removeVnodes 删除节点或者是执行 setTextContent 更新节点文本即可。


我们再来看看 addVnodesremoveVnodes 的逻辑

addVnodes 比较简单,就是依次调用 createElm 创建新节点并挂载在 parentElm下,parentElm 也就是刚才 patchVnode 中的真实节点


function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
  for (; startIdx <= endIdx; ++startIdx) {
    createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
  }
}
复制代码

removeVnodes 稍微复杂一点,会编辑节点数据依次执行 removeAndInvokeRemoveHookinvokeDestroyHook


function removeVnodes (vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    const ch = vnodes[startIdx]
    if (isDef(ch)) {
      if (isDef(ch.tag)) {
        // 触发removeNode及组件节点的一些操作
        removeAndInvokeRemoveHook(ch)
        invokeDestroyHook(ch)
      } else { // Text node
        removeNode(ch.elm)
      }
    }
  }
}
复制代码

再继续看看 removeAndInvokeRemoveHook


function removeAndInvokeRemoveHook (vnode, rm) {
  if (isDef(rm) || isDef(vnode.data)) {
    // 略过
  } else {
    // 我们知道其主要执行removeNode就行
    // removeNode则是直接调用removeChild移除节点
    removeNode(vnode.elm)
  }
}
function removeNode (el) {
  const parent = nodeOps.parentNode(el)
  // element may have already been removed due to v-html / v-text
  if (isDef(parent)) {
    nodeOps.removeChild(parent, el)
  }
}
复制代码

最后再来看看 invokeDestroyHook


function invokeDestroyHook (vnode) {
  let i, j
  const data = vnode.data
  if (isDef(data)) {
    // 触发属性更新函数destroy
    if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
    // 触发组件相关的destory钩子
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  }
  if (isDef(i = vnode.children)) {
    // 同时需要对其子节点进行递归调用invokeDestroyHook进行节点移除销毁
    for (j = 0; j < vnode.children.length; ++j) {
      invokeDestroyHook(vnode.children[j])
    }
  }
}
复制代码


至此,我们就完成 patch 函数的分析。可以得到 patch 函数主要是通过 vnode 数据来创建真实节点,在更新时通过 patchVnode 来达到 diff 更新。这样就完成了虚拟节点到真实节点的映射。其中 diff 相关的分析大家可以看vueDiff 算法解读


总结


本篇文章分析了 patch 函数的一个执行流程,主要分析首次创建及更新两个过程来分析。因为写的比较匆忙,有些逻辑分析的不是特别清楚,有部分略过的逻辑,如组件的钩子函数执行,后面将在组件化实现中专门对其进行分析。good good staduy day day up


相关文章
|
4天前
|
前端开发
利用React Hooks优化前端状态管理
本文将深入介绍如何利用React Hooks优化前端状态管理,包括Hooks的概念、使用方法以及与传统状态管理方式的对比分析,帮助前端开发人员更好地理解和应用这一现代化的状态管理方案。
|
4天前
|
JavaScript 前端开发 API
【Vue3】Hooks:一种全新的组件逻辑组织方式
【Vue3】Hooks:一种全新的组件逻辑组织方式
|
9月前
|
前端开发
【React工作记录八十六】React+Hook+ts+antDesignMobile实现input自动获取功能
【React工作记录八十六】React+Hook+ts+antDesignMobile实现input自动获取功能
55 0
|
JavaScript
从 vue 源码看问题 —— vue 中如何进行 patch ?(三)
从 vue 源码看问题 —— vue 中如何进行 patch ?
94 0
|
JavaScript 算法
从 vue 源码看问题 —— vue 中如何进行 patch ?(一)
从 vue 源码看问题 —— vue 中如何进行 patch ?
128 0
|
JavaScript
从 vue 源码看问题 —— vue 中如何进行 patch ?(二)
从 vue 源码看问题 —— vue 中如何进行 patch ?
63 0
|
算法 JavaScript
vue3组件更新流程
还记得组件挂载阶段中的 setupRenderEffect么? 在这里的时候会进行依赖收集,会在实例instance上挂载一个方法
vue3组件更新流程
|
JavaScript
JSPatch下发笔记2
JSPatch下发笔记2
113 0
|
JavaScript
JSPatch下发笔记9
JSPatch下发笔记9
80 0
|
JavaScript
JSPatch下发笔记10
JSPatch下发笔记10
91 0