# patch

patch 和 Vue实例的挂载相关.

// 带有el参数, 会自动触发挂载
new Vue({
  el: '#app',
  render (h) {
    return h('div', {}, 'text')
  }
})
// 需要手动调用$mount来触发挂载
(new Vue({
  render (h) {
    return h('div', {}, 'text')
  }
})).$mount()
1
2
3
4
5
6
7
8
9
10
11
12
13

挂载流程中, 需要先通过render得到组件的virtual DOM, 然后将其patch到DOM中. 所谓的patch到DOM中就是根据vNode tree生成DOM元素vm.$el, 并将其插入到DOM中.

TIP

上面第二个例子没有指定挂载的DOM的节点, 因此不会挂载到DOM中, 需要手动挂载.

var MyComponent = Vue.extend({
  template: '<div>Hello!</div>'
})

// 创建并挂载到 #app (会替换 #app)
new MyComponent().$mount('#app')

// 同上
new MyComponent({ el: '#app' })

// 或者,在文档之外渲染并且随后挂载
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)
1
2
3
4
5
6
7
8
9
10
11
12
13

src/platforms/web/runtime/index.js

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
1
2
3
4
5
6
7

src/core/instance/lifecycle.js






 





 




export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
    vm.$el = el
// ...

  let updateComponent
  // ...
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

patch的过程在vm._update

# vm._update()

  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    // ....
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

关键代码如下

    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
1
2
3
4
5
6
7

分别对应首次渲染执行patch和状态更新执行patch

如果是首次patch, vm.$el取决于在实例化Vue的时候, 是否传递了el参数.

// 带有el参数, 会自动触发挂载
new Vue({
  el: '#app',
  render (h) {
    return h('div', {}, 'text')
  }
})
// 需要手动调用$mount来触发挂载
(new Vue({
  render (h) {
    return h('div', {}, 'text')
  }
})).$mount()
1
2
3
4
5
6
7
8
9
10
11
12
13

对上述的代码中的栗子, 前者vm.$el是id为#app的DOM元素, 后者为undefined

__patch__过程因平台而异, 对于web平台来说, 最终是对应DOM操作, 其定义在

src/platforms/web/runtime/index.js

import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
1
2

src/platforms/web/runtime/patch.js

/* @flow */

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })
1
2
3
4
5
6
7
8
9
10
11
12

可以看到为了代码复用, 和平台无关的patch逻辑代码在 core/vdom/ 中, 而DOM相关操作代码则分离出来在 web/runtime/ 下.

nodeOps中封装了DOM操作相关(create, insert, remove等)的函数.

modules则分为baseModulesplatformModules.

  • platformModules中封装了对DOM中style, class, events等相关的操作. 可以在patch的过程中调用
  • baseModules则是封装了directives/ref相关的操作

TIP

我们可以给custom directive提供相关钩子函数, 这些钩子函数就是在patch的过程中通过baseModules来调用的

TODO: modules

# patch

core/vdom/patch.js

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  const { modules, nodeOps } = backend

  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {}

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
  //...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

patch除了包括vNode更新时用到的相关对比算法以外, 还有另外一个很重要的功能, 就是在patch阶段各个阶段调用hooks, 而上文提到的modules就是被patch在各个阶段来调用的.

