vue2.x的patch方法和客户端混合(上)

730 阅读3分钟

vue2.x的patch方法和客户端混合

之前确实没自己看过 vue2.x 的 _update 这一块,导致今天被面试官问到了,现在回头补一下这方面的知识。

通过本文,您可以了解到:

  1. 怎么进行 patch 的?
  2. 怎么用 vnode 创建 DOM 元素的?
  3. patchVnode 是怎么进行的?
  4. 怎么进行客户端激活?
  5. vnode 的一个形象化比喻

从初始化 watcher 说起

我们知道,在声明了响应式数据之后,我们再实例化一个 watcher并调用响应式数据,才能把 watcher 添加到响应式数据的依赖里面获得更新。而 vue 也是这么做的,它对 vm 来实例化一个 watcher,这个 watcher 在声明的时候会立即对回调函数 updateComponent 求值,从而执行 _update 和 _render,从而把这个 watcher 添加到相应的响应式数据里面。源码如下:

// lifecycle.js
callHook(vm, 'beforeMount');
// ...
updateComponent = () => {
    vm._update(vm._render(), hydrating)
};
// ...
new Watcher(vm, updateComponent, noop, {
    before () {
        if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
        }
    }
}, true /* isRenderWatcher */)

这里有一个注意的点,就是这里的 _render 函数其实是调用的 render 函数,而解析 template 并进行静态节点优化以及生成render函数是在 $mount 方法里面进行的,也就是说 template 转化为 render 方法是在 beforeMount 生命周期里面进行的。(如果使用了 vue-loader 的话,那么 vue-loader 里面会直接把 template 打包成 render 方法。)

_render方法干了些什么呢

_render 方法生成了 vnode,代码如下:

// render.js
Vue.prototype._render = function (): VNode {
    vnode = render.call(vm._renderProxy, vm.$createElement)
    return vnode
}

_update方法干了些什么呢

_update 方法其实就是调用__patch__方法,代码如下:

// lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    if (!prevVnode) {
        // initial render
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
        // updates
        vm.$el = vm.__patch__(prevVnode, vnode)
    }
}

这里如果是初次渲染的话,prevNode 是没有的,所以会拿vm.$el这个 DOM 元素去和新的 vnode 进行 patch;如果不是初次渲染的话,就拿旧的 vnode 和 新的 vnode 进行 patch。

__patch__方法干了些什么呢

__patch__方法的源码和解释如下:

function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 如果没有新的 vnode,但有旧的 vnode,就销毁旧的 vnode
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // 如果旧的 vnode 没有,就表示是第一次渲染,则直接创建
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // 如果旧的 vnode 不是 DOM node的形式,并且新旧节点一样,则使用 patchVnode 进行更新
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // 如果旧的 vnode 是元素节点,并且有服务端渲染标签的话,就移除标签并且进行服务端渲染
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            // 先判断能不能进行混合,能的话,调用 invokeInsertHook 方法进行混合
            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.'
              )
            }
          }
          // 如果混合失败,则使用空的节点
          oldVnode = emptyNodeAt(oldVnode)
        }

        // 如果新旧节点不相同,则使用新的 vnode 进行渲染
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // 移除老的 vnode
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

这里简单来说分为下面几种情况:

  1. 如果没有新的 vnode 只有旧的 vnode,就直接摧毁掉旧的 vnode;
  2. 如果没有旧的 vnode,就直接用新的 vnode 创建 DOM 元素
  3. 对比新旧 vnode,如果是 sameVnode,就开始进行 patchVnode
  4. 如果旧的 vnode 是 DOM,并且有 ssr 标记,则清除 ssr 标记并进行客户端激活。(如果激活失败则创建一个空的节点);
  5. 如果旧的 vnode 是 DOM,并且没有 ssr 标记,就用新的 vnode 创建 DOM 元素,并且挂载到新的 vnode 的父节点上面。

上面过程中主要有三点需要注意:

  1. 怎么用 vnode 创建 DOM 元素的?
  2. patchVnode 是怎么进行的?
  3. 怎么进行客户端激活?

接下篇:vue2.x的patch方法和客户端混合(下)