Vue实例挂载的过程 - 图1

一、思考

我们都听过知其然知其所以然这句话

那么不知道大家是否思考过new Vue()这个过程中究竟做了些什么?

过程中是如何完成数据的绑定,又是如何将数据渲染到视图的等等

一、分析

首先找到vue的构造函数

源码位置:src\core\instance\index.js

  1. function Vue (options) {
  2. if (process.env.NODE_ENV !== 'production' &&
  3. !(this instanceof Vue)
  4. ) {
  5. warn('Vue is a constructor and should be called with the `new` keyword')
  6. }
  7. this._init(options)
  8. }

options是用户传递过来的配置项,如data、methods等常用的方法

vue构建函数调用_init方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法

  1. initMixin(Vue); // 定义 _init
  2. stateMixin(Vue); // 定义 $set $get $delete $watch 等
  3. eventsMixin(Vue); // 定义事件 $on $once $off $emit
  4. lifecycleMixin(Vue);// 定义 _update $forceUpdate $destroy
  5. renderMixin(Vue); // 定义 _render 返回虚拟dom

首先可以看initMixin方法,发现该方法在Vue原型上定义了_init方法

源码位置:src\core\instance\init.js

  1. Vue.prototype._init = function (options?: Object) {
  2. const vm: Component = this
  3. // a uid
  4. vm._uid = uid++
  5. let startTag, endTag
  6. /* istanbul ignore if */
  7. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  8. startTag = `vue-perf-start:${vm._uid}`
  9. endTag = `vue-perf-end:${vm._uid}`
  10. mark(startTag)
  11. }
  12. // a flag to avoid this being observed
  13. vm._isVue = true
  14. // merge options
  15. // 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法
  16. if (options && options._isComponent) {
  17. // optimize internal component instantiation
  18. // since dynamic options merging is pretty slow, and none of the
  19. // internal component options needs special treatment.
  20. initInternalComponent(vm, options)
  21. } else { // 合并vue属性
  22. vm.$options = mergeOptions(
  23. resolveConstructorOptions(vm.constructor),
  24. options || {},
  25. vm
  26. )
  27. }
  28. /* istanbul ignore else */
  29. if (process.env.NODE_ENV !== 'production') {
  30. // 初始化proxy拦截器
  31. initProxy(vm)
  32. } else {
  33. vm._renderProxy = vm
  34. }
  35. // expose real self
  36. vm._self = vm
  37. // 初始化组件生命周期标志位
  38. initLifecycle(vm)
  39. // 初始化组件事件侦听
  40. initEvents(vm)
  41. // 初始化渲染方法
  42. initRender(vm)
  43. callHook(vm, 'beforeCreate')
  44. // 初始化依赖注入内容,在初始化data、props之前
  45. initInjections(vm) // resolve injections before data/props
  46. // 初始化props/data/method/watch/methods
  47. initState(vm)
  48. initProvide(vm) // resolve provide after data/props
  49. callHook(vm, 'created')
  50. /* istanbul ignore if */
  51. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  52. vm._name = formatComponentName(vm, false)
  53. mark(endTag)
  54. measure(`vue ${vm._name} init`, startTag, endTag)
  55. }
  56. // 挂载元素
  57. if (vm.$options.el) {
  58. vm.$mount(vm.$options.el)
  59. }
  60. }

仔细阅读上面的代码,我们得到以下结论:

  • 在调用beforeCreate之前,数据初始化并未完成,像dataprops这些属性无法访问到
  • 到了created的时候,数据已经初始化完成,能够访问dataprops这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素
  • 挂载方法是调用vm.$mount方法

initState方法是完成props/data/method/watch/methods的初始化

源码位置:src\core\instance\state.js

  1. export function initState (vm: Component) {
  2. // 初始化组件的watcher列表
  3. vm._watchers = []
  4. const opts = vm.$options
  5. // 初始化props
  6. if (opts.props) initProps(vm, opts.props)
  7. // 初始化methods方法
  8. if (opts.methods) initMethods(vm, opts.methods)
  9. if (opts.data) {
  10. // 初始化data
  11. initData(vm)
  12. } else {
  13. observe(vm._data = {}, true /* asRootData */)
  14. }
  15. if (opts.computed) initComputed(vm, opts.computed)
  16. if (opts.watch && opts.watch !== nativeWatch) {
  17. initWatch(vm, opts.watch)
  18. }
  19. }

