Vue的_update是实例的一个私有方法,它被调用的时机有2个

  1. 首次渲染
  2. 数据更新时

_update方法的作用是把VNode渲染成真实的DOM

_update

定义在src/core/instance/lifecycle.js 中

  1. Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  2. const vm: Component = this
  3. const prevEl = vm.$el
  4. const prevVnode = vm._vnode
  5. const restoreActiveInstance = setActiveInstance(vm)
  6. vm._vnode = vnode
  7. // Vue.prototype.__patch__ is injected in entry points
  8. // based on the rendering backend used.
  9. if (!prevVnode) {
  10. // initial render
  11. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  12. } else {
  13. // updates
  14. vm.$el = vm.__patch__(prevVnode, vnode)
  15. }
  16. restoreActiveInstance()
  17. // update __vue__ reference
  18. if (prevEl) {
  19. prevEl.__vue__ = null
  20. }
  21. if (vm.$el) {
  22. vm.$el.__vue__ = vm
  23. }
  24. // if parent is an HOC, update its $el as well
  25. if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
  26. vm.$parent.$el = vm.$el
  27. }
  28. // updated hook is called by the scheduler to ensure that children are
  29. // updated in a parent's updated hook.
  30. }

update的核心就是调用vm._patch方法,在不同平台实现不同;在Web和Weex上定义不一样

vm.patch在Web平台下的定义

在 src/platforms/web/runtime/index.js 中

  1. Vue.prototype.__patch__ = inBrowser ? patch : noop

noop定义在shared/util中

  1. export function noop (a?: any, b?: any, c?: any) {}

在Web平台上是否是服务端渲染也会对这个方法产生影响
在服务端渲染中没有真实的浏览器DOM环境,所以不需要把VNode最终转换成DOM,因此是一个空函数
在浏览器渲染中patch指向了patch方法
patch定义在src/platforms/web/runtime/patch.js中

  1. /* @flow */
  2. import * as nodeOps from 'web/runtime/node-ops'
  3. import { createPatchFunction } from 'core/vdom/patch'
  4. import baseModules from 'core/vdom/modules/index'
  5. import platformModules from 'web/runtime/modules/index'
  6. // the directive module should be applied last, after all
  7. // built-in modules have been applied.
  8. const modules = platformModules.concat(baseModules)
  9. // patch方法的定义是调用createPatchFunction方法的返回值,传入一个对象
  10. export const patch: Function = createPatchFunction({
  11. nodeOps, // 封装了一系列DOM操作方法
  12. modules // 定义了一些模块的钩子函数的实现
  13. })

createPatchFunction

