Skip to content

vm._update()

作用

将虚拟Dom渲染成真实Dom

时机

它会在两个时机调用,即首次渲染和数据更新。 目前还在分析首次渲染阶段,先不看数据更新。复习在 mount一节中提到挂载时调用了mountComponent,在mountComponent中定义了updateComponent函数,将作为new Watcher的回调函数,vm._update就是在该回调函数中被执行的。

定义

它定义在 src/core/instance/lifecycle.js中。

js
 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    // 缓存当前vm实例
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      // 将vm.$el转换成vnode,与当前vnode比较,将比较的结果更新到真实的dom,并存储到$el中
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates 比较 prevVnode 和 vnode
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

_update 的核心就在于vm.__patch__(),而vm.__patch__()的定义与平台相关,因此它定义在src/platforms/web/runtime/patch.js中,

Vue.prototype.__patch__ = inBrowser ? patch : noop

这里判断是否浏览器环境,是因为在服务端渲染中,没有真实的浏览器 DOM 环境,所以不需要把 VNode 最终转换成 DOM,因此是一个空函数。 在浏览器端渲染时,定义为patch方法,找到它的定义src/platforms/web/runtime/patch.js

js
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)
// nodeOps 就是操作dom的api
export const patch: Function = createPatchFunction({ nodeOps, modules })

从上面可以看出patch方法由createPatchFunction方法生成,并在其中传递了对象{ nodeOps, modules }。进入createPatchFunction方法中, createPatchFunction 内部定义了一系列的辅助方法,最终返回了一个 patch 方法,这个方法就赋值给了 vm._update 函数里调用的 vm.__patch__

patch

patch 方法接收 4个参数,oldVnode 表示旧的 VNode 节点,它也可以不存在或者是一个 DOM 对象;VNode 表示执行 _render 后返回的 VNode 的节点;hydrating 表示是否是服务端渲染;removeOnly 是给 内置组件 transition-group 用的。patch中的代码判断是比较多的,不过重点是判断第一个参数是真实 Dom 还是虚拟 Dom

JS
      // 真实dom元素,说明是首次渲染
      const isRealElement = isDef(oldVnode.nodeType)
      //
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        // 比较差异并更新dom
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // 首次渲染
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // 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)
        )

如果oldVnode存在,则继续判断isRealElement;如果不是首次渲染,isRealElementfalse,则调用 patchVnode 来比较旧节点和当前节点;如果是首次渲染,isRealElementtrue,调用 emptyNodeAt 生成一个新的 VNode 来替换 oldVnode,最后调用 createElm

patchVnode

patchVnode 非常重要,因为其中涉及到diff算法。之前介绍虚拟dom库snabbdom时介绍过,Vue实现虚拟Dom映射到真实的DOM同样经过了create vnodepatch的过程,核心一致,只是 Vue 加入了一些处理平台相关和 Vue 特性

进入patchVnode,它主要做的事情就是对比新旧VNode,以及新旧VNode的子节点更新差异,如果新旧VNode都有子节点并且子节点不同,会调用updateChildren对比子节点的差异。

js
  function patchVnode(
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // 其他处理
    ...

    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)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 新节点没有文本
    if (isUndef(vnode.text)) {
      // 新老节点都有子节点
      if (isDef(oldCh) && isDef(ch)) {
        // 新老节点不相等,对子节点进行diff操作
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // 新的有子节点,老的没有
        if (process.env.NODE_ENV !== 'production') {
          // 检查新节点的子节点中是否有重复的key值
          checkDuplicateKeys(ch)
        }
        // 清空老节点的内容
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // 新节点的子节点转化成真实dom 挂载到dom树
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 新的没有子节点,老的有子节点
        // 删除老节点中的子节点
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 老的只有文本内容,清空内容
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 新老节点都有文本节点
      // 直接修改文本
      nodeOps.setTextContent(elm, vnode.text)
    }

  // 其他处理
   ...
  }

首先判断新节点是否有text,如果有,且旧节点也有text,则直接修改节点文本; 如果新节点没有text,这里要分几种情况:

  • 如果新老节点都有子节点,且新老节点的的子节点不相等,在对子节点进行diff操作,执行updateChildren,这个稍后再分析
  • 如果老节点没有子节点,新节点有子节点,则先清空老节点中的内容,再将新节点的子节点转化成真实Dom 挂载到Dom
  • 如果老节点有子节点,而新节点没有子节点,则删除老节点中的子节点
  • 以上情况都不符合时,判断老节点是否text,如果有,则先清空节点中的内容

updateChildren

updateChildren的实现核心就是diff算法,不过在文章中已经分析的比较详细了虚拟dom与diff算法详解[https://juejin.cn/post/7083007456900546573],这里简约给出:

  • 从头和尾开始一次找到相同的子节点进行比较patchVnode,共有四种比较方式
  • 四种比较都不满足时,在老节点的子节点中查找newStartVnode
  • 如果新节点比老节点多,将新增的子节点插入到Dom
  • 如果老节点比新节点多,删除多余的老节点

createElm

createElm的作用就是将VNode转换成真实Dom,并插入到Dom树中,先判断createComponent是否为true,这是用来尝试创建子组件。接着判断VNode是否包含tag,如果tag是一个合法标签,则调用平台Dom的操作创建一个占位符元素。如果VNode节点不包含tag,则它有可能是一个注释或者纯文本节点,可以直接插入到父元素中。

然后需要把节点的children也要转换成真实的Dom,调用createChildren,遍历子虚拟节点,递归调用 createElm。最后会调用insert方法把DOM 插入到父节点中,子元素会优先insert

总结

这部分涉及的内容比较多,涉及到虚拟Domdiff算法等等,掌握它们最好的方式是多调试,从最简单的例子调试即可。

  • invokeCreateHooks有什么作用? TODO