Fork me on GitHub

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

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

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

从初始化 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. 怎么进行客户端激活?
posted @ 2020-10-22 22:27  馒头加梨子  阅读(235)  评论(0编辑  收藏  举报