定义在 src/core/vdom/patch.js 中

  1. const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
  2. export function createPatchFunction (backend) {
  3. let i, j
  4. const cbs = {}
  5. const { modules, nodeOps } = backend
  6. for (i = 0; i < hooks.length; ++i) {
  7. cbs[hooks[i]] = []
  8. for (j = 0; j < modules.length; ++j) {
  9. if (isDef(modules[j][hooks[i]])) {
  10. cbs[hooks[i]].push(modules[j][hooks[i]])
  11. }
  12. }
  13. }
  14. // ...
  15. return function patch (
  16. oldVnode, // 旧的节点,也可以不存在或者是一个DOM对象
  17. vnode, // 执行_render后返回的VNode节点
  18. hydrating, // 是否是服务端渲染
  19. removeOnly // 给transition-group用的
  20. ) {
  21. if (isUndef(vnode)) {
  22. if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
  23. return
  24. }
  25. let isInitialPatch = false
  26. const insertedVnodeQueue = []
  27. if (isUndef(oldVnode)) {
  28. // empty mount (likely as component), create new root element
  29. isInitialPatch = true
  30. createElm(vnode, insertedVnodeQueue)
  31. } else {
  32. const isRealElement = isDef(oldVnode.nodeType)
  33. if (!isRealElement && sameVnode(oldVnode, vnode)) {
  34. // patch existing root node
  35. patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  36. } else {
  37. if (isRealElement) {
  38. // mounting to a real element
  39. // check if this is server-rendered content and if we can perform
  40. // a successful hydration.
  41. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
  42. oldVnode.removeAttribute(SSR_ATTR)
  43. hydrating = true
  44. }
  45. if (isTrue(hydrating)) {
  46. if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
  47. invokeInsertHook(vnode, insertedVnodeQueue, true)
  48. return oldVnode
  49. } else if (process.env.NODE_ENV !== 'production') {
  50. warn(
  51. 'The client-side rendered virtual DOM tree is not matching ' +
  52. 'server-rendered content. This is likely caused by incorrect ' +
  53. 'HTML markup, for example nesting block-level elements inside ' +
  54. '<p>, or missing <tbody>. Bailing hydration and performing ' +
  55. 'full client-side render.'
  56. )
  57. }
  58. }
  59. // either not server-rendered, or hydration failed.
  60. // create an empty node and replace it
  61. oldVnode = emptyNodeAt(oldVnode)
  62. }
  63. // replacing existing element
  64. const oldElm = oldVnode.elm
  65. const parentElm = nodeOps.parentNode(oldElm)
  66. // create new node
  67. createElm(
  68. vnode,
  69. insertedVnodeQueue,
  70. // extremely rare edge case: do not insert if old element is in a
  71. // leaving transition. Only happens when combining transition +
  72. // keep-alive + HOCs. (#4590)
  73. oldElm._leaveCb ? null : parentElm,
  74. nodeOps.nextSibling(oldElm)
  75. )
  76. // update parent placeholder node element, recursively
  77. if (isDef(vnode.parent)) {
  78. let ancestor = vnode.parent
  79. const patchable = isPatchable(vnode)
  80. while (ancestor) {
  81. for (let i = 0; i < cbs.destroy.length; ++i) {
  82. cbs.destroy[i](ancestor)
  83. }
  84. ancestor.elm = vnode.elm
  85. if (patchable) {
  86. for (let i = 0; i < cbs.create.length; ++i) {
  87. cbs.create[i](emptyNode, ancestor)
  88. }
  89. // #6513
  90. // invoke insert hooks that may have been merged by create hooks.
  91. // e.g. for directives that uses the "inserted" hook.
  92. const insert = ancestor.data.hook.insert
  93. if (insert.merged) {
  94. // start at index 1 to avoid re-invoking component mounted hook
  95. for (let i = 1; i < insert.fns.length; i++) {
  96. insert.fns[i]()
  97. }
  98. }
  99. } else {
  100. registerRef(ancestor)
  101. }
  102. ancestor = ancestor.parent
  103. }
  104. }
  105. // destroy old node
  106. if (isDef(parentElm)) {
  107. removeVnodes([oldVnode], 0, 0)
  108. } else if (isDef(oldVnode.tag)) {
  109. invokeDestroyHook(oldVnode)
  110. }
  111. }
  112. }
  113. invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  114. return vnode.elm
  115. }

内部定义了一系列的辅助方法,最终返回一个patch方法,这个方法就赋值给了vm.update函数里调用的vm.patch

例子

  1. var app = new Vue({
  2. el: '#app',
  3. render: function (createElement) {
  4. return createElement('div', {
  5. attrs: {
  6. id: 'app'
  7. },
  8. }, this.message)
  9. },
  10. data: {
  11. message: 'Hello Vue!'
  12. }
  13. })

然后在vm.update的方法里调用patch方法

  1. // initial render
  2. vm.$el = vm.__patch__(
  3. vm.$el, // 本例子中 对应的是id为app的DOM对象
  4. vnode, // 本例子中 对应的是调用render函数的返回值
  5. hydrating, // 非服务端渲染情况下为false
  6. false /* removeOnly */
  7. )