createPatchFunction利用闭包将modules中的钩子函数注册到cbs对象中, 并返回一个patch函数

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {

      } else {
        if (isRealElement) {

        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

可以看到patch函数主要主要处理如下几种情况

  • vnode创建时 (oldVnode传入undefined, 即调用$mount, 不传入el)
  • vnode更新时
    • 状态更新触发视图更新时, 且满足sameVnode
    • 传入了el参数, 此时oldVnode为真实的DOM元素
    • 状态更新触发视图更新时, 且不满足sameVnode, 且oldVnode不为真实的DOM元素

TIP

!isRealElement && sameVnode(oldVnode, vnode)这个判断有点疑问, 我觉得当满足sameVnode的时候, 能够保证!isRealElementtrue, 而vue以前的代码 (opens new window)确实只有sameVnode这一个判断条件, 可能是为了某种我暂时没有想到的边界条件

在patch最前面有这样一段逻辑

    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
1
2
3
4

当vnode未定义, 而oldVnode有定义时, 则执行destroy hook, 包括module中的destroy hook和组件的destroy hook.

这段逻辑其实和patch的关系并不大, 因为正常的patch过程, vnode再不济也是emptyVNode,

src/core/instance/lifecycle.js

  Vue.prototype.$destroy = function () {
    //...
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    //...    
  }
1
2
3
4
5
6
7
8

主要是为了处理一些边界情况, 可以查看相关提交和issue (opens new window)

# vnode创建时


    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      //...
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上述过程对应vnode创建时, 如下情况会走这分支

  • 实例化Vue不指定el参数, 调用$mount()来挂载(empty mount)
  • 创建子组件时(也是通过调用$mount())

TIP

var MyComponent = Vue.extend({
  template: '<div>Hello!</div>'
})

// create and mount to #app (will replace #app)
new MyComponent().$mount('#app')

// the above is the same as:
new MyComponent({ el: '#app' })

// or, render off-document and append afterwards:
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)
1
2
3
4
5
6
7
8
9
10
11
12
13

# vnode更新时

 const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {

      } else {
        if (isRealElement) {

        }
      }
1
2
3
4
5
6
7
8
  • 状态更新触发视图更新时, 且满足sameVnode
  • 传入了el参数, 此时oldVnode为真实的DOM元素
  • 状态更新触发视图更新时, 且不满足sameVnode, 且oldVnode不为真实的DOM元素

最后一种情况, 类似下面这种

new Vue({
  el: "#app",
  data () {
    return {
      test: true
    }
  },
  render (h) {
    if (this.test) {
    return h('div',{class: 'test'}, 'test')
    } else {
      return h('a', {}, 'a')
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

test变化触发视图更新时, oldVnode和 vnode不满足sameVnode

# case study 传入el参数的情况

new Vue({
  el: "#app",
  render (h) {
    return h('div',{class: 'test'}, [
      'child1',
      'child2'
    ])
  }
})
1
2
3
4
5
6
7
8
9
<div id="app"></div>
1

new Vue最终执行Vue.prototype._init中的挂载逻辑

src/core/init.js


  Vue.prototype._init = function (options?: Object) {
    // ...
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
1
2
3
4
5
6
7

src/platforms/web/runtime/index.js

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
1
2
3
4
5
6
7

由于我们指定了el参数, 所以最终调用

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
1

vm.$el为页面中id为app的div元素, vnode为根组件的virtual DOM, hydrating为服务端渲染相关, 为false, removeOnly则用于组件transition-group

在vue源码中你可能会经常看到hydrating这个与ssr相关的东西, 这里推荐一篇知乎的文章

不只是同构应用(isomorphic 工程化你所忽略的细节) - Lucas HC的文章 - 知乎 (opens new window)

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      //...
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // 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)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            //...
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          //,,,
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

满足sameVnode的情况, 执行patchVnode也是相对复杂的一部分, 我们的case study暂时没有涉及到.

  if (!isRealElement && sameVnode(oldVnode, vnode)) {
    // patch existing root node
    patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  } else {
1
2
3
4

对于__patch__返回的patch函数, 由于传入el, 不满足!isRealElement && sameVnode(oldVnode, vnode)

则根据#app的DOM元素创建一个vnode

oldVnode = emptyNodeAt(oldVnode)

// emptyNodeAt
  function emptyNodeAt (elm) {
    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
  }
1
2
3
4
5
6

之后需要做的就是根据vnode创建DOM元素, 准备替换#app

const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
1
2

oldVnode.elm对应#appDOM元素, parentElm则是其父元素body

之后根据vnode创建DOM元素

    createElm(
      vnode,
      insertedVnodeQueue,
      // extremely rare edge case: do not insert if old element is in a
      // leaving transition. Only happens when combining transition +
      // keep-alive + HOCs. (#4590)
      oldElm._leaveCb ? null : parentElm,
      nodeOps.nextSibling(oldElm)
    )
// createElm
  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

先跳过createElm的具体步骤, 可以预见, createElm就是通过vnode, 调用nodeOps中相关来创建DOM元素的, 并插入到parentElm(body)中

if (isDef(vnode.parent)) {
  //...
}
1
2
3

vnode.parent指的是组件vnode的占位符vnode, 我们的case中没有子组件, 所以不涉及.

  // destroy old node
  if (isDef(parentElm)) {
    removeVnodes(parentElm, [oldVnode], 0, 0)
  } else if (isDef(oldVnode.tag)) {
    invokeDestroyHook(oldVnode)
  }
1
2
3
4
5
6

之后就需要, 将body中的原来的#app删除掉

# 疑问

暂时没有想到会调用isDef(oldVnode.tag)分支的场景, 可以研究这个commit (opens new window)

更新: 想到了执行上面代码的场景

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
1

patch函数最后还执行了invokeInsertHook, 就像invokeDestroyHook一样, 这些和生命钩子相关的函数全是与Vue组件化相关的.

# createElm

  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        //...
      }

      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
        // ...
      } else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

分析一下createElm的主要逻辑

insertedVnodeQueue默认传入的是空数组. 同样是组件化相关

在上面的栗子中, parentElm是body, refElm则是#app的next sibling

    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }
1
2
3

如果vnode对应着组件, 则createComponent则返回true, 直接return, 我们的case不涉及子组件, 所以可以忽略

  vnode.elm = vnode.ns
    ? nodeOps.createElementNS(vnode.ns, tag)
    : nodeOps.createElement(tag, vnode)
  setScope(vnode)
1
2
3
4

DOM元素对应的vnode, 则创建对应DOM元素, 并设置style scope

  render (h) {
    return h('div',{class: 'test'}, [
      'child1',
      'child2',
    ])
  }
1
2
3
4
5
6

这里所创建的DOM元素为div元素

  function createChildren (vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(children)
      }
      for (let i = 0; i < children.length; ++i) {
        createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
      }
    } else if (isPrimitive(vnode.text)) {
      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12

接着创建div的子元素, 遍历children调用createElm, 并传入parentElm(vnode.elm class为test的div元素), 可见这是一个深度优先遍历的过程. 除此之外, refElmnull (因为不需要参考的元素, 直接appendChild往父元素中添加子元素就可以), 传入了ownerArray, index(作用暂且不讨论 TODO:)

      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
1
2
3
4

在创建子元素之后, 会调用invokeCreateHooks, insert, 显然insert的作用是将生成的DOM元素插入到DOM中

  function invokeCreateHooks (vnode, insertedVnodeQueue) {
    for (let i = 0; i < cbs.create.length; ++i) {
      cbs.create[i](emptyNode, vnode)
    }
    i = vnode.data.hook // Reuse variable
    if (isDef(i)) {
      if (isDef(i.create)) i.create(emptyNode, vnode)
      if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
    }
  }
1
2
3
4
5
6
7
8
9
10

invokeCreateHook的一个作用是触发vnode的create钩子, 我们的例子中, 所创建的div元素的class为test, 给DOM元素添加class的这个操作就是在create钩子中调用的

platformModules中封装了对DOM中style, class, events等相关的操作. 可以在patch的过程中调用

insertedVnodeQueue与组件化相关, 暂不提

最后patch函数返回vnode.elm, 并赋值给vm.$el

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
1

# chart

patch

# 更新

之前一处疑问

var a = new Vue({
  data () {
    return {
      test: true
    }
  },
  render (h) {
    if (this.test) {
    return h('div',{class: 'test', ref: 'test'}, 'test')
    } else {
      return h('a', {}, 'a')
    }
  }
})
a.$mount()
setTimeout(() => {
  a.test = false
}, 10000)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在上面的场景中, 当timeout中的逻辑执行时, 会销毁old vnode,

  // destroy old node
  if (isDef(parentElm)) {
    removeVnodes(parentElm, [oldVnode], 0, 0)
  } else if (isDef(oldVnode.tag)) {
    invokeDestroyHook(oldVnode)
  }
1
2
3
4
5
6

由于我们的Vue实例并没有关联到DOM中, 这样我们不需要从DOM中删除该vnode 对应的元素, 并执行一些destroy相关的hook. 我们仅仅需要执行destroy相关的hook, 比如, 如果我们使用了ref属性, 那么destroy的时候, 我们需要将其在vm.$refs中剔除, 置为undefined