zoukankan      html  css  js  c++  java
  • 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. 怎么进行客户端激活?
  • 相关阅读:
    初识Tensorboard
    sql优化的几种方法
    nginx+ffmpeg+jwplayer
    jwplayer播放器
    详解spring 每个jar的作用
    RabbitMQ安装
    Migration 使用方法
    VisualSVN server 启用日志编辑
    nodejs prefix(全局)和cache(缓存)windows下设置
    python3 eval字符串str 转字典dict
  • 原文地址:https://www.cnblogs.com/yangzhou33/p/13861424.html
Copyright © 2011-2022 走看看