vm.$el的赋值是在之前mountComponent函数中做的
再看createPatchFunction方法

  1. const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
  2. export function createPatchFunction (backend) {
  3. let i, j
  4. const cbs = {}
  5. const { modules, nodeOps } = backend
  6. for (i = 0; i < hooks.length; ++i) {
  7. cbs[hooks[i]] = []
  8. for (j = 0; j < modules.length; ++j) {
  9. if (isDef(modules[j][hooks[i]])) {
  10. cbs[hooks[i]].push(modules[j][hooks[i]])
  11. }
  12. }
  13. }
  14. // ...
  15. return function patch (
  16. oldVnode, // 旧的节点,也可以不存在或者是一个DOM对象
  17. vnode, // 执行_render后返回的VNode节点
  18. hydrating, // 是否是服务端渲染
  19. removeOnly // 给transition-group用的
  20. ) {
  21. if (isUndef(vnode)) {
  22. if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
  23. return
  24. }
  25. let isInitialPatch = false
  26. const insertedVnodeQueue = []
  27. if (isUndef(oldVnode)) {
  28. // empty mount (likely as component), create new root element
  29. isInitialPatch = true
  30. createElm(vnode, insertedVnodeQueue)
  31. } else {
  32. // oldVnode实际上是一个DOM Container
  33. const isRealElement = isDef(oldVnode.nodeType) // true
  34. if (!isRealElement && sameVnode(oldVnode, vnode)) {
  35. // patch existing root node
  36. patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  37. } else {
  38. if (isRealElement) {
  39. // mounting to a real element
  40. // check if this is server-rendered content and if we can perform
  41. // a successful hydration.
  42. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
  43. oldVnode.removeAttribute(SSR_ATTR)
  44. hydrating = true
  45. }
  46. if (isTrue(hydrating)) {
  47. if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
  48. invokeInsertHook(vnode, insertedVnodeQueue, true)
  49. return oldVnode
  50. } else if (process.env.NODE_ENV !== 'production') {
  51. warn(
  52. 'The client-side rendered virtual DOM tree is not matching ' +
  53. 'server-rendered content. This is likely caused by incorrect ' +
  54. 'HTML markup, for example nesting block-level elements inside ' +
  55. '<p>, or missing <tbody>. Bailing hydration and performing ' +
  56. 'full client-side render.'
  57. )
  58. }
  59. }
  60. // either not server-rendered, or hydration failed.
  61. // create an empty node and replace it
  62. // emptyNodeAt方法把oldVnode转换成VNode对象
  63. oldVnode = emptyNodeAt(oldVnode)
  64. }
  65. // replacing existing element
  66. const oldElm = oldVnode.elm
  67. const parentElm = nodeOps.parentNode(oldElm)
  68. // create new node
  69. // createElm作用是通过虚拟节点创建真实的DOM并插入到它的父节点中
  70. createElm(
  71. vnode,
  72. insertedVnodeQueue,
  73. // extremely rare edge case: do not insert if old element is in a
  74. // leaving transition. Only happens when combining transition +
  75. // keep-alive + HOCs. (#4590)
  76. oldElm._leaveCb ? null : parentElm,
  77. nodeOps.nextSibling(oldElm)
  78. )
  79. // update parent placeholder node element, recursively
  80. if (isDef(vnode.parent)) {
  81. let ancestor = vnode.parent
  82. const patchable = isPatchable(vnode)
  83. while (ancestor) {
  84. for (let i = 0; i < cbs.destroy.length; ++i) {
  85. cbs.destroy[i](ancestor)
  86. }
  87. ancestor.elm = vnode.elm
  88. if (patchable) {
  89. for (let i = 0; i < cbs.create.length; ++i) {
  90. cbs.create[i](emptyNode, ancestor)
  91. }
  92. // #6513
  93. // invoke insert hooks that may have been merged by create hooks.
  94. // e.g. for directives that uses the "inserted" hook.
  95. const insert = ancestor.data.hook.insert
  96. if (insert.merged) {
  97. // start at index 1 to avoid re-invoking component mounted hook
  98. for (let i = 1; i < insert.fns.length; i++) {
  99. insert.fns[i]()
  100. }
  101. }
  102. } else {
  103. registerRef(ancestor)
  104. }
  105. ancestor = ancestor.parent
  106. }
  107. }
  108. // destroy old node
  109. if (isDef(parentElm)) {
  110. removeVnodes([oldVnode], 0, 0)
  111. } else if (isDef(oldVnode.tag)) {
  112. invokeDestroyHook(oldVnode)
  113. }
  114. }
  115. }
  116. invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  117. return vnode.elm
  118. }