我们和这里主要看初始化data的方法为initData,它与initState在同一文件上

  1. function initData (vm: Component) {
  2. let data = vm.$options.data
  3. // 获取到组件上的data
  4. data = vm._data = typeof data === 'function'
  5. ? getData(data, vm)
  6. : data || {}
  7. if (!isPlainObject(data)) {
  8. data = {}
  9. process.env.NODE_ENV !== 'production' && warn(
  10. 'data functions should return an object:\n' +
  11. 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
  12. vm
  13. )
  14. }
  15. // proxy data on instance
  16. const keys = Object.keys(data)
  17. const props = vm.$options.props
  18. const methods = vm.$options.methods
  19. let i = keys.length
  20. while (i--) {
  21. const key = keys[i]
  22. if (process.env.NODE_ENV !== 'production') {
  23. // 属性名不能与方法名重复
  24. if (methods && hasOwn(methods, key)) {
  25. warn(
  26. `Method "${key}" has already been defined as a data property.`,
  27. vm
  28. )
  29. }
  30. }
  31. // 属性名不能与state名称重复
  32. if (props && hasOwn(props, key)) {
  33. process.env.NODE_ENV !== 'production' && warn(
  34. `The data property "${key}" is already declared as a prop. ` +
  35. `Use prop default value instead.`,
  36. vm
  37. )
  38. } else if (!isReserved(key)) { // 验证key值的合法性
  39. // 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据
  40. proxy(vm, `_data`, key)
  41. }
  42. }
  43. // observe data
  44. // 响应式监听data是数据的变化
  45. observe(data, true /* asRootData */)
  46. }

仔细阅读上面的代码,我们可以得到以下结论:

  • 初始化顺序:propsmethodsdata
  • data定义的时候可选择函数形式或者对象形式(组件只能为函数形式)

关于数据响应式在这就不展开详细说明

上文提到挂载方法是调用vm.$mount方法

