Vue 2.0 patch 原理分析

本文基于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原理就分析完了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,724评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,104评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,142评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,086评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,076评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,914评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,220评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,871评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,318评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,834评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,951评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,574评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,162评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,162评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,383评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,349评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,652评论 2 343

推荐阅读更多精彩内容