美文网首页Vuevue原理
Vue 2.0 patch 原理分析

Vue 2.0 patch 原理分析

作者: fehysunny | 来源:发表于2017-10-02 12:14 被阅读866次

    本文基于vue-2.4.4源码进行分析

    Vue 2.0开始,引入VirtualDOM

    使用VirtualDOM而不使用真实DOM是出于性能优化的考虑。

    真实DOM使用document.createElement创建DOM元素,但是这个方法会带来性能上的损失。

    举个例子:

    let div = document.createElement('div');
    let count = 0
    for(let k in div) {
        count++
    }
    console.log(count)  // 231
    

    执行上面的代码,我们可以看到该方法创建的DOM元素的属性多达231个,但是我们真正需要的可能只有不到10%。

    为了解决这个问题,VirtualDOM应运而生。它和真实DOM保持映射关系,每个VNode节点都存储了对应真实DOM节点的一些重要参数,当数据发生改变时,在改变真实DOM节点之前,会先比较相应的VNode的的数据,如果需要改变,才更新真实DOM。这样就可以通过操作VirtualDOM来提高直接操作DOM的效率和性能。

    比较VNode数据这个操作就是我们今天要讨论的patch,在讨论之前,我们先简单说下VNode

    VNode

    在上篇Vue 2.0 模板编译源码分析中我们得出模板编译的结果是render function

    render function的运行结果就是VNode, 参考src/core/instance/render.js

    Vue.prototype._render = function (): VNode {
      ...
      const {
        render,
        staticRenderFns,
        _parentVnode
      } = vm.$options
      ... 
      vnode = render.call(vm._renderProxy, vm.$createElement)
      ...
    }   
    

    Vue 2.0中的VNode(src/core/vdom/vnode.js)定义如下:

    export default class VNode {
    constructor (
        tag?: string,
        data?: VNodeData,
        children?: ?Array<VNode>,
        text?: string,
        elm?: Node,
        context?: Component,
        componentOptions?: VNodeComponentOptions,
        asyncFactory?: Function
      ) {
        this.tag = tag    // 元素标签
        this.data = data    // 属性
        this.children = children    // 子元素列表
        this.text = text
        this.elm = elm    //  对应的真实 DOM 元素
        this.ns = undefined
        this.context = context
        this.functionalContext = undefined
        this.key = data && data.key
        this.componentOptions = componentOptions
        this.componentInstance = undefined
        this.parent = undefined
        this.raw = false
        this.isStatic = false     // 是否被标记为静态节点
        this.isRootInsert = true
        this.isComment = false
        this.isCloned = false
        this.isOnce = false
        this.asyncFactory = asyncFactory
        this.asyncMeta = undefined
        this.isAsyncPlaceholder = false
      }
    }
    

    它是真实DOM的简化版,与真实DOM一一对映。通过new实例化的VNode可以分为:EmptyVNode(注释节点)、TextVNode(文本节点)、ElementVNode(元素节点)、ComponentVNode(组件节点)、CloneVNode(克隆节点)等。

    patch原理

    再拉通一下整个思路,目前我们晓得

    render function 生成 VNode,是在 vm._render 里完成的。

    那么vm._render方法又是在什么时候调用的呢?

    debugger一下代码,可以看到流程如下:

    初始化时,通过render function 生成 VNode的同时进行Watcher的绑定。当数据发生会变化时,会执行_update方法,生成一个新的VNode对象,然后调用__patch__方法,比较新生成的VNode和旧的VNode,最后将差异(变化的节点)更新到真实的DOM树上。

    patch(src/core/vdom/patch.js)所用的diff算法来源于snabbdom,只会在同层级进行比较,不会跨层级比较。图示如下:

    diff algorithm (by Christopher Chedeau)

    下面结合源码进行原理分析:

    入参

    patch方法接收6个参数:

    function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
      ...
    }
    
    • oldVnode: 旧的VNode或旧的真实DOM节点
    • vnode: 新的VNode
    • hydrating: 是否要和真实DOM混合
    • removeOnly: 特殊的flag,用于<transition-group>
    • parentElm: 父节点
    • refElm: 新节点将插入到refElm之前

    流程

    1. 如果vnode不存在,但是oldVnode存在,说明是需要销毁旧节点,则调用invokeDestroyHook(oldVnode)来销毁oldVnode

      if (isUndef(vnode)) {
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
        return
      }
      
    2. 如果vnode存在,但是oldVnode不存在,说明是需要创建新节点,则调用createElm来创建新节点。

      if (isUndef(oldVnode)) {
       isInitialPatch = true  // 用于做延迟插值处理
       createElm(vnode, insertedVnodeQueue, parentElm, refElm)
      }   
      
    3. vnodeoldVnode都存在时

    • 3.1 如果oldVnode不是真实节点,并且vnodeoldVnode是同一节点时,说明是需要比较新旧节点,则调用patchVnode进行patch

    • 3.2 如果oldVnode是真实节点时

      • 3.2.1 如果oldVnode是元素节点,且含有data-server-rendered属性时,移除该属性,并设置hydratingtrue
      • 3.2.2 如果hydratingtrue时,调用hydrate方法,将Virtural DOM与真实DOM进行映射,然后将oldVnode设置为对应的Virtual DOM
    • 3.3 如果oldVnode是真实节点时或vnodeoldVnode不是同一节点时,找到oldVnode.elm的父节点,根据vnode创建一个真实的DOM节点,并插入到该父节点中的oldVnode.elm位置。如果组件根节点被替换,遍历更新父节点element。然后移除旧节点。

      {
          // 3. 当vnode和oldVnode都存在时
          const isRealElement = isDef(oldVnode.nodeType)
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
           // 3.1 如果oldVnode不是真实节点,并且vnode和oldVnode是同一节点时
          // patch existing root node
          patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
        } else {
          if (isRealElement) {
              // 3.2 如果oldVnode是真实节点时
            // 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)) {
              // 3.2.1 如果oldVnode是元素节点,且含有`data-server-rendered`属性时
              oldVnode.removeAttribute(SSR_ATTR)
              hydrating = true
            }
            if (isTrue(hydrating)) {
              // 3.2.2 如果hydrating为true时
              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)
          }
          // 3.3 
          // replacing existing element
          const oldElm = oldVnode.elm
          const parentElm = nodeOps.parentNode(oldElm)
          createElm(
            vnode,
            insertedVnodeQueue,
            oldElm._leaveCb ? null : parentElm,
            nodeOps.nextSibling(oldElm)
          )
      
          if (isDef(vnode.parent)) {
            // component root element replaced.
            // update parent placeholder node element, recursively
            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) {
                  // start at index 1 to avoid re-invoking component mounted hook
                  for (let i = 1; i < insert.fns.length; i++) {
                    insert.fns[i]()
                  }
                }
              }
              ancestor = ancestor.parent
            }
          }
      
          if (isDef(parentElm)) {
              // 移除老节点
            removeVnodes(parentElm, [oldVnode], 0, 0)
          } else if (isDef(oldVnode.tag)) {
            invokeDestroyHook(oldVnode)
          }
        }
      
    1. 最后返回 vnode.elm

    原理

    由上面的流程我们知道了当vnodeoldVnode都存在、oldVnode不是真实节点,并且vnodeoldVnode是同一节点时,才会调用patchVnode进行patch

    下面根据patchVnode源码分析patch的原理:

    1. 如果oldVnodevnode完全一致,则可认为没有变化,return
    2. 如果oldVnodeisAsyncPlaceholder属性为true时,跳过检查异步组件,return
    3. 如果oldVnodevnode都是静态节点,且具有相同的key,并且当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elmoldVnode.child都复制到vnode上,也不用再有其他操作,return
    4. 否则,如果vnode不是文本节点时
    • 4.1 如果vnodeoldVnode都有子节点并且两者的子节点不一致时,就调用updateChildren更新子节点。updateChildren方法详细的解析可参考解析vue2.0的diff算法,图示说明,很形象。

    • 4.2 如果只有vnode有子节点,则调用addVnodes创建子节点;

    • 4.3 如果只有oldVnode有子节点,则调用removeVnodes把这些节点都删除;

    • 4.4 如果oldVnodevnode都没有子节点,但是oldVnode是文本节点时,则把vnode.elm的文本设置为空字符串;

    1. 如果vnode是文本节点但是vnode.text != oldVnode.text时只需要更新vnode.elm的文本内容就可以。

    原理流程图如下:

    自此,Vue的patch原理就分析完了。

    相关文章

      网友评论

        本文标题:Vue 2.0 patch 原理分析

        本文链接:https://www.haomeiwen.com/subject/mgrzextx.html