Vue源码分析(二) : Vue实例挂载

author: @TiffanysBear

实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-with-compiler.js & src/platforms/web/runtime/index.js 等文件中都有对Vue.prototype.$mount的定义:

  1. // vue/platforms/web/entry-runtime-with-compiler.js
  2. Vue.prototype.$mount = function (
  3. el?: string | Element,
  4. hydrating?: boolean
  5. ): Component {
  6. el = el && query(el)
  7. /* istanbul ignore if */
  8. if (el === document.body || el === document.documentElement) {
  9. process.env.NODE_ENV !== 'production' && warn(
  10. `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
  11. )
  12. return this
  13. }
  14. const options = this.$options
  15. // resolve template/el and convert to render function
  16. if (!options.render) {
  17. let template = options.template
  18. if (template) {
  19. if (typeof template === 'string') {
  20. if (template.charAt(0) === '#') {
  21. template = idToTemplate(template)
  22. /* istanbul ignore if */
  23. if (process.env.NODE_ENV !== 'production' && !template) {
  24. warn(
  25. `Template element not found or is empty: ${options.template}`,
  26. this
  27. )
  28. }
  29. }
  30. } else if (template.nodeType) {
  31. template = template.innerHTML
  32. } else {
  33. if (process.env.NODE_ENV !== 'production') {
  34. warn('invalid template option:' + template, this)
  35. }
  36. return this
  37. }
  38. } else if (el) {
  39. template = getOuterHTML(el)
  40. }
  41. if (template) {
  42. /* istanbul ignore if */
  43. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  44. mark('compile')
  45. }
  46. const { render, staticRenderFns } = compileToFunctions(template, {
  47. outputSourceRange: process.env.NODE_ENV !== 'production',
  48. shouldDecodeNewlines,
  49. shouldDecodeNewlinesForHref,
  50. delimiters: options.delimiters,
  51. comments: options.comments
  52. }, this)
  53. options.render = render
  54. options.staticRenderFns = staticRenderFns
  55. /* istanbul ignore if */
  56. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  57. mark('compile end')
  58. measure(`vue ${this._name} compile`, 'compile', 'compile end')
  59. }
  60. }
  61. }
  62. return mount.call(this, el, hydrating)
  63. }

$mount方法进来会先进行缓存,之后再进行覆盖重写,再重写的方法里面会调用之前缓存的mount方法,这种做法是因为,多个平台platform的mount方法不同,在入口处进行重写,使后续的多入口能够复用公用定义的mount方法(原先原型上的 $mount 方法在 src/platform/web/runtime/index.js 中定义)。

在$mount方法中,会先判断options中 el 是否存在,再判断 render (有template存在的条件下也需要有render函数),之后再是再判断template,会对template做一定的校验,最后使用 compileToFunctions 将template转化为renderstaticRenderFns.

compileToFunctions编译过程就放在下面文章中再详细解释。

mountComponent方法定义在 src/core/instance/lifecycle.js中,

  1. // src/core/instance/lifecycle.js
  2. export function mountComponent (
  3. vm: Component,
  4. el: ?Element,
  5. hydrating?: boolean
  6. ): Component {
  7. vm.$el = el
  8. if (!vm.$options.render) {
  9. vm.$options.render = createEmptyVNode
  10. if (process.env.NODE_ENV !== 'production') {
  11. /* istanbul ignore if */
  12. if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
  13. vm.$options.el || el) {
  14. warn(
  15. 'You are using the runtime-only build of Vue where the template ' +
  16. 'compiler is not available. Either pre-compile the templates into ' +
  17. 'render functions, or use the compiler-included build.',
  18. vm
  19. )
  20. } else {
  21. warn(
  22. 'Failed to mount component: template or render function not defined.',
  23. vm
  24. )
  25. }
  26. }
  27. }
  28. callHook(vm, 'beforeMount')
  29. let updateComponent
  30. /* istanbul ignore if */
  31. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  32. updateComponent = () => {
  33. const name = vm._name
  34. const id = vm._uid
  35. const startTag = `vue-perf-start:${id}`
  36. const endTag = `vue-perf-end:${id}`
  37. mark(startTag)
  38. const vnode = vm._render()
  39. mark(endTag)
  40. measure(`vue ${name} render`, startTag, endTag)
  41. mark(startTag)
  42. vm._update(vnode, hydrating)
  43. mark(endTag)
  44. measure(`vue ${name} patch`, startTag, endTag)
  45. }
  46. } else {
  47. updateComponent = () => {
  48. vm._update(vm._render(), hydrating)
  49. }
  50. }
  51. // we set this to vm._watcher inside the watcher's constructor
  52. // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  53. // component's mounted hook), which relies on vm._watcher being already defined
  54. new Watcher(vm, updateComponent, noop, {
  55. before () {
  56. if (vm._isMounted && !vm._isDestroyed) {
  57. callHook(vm, 'beforeUpdate')
  58. }
  59. }
  60. }, true /* isRenderWatcher */)
  61. hydrating = false
  62. // manually mounted instance, call mounted on self
  63. // mounted is called for render-created child components in its inserted hook
  64. if (vm.$vnode == null) {
  65. vm._isMounted = true
  66. callHook(vm, 'mounted')
  67. }
  68. return vm
  69. }

从上面的代码可以看到,mountComponent 核心就是先实例化一个渲染Watcher(字段isRenderWatcher),在它的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。

Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数,这块儿我们会在之后的章节中介绍。

函数最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。

因此接下来分析的重点在于:vm._updatem._render

_render

Vue的_render是实例的一个私有方法,定义在 src/core/instance/render.js文件中,返回一个虚拟节点vnode。

  1. // src/core/instance/render.js
  2. Vue.prototype._render = function (): VNode {
  3. const vm: Component = this
  4. const { render, _parentVnode } = vm.$options
  5. if (_parentVnode) {
  6. vm.$scopedSlots = normalizeScopedSlots(
  7. _parentVnode.data.scopedSlots,
  8. vm.$slots,
  9. vm.$scopedSlots
  10. )
  11. }
  12. // set parent vnode. this allows render functions to have access
  13. // to the data on the placeholder node.
  14. vm.$vnode = _parentVnode
  15. // render self
  16. let vnode
  17. try {
  18. // There's no need to maintain a stack because all render fns are called
  19. // separately from one another. Nested component's render fns are called
  20. // when parent component is patched.
  21. currentRenderingInstance = vm
  22. vnode = render.call(vm._renderProxy, vm.$createElement)
  23. } catch (e) {
  24. handleError(e, vm, `render`)
  25. // return error render result,
  26. // or previous vnode to prevent render error causing blank component
  27. /* istanbul ignore else */
  28. if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
  29. try {
  30. vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
  31. } catch (e) {
  32. handleError(e, vm, `renderError`)
  33. vnode = vm._vnode
  34. }
  35. } else {
  36. vnode = vm._vnode
  37. }
  38. } finally {
  39. currentRenderingInstance = null
  40. }
  41. // if the returned array contains only a single node, allow it
  42. if (Array.isArray(vnode) && vnode.length === 1) {
  43. vnode = vnode[0]
  44. }
  45. // return empty vnode in case the render function errored out
  46. if (!(vnode instanceof VNode)) {
  47. if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
  48. warn(
  49. 'Multiple root nodes returned from render function. Render function ' +
  50. 'should return a single root node.',
  51. vm
  52. )
  53. }
  54. vnode = createEmptyVNode()
  55. }
  56. // set parent
  57. vnode.parent = _parentVnode
  58. return vnode
  59. }

这段函数方法的重点在于render方法的调用,第一种是分为手写的render函数,这种并不常用,比较常用的是template模板,在之前的 mounted 方法的实现时,会将template编译为一个render函数。

其中vm._renderProxy是定义在/src/core/instance/proxy.js文件中,判断如果支持Proxy,如果不支持,返回的是vm,支持的话返回用Proxy代理的vm。

  1. // src/core/instance/proxy.js
  2. initProxy = function initProxy (vm) {
  3. if (hasProxy) {
  4. // determine which proxy handler to use
  5. const options = vm.$options
  6. const handlers = options.render && options.render._withStripped
  7. ? getHandler
  8. : hasHandler
  9. vm._renderProxy = new Proxy(vm, handlers)
  10. } else {
  11. vm._renderProxy = vm
  12. }
  13. }

其中vm.$createElement也就是在 src/core/instance/render.js文件中:

  1. // src/core/instance/render.js
  2. import { createElement } from '../vdom/create-element'
  3. // bind the createElement fn to this instance
  4. // so that we get proper render context inside it.
  5. // args order: tag, data, children, normalizationType, alwaysNormalize
  6. // internal version is used by render functions compiled from templates
  7. vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  8. // normalization is always applied for the public version, used in
  9. // user-written render functions.
  10. vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

可以从注释中看出:

vm._c是template模板编译为render function时使用的;

vm.$createElement是用户手写的render function时使用;

这两个函数的支持的参数相同,并且内部都调用了 vdom/create-elementcreateElement方法。

Virtual DOM

在讲_update方法之前,了解下Virtual DOM到底是什么?

Virtual DOM也就是虚拟DOM,是真实数据和页面DOM元素之前的缓冲;数据一变化,并不是立马更新所有视图,而是先更新虚拟DOM,再将虚拟DOM和真实DOM进行对比diff,发生变化的部分再更新到真实DOM中,未发生变化的部分,则不进行更新。

下面是Vue对于VNode的定义:

  1. // vue/src/core/vdom/vnode.js
  2. export default class VNode {
  3. tag: string | void;
  4. data: VNodeData | void;
  5. children: ?Array<VNode>;
  6. text: string | void;
  7. elm: Node | void;
  8. ns: string | void;
  9. context: Component | void; // rendered in this component's scope
  10. key: string | number | void;
  11. componentOptions: VNodeComponentOptions | void;
  12. componentInstance: Component | void; // component instance
  13. parent: VNode | void; // component placeholder node
  14. // strictly internal
  15. raw: boolean; // contains raw HTML? (server only)
  16. isStatic: boolean; // hoisted static node
  17. isRootInsert: boolean; // necessary for enter transition check
  18. isComment: boolean; // empty comment placeholder?
  19. isCloned: boolean; // is a cloned node?
  20. isOnce: boolean; // is a v-once node?
  21. asyncFactory: Function | void; // async component factory function
  22. asyncMeta: Object | void;
  23. isAsyncPlaceholder: boolean;
  24. ssrContext: Object | void;
  25. fnContext: Component | void; // real context vm for functional nodes
  26. fnOptions: ?ComponentOptions; // for SSR caching
  27. devtoolsMeta: ?Object; // used to store functional render context for devtools
  28. fnScopeId: ?string; // functional scope id support
  29. constructor (
  30. tag?: string,
  31. data?: VNodeData,
  32. children?: ?Array<VNode>,
  33. text?: string,
  34. elm?: Node,
  35. context?: Component,
  36. componentOptions?: VNodeComponentOptions,
  37. asyncFactory?: Function
  38. ) {
  39. this.tag = tag
  40. this.data = data
  41. this.children = children
  42. this.text = text
  43. this.elm = elm
  44. this.ns = undefined
  45. this.context = context
  46. this.fnContext = undefined
  47. this.fnOptions = undefined
  48. this.fnScopeId = undefined
  49. this.key = data && data.key
  50. this.componentOptions = componentOptions
  51. this.componentInstance = undefined
  52. this.parent = undefined
  53. this.raw = false
  54. this.isStatic = false
  55. this.isRootInsert = true
  56. this.isComment = false
  57. this.isCloned = false
  58. this.isOnce = false
  59. this.asyncFactory = asyncFactory
  60. this.asyncMeta = undefined
  61. this.isAsyncPlaceholder = false
  62. }
  63. // DEPRECATED: alias for componentInstance for backwards compat.
  64. /* istanbul ignore next */
  65. get child (): Component | void {
  66. return this.componentInstance
  67. }
  68. }

实际上 Vue.js 中 Virtual DOM 是借鉴了一个开源库 snabbdom 的实现,如果对Virtual DOM感兴趣的话,可以参考virtual-dom,正如其介绍,

A JavaScript DOM model supporting element creation, diff computation and patch operations for efficient re-rendering

VNode是对真实DOM的抽象描述,主要是由几个关键属性、标签名等数据组成,并不是很复杂,主要复杂的对VNode的create、diff、patch等过程。

createElement是怎么实现的

方法入口

Vue.js通过文件 src/core/vdom/create-element.js 来创建VNode元素:

  1. // src/core/vdom/create-element.js
  2. // wrapper function for providing a more flexible interface
  3. // without getting yelled at by flow
  4. export function createElement (
  5. context: Component,
  6. tag: any,
  7. data: any,
  8. children: any,
  9. normalizationType: any,
  10. alwaysNormalize: boolean
  11. ): VNode | Array<VNode> {
  12. if (Array.isArray(data) || isPrimitive(data)) {
  13. normalizationType = children
  14. children = data
  15. data = undefined
  16. }
  17. if (isTrue(alwaysNormalize)) {
  18. normalizationType = ALWAYS_NORMALIZE
  19. }
  20. return _createElement(context, tag, data, children, normalizationType)
  21. }
  22. export function _createElement (
  23. context: Component,
  24. tag?: string | Class<Component> | Function | Object,
  25. data?: VNodeData,
  26. children?: any,
  27. normalizationType?: number
  28. ): VNode | Array<VNode> {
  29. if (isDef(data) && isDef((data: any).__ob__)) {
  30. process.env.NODE_ENV !== 'production' && warn(
  31. `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
  32. 'Always create fresh vnode data objects in each render!',
  33. context
  34. )
  35. return createEmptyVNode()
  36. }
  37. // object syntax in v-bind
  38. if (isDef(data) && isDef(data.is)) {
  39. tag = data.is
  40. }
  41. if (!tag) {
  42. // in case of component :is set to falsy value
  43. return createEmptyVNode()
  44. }
  45. // warn against non-primitive key
  46. if (process.env.NODE_ENV !== 'production' &&
  47. isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  48. ) {
  49. if (!__WEEX__ || !('@binding' in data.key)) {
  50. warn(
  51. 'Avoid using non-primitive value as key, ' +
  52. 'use string/number value instead.',
  53. context
  54. )
  55. }
  56. }
  57. // support single function children as default scoped slot
  58. if (Array.isArray(children) &&
  59. typeof children[0] === 'function'
  60. ) {
  61. data = data || {}
  62. data.scopedSlots = { default: children[0] }
  63. children.length = 0
  64. }
  65. if (normalizationType === ALWAYS_NORMALIZE) {
  66. children = normalizeChildren(children)
  67. } else if (normalizationType === SIMPLE_NORMALIZE) {
  68. children = simpleNormalizeChildren(children)
  69. }
  70. let vnode, ns
  71. if (typeof tag === 'string') {
  72. let Ctor
  73. ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  74. if (config.isReservedTag(tag)) {
  75. // platform built-in elements
  76. if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
  77. warn(
  78. `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
  79. context
  80. )
  81. }
  82. vnode = new VNode(
  83. config.parsePlatformTagName(tag), data, children,
  84. undefined, undefined, context
  85. )
  86. } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
  87. // component
  88. vnode = createComponent(Ctor, data, context, children, tag)
  89. } else {
  90. // unknown or unlisted namespaced elements
  91. // check at runtime because it may get assigned a namespace when its
  92. // parent normalizes children
  93. vnode = new VNode(
  94. tag, data, children,
  95. undefined, undefined, context
  96. )
  97. }
  98. } else {
  99. // direct component options / constructor
  100. vnode = createComponent(tag, data, context, children)
  101. }
  102. if (Array.isArray(vnode)) {
  103. return vnode
  104. } else if (isDef(vnode)) {
  105. if (isDef(ns)) applyNS(vnode, ns)
  106. if (isDef(data)) registerDeepBindings(data)
  107. return vnode
  108. } else {
  109. return createEmptyVNode()
  110. }
  111. }

重点是对于 simpleNormalizeChildrennormalizeChildren 的处理,基本的操作就是将树状结构的children数组打平成一维数组。

normalizeArrayChildren 也就是将createElement的第三个参数,即将children不断遍历打平,不断往res里面push数据,只要是数据Array类型就不断遍历,直到是基础类型TextNode,再进行createTextVNode进行创建。

还有对于组件Component的创建,此处先按下不讲,下文再讲。

  1. // The template compiler attempts to minimize the need for normalization by
  2. // statically analyzing the template at compile time.
  3. //
  4. // For plain HTML markup, normalization can be completely skipped because the
  5. // generated render function is guaranteed to return Array<VNode>. There are
  6. // two cases where extra normalization is needed:
  7. // 1. When the children contains components - because a functional component
  8. // may return an Array instead of a single root. In this case, just a simple
  9. // normalization is needed - if any child is an Array, we flatten the whole
  10. // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
  11. // because functional components already normalize their own children.
  12. export function simpleNormalizeChildren (children: any) {
  13. for (let i = 0; i < children.length; i++) {
  14. if (Array.isArray(children[i])) {
  15. return Array.prototype.concat.apply([], children)
  16. }
  17. }
  18. return children
  19. }
  20. // 2. When the children contains constructs that always generated nested Arrays,
  21. // e.g. <template>, <slot>, v-for, or when the children is provided by user
  22. // with hand-written render functions / JSX. In such cases a full normalization
  23. // is needed to cater to all possible types of children values.
  24. export function normalizeChildren (children: any): ?Array<VNode> {
  25. return isPrimitive(children)
  26. ? [createTextVNode(children)]
  27. : Array.isArray(children)
  28. ? normalizeArrayChildren(children)
  29. : undefined
  30. }
  31. function isTextNode (node): boolean {
  32. return isDef(node) && isDef(node.text) && isFalse(node.isComment)
  33. }
  34. function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  35. const res = []
  36. let i, c, lastIndex, last
  37. for (i = 0; i < children.length; i++) {
  38. c = children[i]
  39. if (isUndef(c) || typeof c === 'boolean') continue
  40. lastIndex = res.length - 1
  41. last = res[lastIndex]
  42. // nested
  43. if (Array.isArray(c)) {
  44. if (c.length > 0) {
  45. c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
  46. // merge adjacent text nodes
  47. if (isTextNode(c[0]) && isTextNode(last)) {
  48. res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
  49. c.shift()
  50. }
  51. res.push.apply(res, c)
  52. }
  53. } else if (isPrimitive(c)) {
  54. if (isTextNode(last)) {
  55. // merge adjacent text nodes
  56. // this is necessary for SSR hydration because text nodes are
  57. // essentially merged when rendered to HTML strings
  58. res[lastIndex] = createTextVNode(last.text + c)
  59. } else if (c !== '') {
  60. // convert primitive to vnode
  61. res.push(createTextVNode(c))
  62. }
  63. } else {
  64. if (isTextNode(c) && isTextNode(last)) {
  65. // merge adjacent text nodes
  66. res[lastIndex] = createTextVNode(last.text + c.text)
  67. } else {
  68. // default key for nested array children (likely generated by v-for)
  69. if (isTrue(children._isVList) &&
  70. isDef(c.tag) &&
  71. isUndef(c.key) &&
  72. isDef(nestedIndex)) {
  73. c.key = `__vlist${nestedIndex}_${i}__`
  74. }
  75. res.push(c)
  76. }
  77. }
  78. }
  79. return res
  80. }

_update

_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. }

可以看出,主要是对__patch__方法的调用,分别是首次渲染和数据更新的时候会调用;这次先是分析首次调用时,数据更新的部分会在之后响应式原理的时候再进行分析。

_update的主要目的就是将虚拟DOM渲染生成真实的DOM元素。

而__patch__方法在不同平台的调用是不同的,在浏览器中时,是patch方法,而在非浏览器环境中,比如node后端环境时,是一个noop空函数,主要也是因为只要在浏览器环境时才会有DOM元素。

文件:src/platforms/web/runtime/index.js

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

最终 patch 调用的是 src/core/vdom/patch.js 中的 createPatchFunction ,其中有个采用闭包来判断环境的技巧,因为patch方法可能是会在 weex 或者 浏览器端 上调用,如果每次调用都 if else 判断一遍,浪费性能不说,还增加了冗余的判断。于是,它采用了通过闭包判断再返回函数覆盖 patch 的方法,这样环境差异就只会判断一次,进而再次执行的时候,就不会再次判断环境。

  1. export function createPatchFunction (backend) {
  2. // 环境判断
  3. // ...
  4. return function patch (oldVnode, vnode, hydrating, removeOnly) {
  5. // ...
  6. }
  7. }

同时,createPatchFunction 内部定义了一系列的辅助方法。

所以从例子来分析:

  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__(vm.$el, vnode, hydrating, false /* removeOnly */)

结合例子,在首次渲染时,所以在执行 patch 函数的时候,传入的 vm.$el 对应的是例子中 id 为 app 的 DOM 对象,这个也就是 <div id="app">, vm.$el 的赋值是在之前 mountComponent 函数做的,vnode 对应的是调用 render 函数的返回值,hydrating 在非服务端渲染情况下为 false,removeOnly 为 false。

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

由于我们传入的 oldVnode 实际上是一个 DOM container,所以 isRealElement 为 true,接下来又通过 emptyNodeAt 方法把 oldVnode 转换成 VNode 对象,然后再调用 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. if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  20. return
  21. }
  22. const data = vnode.data
  23. const children = vnode.children
  24. const tag = vnode.tag
  25. if (isDef(tag)) {
  26. if (process.env.NODE_ENV !== 'production') {
  27. if (data && data.pre) {
  28. creatingElmInVPre++
  29. }
  30. if (isUnknownElement(vnode, creatingElmInVPre)) {
  31. warn(
  32. 'Unknown custom element: <' + tag + '> - did you ' +
  33. 'register the component correctly? For recursive components, ' +
  34. 'make sure to provide the "name" option.',
  35. vnode.context
  36. )
  37. }
  38. }
  39. vnode.elm = vnode.ns
  40. ? nodeOps.createElementNS(vnode.ns, tag)
  41. : nodeOps.createElement(tag, vnode)
  42. setScope(vnode)
  43. /* istanbul ignore if */
  44. if (__WEEX__) {
  45. // in Weex, the default insertion order is parent-first.
  46. // List items can be optimized to use children-first insertion
  47. // with append="tree".
  48. const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
  49. if (!appendAsTree) {
  50. if (isDef(data)) {
  51. invokeCreateHooks(vnode, insertedVnodeQueue)
  52. }
  53. insert(parentElm, vnode.elm, refElm)
  54. }
  55. createChildren(vnode, children, insertedVnodeQueue)
  56. if (appendAsTree) {
  57. if (isDef(data)) {
  58. invokeCreateHooks(vnode, insertedVnodeQueue)
  59. }
  60. insert(parentElm, vnode.elm, refElm)
  61. }
  62. } else {
  63. createChildren(vnode, children, insertedVnodeQueue)
  64. if (isDef(data)) {
  65. invokeCreateHooks(vnode, insertedVnodeQueue)
  66. }
  67. insert(parentElm, vnode.elm, refElm)
  68. }
  69. if (process.env.NODE_ENV !== 'production' && data && data.pre) {
  70. creatingElmInVPre--
  71. }
  72. } else if (isTrue(vnode.isComment)) {
  73. vnode.elm = nodeOps.createComment(vnode.text)
  74. insert(parentElm, vnode.elm, refElm)
  75. } else {
  76. vnode.elm = nodeOps.createTextNode(vnode.text)
  77. insert(parentElm, vnode.elm, refElm)
  78. }
  79. }

createElm 的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中。createComponent 方法目的是尝试创建子组件,接下来判断 vnode 是否包含 tag,如果包含,先简单对 tag 的合法性在非生产环境下做校验,看是否是一个合法标签;然后再去调用平台 DOM 的操作去创建一个占位符元素。

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

接下来是通过 createChildren 创建子元素:

  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. }

createChildren 的逻辑很简单,实际上是遍历子虚拟节点,递归调用 createElm,这是一种常用的深度优先的遍历算法,这里要注意的一点是在遍历过程中会把 vnode.elm 作为父容器的 DOM 节点占位符传入。

最后调用 insert 方法把 DOM 插入到父节点中,因为是递归调用,子元素会优先调用 insert,所以整个 vnode 树节点的插入顺序是先子后父。来看一下 insert 方法,它的定义在 src/core/vdom/patch.js 上。

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

insert 逻辑很简单,调用一些 nodeOps 把子节点插入到父节点中,这些辅助方法定义在 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 操作。

createElm 过程中,如果 vnode 节点不包含 tag,则它有可能是一个注释或者纯文本节点,可以直接插入到父元素中。在我们这个例子中,最内层就是一个文本 vnode,它的 text 值取的就是之前的 this.message 的值 Hello Vue!。

再回到 patch 方法,首次渲染我们调用了 createElm 方法,这里传入的 parentElm 是 oldVnode.elm 的父元素,在我们的例子是 id 为 #app div 的父元素,也就是 Body;实际上整个过程就是递归创建了一个完整的 DOM 树并插入到 Body 上。

最后,我们根据之前递归 createElm 生成的 vnode 插入顺序队列,执行相关的 insert 钩子函数。

总结

这里只是分析了最简单的场景,在实际的项目中,会比这些复杂的很多。

Vue源码分析(二) : Vue实例挂载的更多相关文章

  1. Vue源码学习二 ———— Vue原型对象包装

    Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ...

  2. 手牵手,从零学习Vue源码 系列二(变化侦测篇)

    系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 陆续更新中... 预计八月中旬更新完毕. 1 概述 Vue最大的特点之一就是数据驱动视 ...

  3. vue 快速入门 系列 —— 侦测数据的变化 - [vue 源码分析]

    其他章节请看: vue 快速入门 系列 侦测数据的变化 - [vue 源码分析] 本文将 vue 中与数据侦测相关的源码摘了出来,配合上文(侦测数据的变化 - [基本实现]) 一起来分析一下 vue ...

  4. [Vue源码分析] v-model实现原理

    最近小组有个关于vue源码分析的分享会,提前准备一下… 前言:我们都知道使用v-model可以实现数据的双向绑定,及实现数据的变化驱动dom的更新,dom的更新影响数据的变化.那么v-model是怎么 ...

  5. Vue源码分析(一) : new Vue() 做了什么

    Vue源码分析(一) : new Vue() 做了什么 author: @TiffanysBear 在了解new Vue做了什么之前,我们先对Vue源码做一些基础的了解,如果你已经对基础的源码目录设计 ...

  6. 前端Vue 源码分析-逻辑层

    Vue 源码分析-逻辑层 预期的效果: 监听input的输入,input在输入的时候,会触发 watch与computed函数,并且会更新原始的input的数值.所以直接跟input相关的处理就有3处 ...

  7. Vue源码学习1——Vue构造函数

    Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ...

  8. Fresco 源码分析(二) Fresco客户端与服务端交互(1) 解决遗留的Q1问题

    4.2 Fresco客户端与服务端的交互(一) 解决Q1问题 从这篇博客开始,我们开始讨论客户端与服务端是如何交互的,这个交互的入口,我们从Q1问题入手(博客按照这样的问题入手,是因为当时我也是从这里 ...

  9. 框架-springmvc源码分析(二)

    框架-springmvc源码分析(二) 参考: http://www.cnblogs.com/leftthen/p/5207787.html http://www.cnblogs.com/leftth ...

随机推荐

  1. 改变说明文档显示位置wrap

    装饰器会改变文档的显示位置 例子1:使用wrap前,输出内函数中的说明文档 def check(fun): """检查权限的装饰器""" d ...

  2. SQL SERVER-Login搬迁脚本

    USE master GO IF OBJECT_ID ('sp_hexadecimal') IS NOT NULL DROP PROCEDURE sp_hexadecimal GO CREATE PR ...

  3. CentOS7下编译安装Python3.7.x【亲测有效】

    所有操作都在root用户下操作 下载安装包 编译安装 建立软链接 验证 安装: 更新yum: yum update 安装Python依赖: yum install openssl-devel bzip ...

  4. 某个Spring Cloud分布式架构

  5. iptables详解(6):iptables扩展模块之 state 扩展

    为了防止恶意攻击主动连接到你的主机 我们需要通过iptables的扩展模块判断报文是为了回应我们之前发出的报文还是主动向我们发送的报文 state模块可以让iptables实现 连接追踪机制 NEW表 ...

  6. Go语言中Goroutine的设置

    一. 通过runtime包进行多核设置 1.NumCPU()获取当前系统的cpu核数 2.GOMAXPROCS设置当前程序运行时占用的cpu核数 版本1.6之前默认是使用1个核,而之后是全部使用. 好 ...

  7. 运输层7——TCP的流量控制和拥塞控制

    目录 1. TCP的流量控制 2. TCP的拥塞控制 写在前面:本文章是针对<计算机网络第七版>的学习笔记 运输层1--运输层协议概述 运输层2--用户数据报协议UDP 运输层3--传输控 ...

  8. 链表(python)

    链表1.为什么需要链表顺序表的构建需要预先知道数据大小来申请连续的存储空间,而在进行扩充时又需要进行数据的搬迁,所以使用起来并不是很灵活.链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理. ...

  9. mybatis3.0-[topic10-14] -全局配置文件_plugins插件简介/ typeHandlers_类型处理器简介 /enviroments_运行环境 /多数据库支持/mappers_sql映射注册

    mybatis3.0-全局配置文件_   下面为中文官网解释 全局配置文件的标签需要按如下定义的顺序: <!ELEMENT configuration (properties?, setting ...

  10. Run Code Once on First Load (Concurrency Safe)

    原文: https://golangcode.com/run-code-once-with-sync/ ------------------------------------------------ ...