createElm

  1. function createElm (
  2. vnode,
  3. insertedVnodeQueue,
  4. parentElm,
  5. refElm,
  6. nested,
  7. ownerArray,
  8. index
  9. ) {
  10. if (isDef(vnode.elm) && isDef(ownerArray)) {
  11. // This vnode was used in a previous render!
  12. // now it's used as a new node, overwriting its elm would cause
  13. // potential patch errors down the road when it's used as an insertion
  14. // reference node. Instead, we clone the node on-demand before creating
  15. // associated DOM element for it.
  16. vnode = ownerArray[index] = cloneVNode(vnode)
  17. }
  18. vnode.isRootInsert = !nested // for transition enter check
  19. // createComponent方法目的是尝试创建子组件,该例子下返回值是false
  20. if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  21. return
  22. }
  23. const data = vnode.data
  24. const children = vnode.children
  25. const tag = vnode.tag
  26. // 判断vnode是否包含tag
  27. if (isDef(tag)) {
  28. // 对tag的合法性在非生产环境下做校验,看是否是一个合法标签
  29. if (process.env.NODE_ENV !== 'production') {
  30. if (data && data.pre) {
  31. creatingElmInVPre++
  32. }
  33. // 不是合法标签
  34. if (isUnknownElement(vnode, creatingElmInVPre)) {
  35. warn(
  36. 'Unknown custom element: <' + tag + '> - did you ' +
  37. 'register the component correctly? For recursive components, ' +
  38. 'make sure to provide the "name" option.',
  39. vnode.context
  40. )
  41. }
  42. }
  43. // 调用平台的DOM的操作去创建一个占位符元素
  44. vnode.elm = vnode.ns
  45. ? nodeOps.createElementNS(vnode.ns, tag)
  46. : nodeOps.createElement(tag, vnode)
  47. setScope(vnode)
  48. /* istanbul ignore if */
  49. if (__WEEX__) {
  50. // in Weex, the default insertion order is parent-first.
  51. // List items can be optimized to use children-first insertion
  52. // with append="tree".
  53. const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
  54. if (!appendAsTree) {
  55. if (isDef(data)) {
  56. invokeCreateHooks(vnode, insertedVnodeQueue)
  57. }
  58. insert(parentElm, vnode.elm, refElm)
  59. }
  60. createChildren(vnode, children, insertedVnodeQueue)
  61. if (appendAsTree) {
  62. if (isDef(data)) {
  63. invokeCreateHooks(vnode, insertedVnodeQueue)
  64. }
  65. insert(parentElm, vnode.elm, refElm)
  66. }
  67. } else {
  68. // 创建子元素
  69. createChildren(vnode, children, insertedVnodeQueue)
  70. if (isDef(data)) {
  71. // 执行所有的create的钩子并把vnode push到insertedVnodeQueue
  72. invokeCreateHooks(vnode, insertedVnodeQueue)
  73. }
  74. // 把DOM插入到父节点中
  75. // 因为是递归调用,子元素会优先调用insert,所以整个vnode树节点的插入顺序是先子后父
  76. insert(parentElm, vnode.elm, refElm)
  77. }
  78. if (process.env.NODE_ENV !== 'production' && data && data.pre) {
  79. creatingElmInVPre--
  80. }
  81. } else if (isTrue(vnode.isComment)) { // vnode节点不包含tag,是一个注释,直接插入到父元素中
  82. vnode.elm = nodeOps.createComment(vnode.text)
  83. insert(parentElm, vnode.elm, refElm)
  84. } else { // vnode节点不包含tag,是一个纯文本节点,直接插入到父元素中
  85. vnode.elm = nodeOps.createTextNode(vnode.text)
  86. insert(parentElm, vnode.elm, refElm)
  87. }
  88. }

createChildren

遍历子虚拟节点,递归调用createElm方法 —- 深度优先的遍历算法
遍历过程中会把vnode.elm作为父容器的DOM节点占位符传入

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

invokeCreateHooks

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

insert

调用一些 nodeOps 把子节点插入到父节点中

  1. function insert (parent, elm, ref) {
  2. if (isDef(parent)) {
  3. if (isDef(ref)) {
  4. if (nodeOps.parentNode(ref) === parent) {
  5. nodeOps.insertBefore(parent, elm, ref)
  6. }
  7. } else {
  8. nodeOps.appendChild(parent, elm)
  9. }
  10. }
  11. }

insertBefore和appendChild

定义在 src/platforms/web/runtime/node-ops.js 中

  1. export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  2. parentNode.insertBefore(newNode, referenceNode)
  3. }
  4. export function appendChild (node: Node, child: Node) {
  5. node.appendChild(child)
  6. }

其实就是调用原生DOM的API进行DOM操作

为何 Vue.js 源码绕了这么一大圈,把vm.patch相关代码分散到各个目录

patch 是平台相关的,在 Web 和 Weex 环境,它们把虚拟 DOM 映射到 “平台 DOM” 的方法是不同的,并且对 “DOM” 包括的属性模块创建和更新也不尽相同。因此每个平台都有各自的 nodeOps 和 modules,它们的代码需要托管在 src/platforms 这个大目录下
不同平台的 patch 的主要逻辑部分是相同的,所以这部分公共的部分托管在 core 这个大目录下。差异化部分只需要通过参数来区别,这里用到了一个函数柯里化的技巧,通过 createPatchFunction 把差异化参数提前固化,这样不用每次调用 patch 的时候都传递 nodeOps 和 modules 了
nodeOps 表示对 “平台 DOM” 的一些操作方法,modules 表示平台的一些模块,它们会在整个 patch 过程的不同阶段执行相应的钩子函数

流程

从初始化 Vue 到最终渲染的整个过程
image.png