源码位置:

  1. Vue.prototype.$mount = function (
  2. el?: string | Element,
  3. hydrating?: boolean
  4. ): Component {
  5. // 获取或查询元素
  6. el = el && query(el)
  7. /* istanbul ignore if */
  8. // vue 不允许直接挂载到body或页面文档上
  9. if (el === document.body || el === document.documentElement) {
  10. process.env.NODE_ENV !== 'production' && warn(
  11. `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
  12. )
  13. return this
  14. }
  15. const options = this.$options
  16. // resolve template/el and convert to render function
  17. if (!options.render) {
  18. let template = options.template
  19. // 存在template模板,解析vue模板文件
  20. if (template) {
  21. if (typeof template === 'string') {
  22. if (template.charAt(0) === '#') {
  23. template = idToTemplate(template)
  24. /* istanbul ignore if */
  25. if (process.env.NODE_ENV !== 'production' && !template) {
  26. warn(
  27. `Template element not found or is empty: ${options.template}`,
  28. this
  29. )
  30. }
  31. }
  32. } else if (template.nodeType) {
  33. template = template.innerHTML
  34. } else {
  35. if (process.env.NODE_ENV !== 'production') {
  36. warn('invalid template option:' + template, this)
  37. }
  38. return this
  39. }
  40. } else if (el) {
  41. // 通过选择器获取元素内容
  42. template = getOuterHTML(el)
  43. }
  44. if (template) {
  45. /* istanbul ignore if */
  46. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  47. mark('compile')
  48. }
  49. /**
  50. * 1.将temmplate解析ast tree
  51. * 2.将ast tree转换成render语法字符串
  52. * 3.生成render方法
  53. */
  54. const { render, staticRenderFns } = compileToFunctions(template, {
  55. outputSourceRange: process.env.NODE_ENV !== 'production',
  56. shouldDecodeNewlines,
  57. shouldDecodeNewlinesForHref,
  58. delimiters: options.delimiters,
  59. comments: options.comments
  60. }, this)
  61. options.render = render
  62. options.staticRenderFns = staticRenderFns
  63. /* istanbul ignore if */
  64. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  65. mark('compile end')
  66. measure(`vue ${this._name} compile`, 'compile', 'compile end')
  67. }
  68. }
  69. }
  70. return mount.call(this, el, hydrating)
  71. }

阅读上面代码,我们能得到以下结论:

  • 不要将根元素放到body或者html
  • 可以在对象中定义template/render或者直接使用templateel表示元素选择器
  • 最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数

template的解析步骤大致分为以下几步:

  • html文档片段解析成ast描述符
  • ast描述符解析成字符串
  • 生成render函数

生成render函数,挂载到vm上后,会再次调用mount方法

源码位置:src\platforms\web\runtime\index.js

  1. // public mount method
  2. Vue.prototype.$mount = function (
  3. el?: string | Element,
  4. hydrating?: boolean
  5. ): Component {
  6. el = el && inBrowser ? query(el) : undefined
  7. // 渲染组件
  8. return mountComponent(this, el, hydrating)
  9. }

调用mountComponent渲染组件

  1. export function mountComponent (
  2. vm: Component,
  3. el: ?Element,
  4. hydrating?: boolean
  5. ): Component {
  6. vm.$el = el
  7. // 如果没有获取解析的render函数,则会抛出警告
  8. // render是解析模板文件生成的
  9. if (!vm.$options.render) {
  10. vm.$options.render = createEmptyVNode
  11. if (process.env.NODE_ENV !== 'production') {
  12. /* istanbul ignore if */
  13. if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
  14. vm.$options.el || el) {
  15. warn(
  16. 'You are using the runtime-only build of Vue where the template ' +
  17. 'compiler is not available. Either pre-compile the templates into ' +
  18. 'render functions, or use the compiler-included build.',
  19. vm
  20. )
  21. } else {
  22. // 没有获取到vue的模板文件
  23. warn(
  24. 'Failed to mount component: template or render function not defined.',
  25. vm
  26. )
  27. }
  28. }
  29. }
  30. // 执行beforeMount钩子
  31. callHook(vm, 'beforeMount')
  32. let updateComponent
  33. /* istanbul ignore if */
  34. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  35. updateComponent = () => {
  36. const name = vm._name
  37. const id = vm._uid
  38. const startTag = `vue-perf-start:${id}`
  39. const endTag = `vue-perf-end:${id}`
  40. mark(startTag)
  41. const vnode = vm._render()
  42. mark(endTag)
  43. measure(`vue ${name} render`, startTag, endTag)
  44. mark(startTag)
  45. vm._update(vnode, hydrating)
  46. mark(endTag)
  47. measure(`vue ${name} patch`, startTag, endTag)
  48. }
  49. } else {
  50. // 定义更新函数
  51. updateComponent = () => {
  52. // 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
  53. vm._update(vm._render(), hydrating)
  54. }
  55. }
  56. // we set this to vm._watcher inside the watcher's constructor
  57. // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  58. // component's mounted hook), which relies on vm._watcher being already defined
  59. // 监听当前组件状态,当有数据变化时,更新组件
  60. new Watcher(vm, updateComponent, noop, {
  61. before () {
  62. if (vm._isMounted && !vm._isDestroyed) {
  63. // 数据更新引发的组件更新
  64. callHook(vm, 'beforeUpdate')
  65. }
  66. }
  67. }, true /* isRenderWatcher */)
  68. hydrating = false
  69. // manually mounted instance, call mounted on self
  70. // mounted is called for render-created child components in its inserted hook
  71. if (vm.$vnode == null) {
  72. vm._isMounted = true
  73. callHook(vm, 'mounted')
  74. }
  75. return vm
  76. }

阅读上面代码,我们得到以下结论:

  • 会触发beforeCreate钩子
  • 定义updateComponent渲染页面视图的方法
  • 监听组件数据,一旦发生变化,触发beforeUpdate生命钩子

updateComponent方法主要执行在vue初始化时声明的renderupdate方法

render的作用主要是生成vnode

源码位置:src\core\instance\render.js

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

_update主要功能是调用patch,将vnode转换为真实DOM,并且更新到页面中

源码位置: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. // 设置当前激活的作用域
  6. const restoreActiveInstance = setActiveInstance(vm)
  7. vm._vnode = vnode
  8. // Vue.prototype.__patch__ is injected in entry points
  9. // based on the rendering backend used.
  10. if (!prevVnode) {
  11. // initial render
  12. // 执行具体的挂载逻辑
  13. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  14. } else {
  15. // updates
  16. vm.$el = vm.__patch__(prevVnode, vnode)
  17. }
  18. restoreActiveInstance()
  19. // update __vue__ reference
  20. if (prevEl) {
  21. prevEl.__vue__ = null
  22. }
  23. if (vm.$el) {
  24. vm.$el.__vue__ = vm
  25. }
  26. // if parent is an HOC, update its $el as well
  27. if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
  28. vm.$parent.$el = vm.$el
  29. }
  30. // updated hook is called by the scheduler to ensure that children are
  31. // updated in a parent's updated hook.
  32. }

三、结论

  • new Vue的时候调用会调用_init方法
    • 定义 $set$get$delete$watch 等方法
    • 定义 $on$off$emit$off等事件
    • 定义 _update$forceUpdate$destroy生命周期
  • 调用$mount进行页面的挂载
  • 挂载的时候主要是通过mountComponent方法
  • 定义updateComponent更新函数
  • 执行render生成虚拟DOM
  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中