Vue实例挂载的过程发生了什么

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Vue实例挂载的过程发生了什么

一、分析

首先找到vue构造函数

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


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

 

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

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


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

 

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

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


 Vue.prototype._init = function (options?: Object) {
 const vm: Component = this
 // a uid
     vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
 mark(startTag)
     }
 // a flag to avoid this being observed
     vm._isVue = true
 // merge options
 // 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法
 if (options && options._isComponent) {
 // optimize internal component instantiation
 // since dynamic options merging is pretty slow, and none of the
 // internal component options needs special treatment.
 initInternalComponent(vm, options)
     } else { // 合并vue属性
       vm.$options = mergeOptions(
 resolveConstructorOptions(vm.constructor),
         options || {},
         vm
       )
     }
 /* istanbul ignore else */
 if (process.env.NODE_ENV !== 'production') {
 // 初始化proxy拦截器
 initProxy(vm)
     } else {
       vm._renderProxy = vm
     }
 // expose real self
     vm._self = vm
 // 初始化组件生命周期标志位
 initLifecycle(vm)
 // 初始化组件事件侦听
 initEvents(vm)
 // 初始化渲染方法
 initRender(vm)
 callHook(vm, 'beforeCreate')
 // 初始化依赖注入内容,在初始化data、props之前
 initInjections(vm) // resolve injections before data/props
 // 初始化props/data/method/watch/methods
 initState(vm)
 initProvide(vm) // resolve provide after data/props
 callHook(vm, 'created')
 /* istanbul ignore if */
 if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
       vm._name = formatComponentName(vm, false)
 mark(endTag)
 measure(`vue ${vm._name} init`, startTag, endTag)
     }
 // 挂载元素
 if (vm.$options.el) {
       vm.$mount(vm.$options.el)
     }
   }

 

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

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

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

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


 export function initState (vm: Component) {
 // 初始化组件的watcher列表
   vm._watchers = []
 const opts = vm.$options
 // 初始化props
 if (opts.props) initProps(vm, opts.props)
// 初始化methods方法
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
 // 初始化data  
 initData(vm)
   } else {
 observe(vm._data = {}, true /* asRootData */)
   }
 if (opts.computed) initComputed(vm, opts.computed)
 if (opts.watch && opts.watch !== nativeWatch) {
 initWatch(vm, opts.watch)
   }
 }


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


 function initData (vm: Component) {
 let data = vm.$options.data
 // 获取到组件上的data
   data = vm._data = typeof data === 'function'
     ? getData(data, vm)
     : data || {}
 if (!isPlainObject(data)) {
     data = {}
    process.env.NODE_ENV !== 'production' && warn(
 'data functions should return an object:\n' +
 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
       vm
     )
   }
 // proxy data on instance
 const keys = Object.keys(data)
 const props = vm.$options.props
 const methods = vm.$options.methods
 let i = keys.length
 while (i--) {
 const key = keys[i]
 if (process.env.NODE_ENV !== 'production') {
 // 属性名不能与方法名重复
 if (methods && hasOwn(methods, key)) {
 warn(
 `Method "${key}" has already been defined as a data property.`,
           vm
         )
       }
     }
 // 属性名不能与state名称重复
 if (props && hasOwn(props, key)) {
       process.env.NODE_ENV !== 'production' && warn(
 `The data property "${key}" is already declared as a prop. ` +
 `Use prop default value instead.`,
         vm
       )
     } else if (!isReserved(key)) { // 验证key值的合法性
 // 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据
 proxy(vm, `_data`, key)
     }
   }
 // observe data
 // 响应式监听data是数据的变化
 observe(data, true /* asRootData */)
 }


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

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

挂载方法是调用vm.$mount方法

源码位置:


 Vue.prototype.$mount = function (
   el?: string | Element,
   hydrating?: boolean
 ): Component {
 // 获取或查询元素
   el = el && query(el)
/* istanbul ignore if */
// vue 不允许直接挂载到body或页面文档上
 if (el === document.body || el === document.documentElement) {
     process.env.NODE_ENV !== 'production' && warn(
 `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
     )
 return this
   }
 const options = this.$options
 // resolve template/el and convert to render function
 if (!options.render) {
 let template = options.template
 // 存在template模板,解析vue模板文件
 if (template) {
 if (typeof template === 'string') {
 if (template.charAt(0) === '#') {
           template = idToTemplate(template)
 /* istanbul ignore if */
 if (process.env.NODE_ENV !== 'production' && !template) {
 warn(
 `Template element not found or is empty: ${options.template}`,
 this
             )
           }
         }
       } else if (template.nodeType) {
         template = template.innerHTML
       } else {
 if (process.env.NODE_ENV !== 'production') {
 warn('invalid template option:' + template, this)
         }
 return this
       }
     } else if (el) {
 // 通过选择器获取元素内容
       template = getOuterHTML(el)
     }
 if (template) {
 /* istanbul ignore if */
 if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
 mark('compile')
       }
 /**
        *  1.将temmplate解析ast tree
        *  2.将ast tree转换成render语法字符串
        *  3.生成render方法
        */
 const { render, staticRenderFns } = compileToFunctions(template, {
 outputSourceRange: process.env.NODE_ENV !== 'production',
         shouldDecodeNewlines,
         shouldDecodeNewlinesForHref,
 delimiters: options.delimiters,
 comments: options.comments
       }, this)
       options.render = render
       options.staticRenderFns = staticRenderFns
 /* istanbul ignore if */
 if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
 mark('compile end')
 measure(`vue ${this._name} compile`, 'compile', 'compile end')
       }
     }
   }
 return mount.call(this, el, hydrating)
 }


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

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

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

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

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

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


 // public mount method
 Vue.prototype.$mount = function (
   el?: string | Element,
   hydrating?: boolean
 ): Component {
   el = el && inBrowser ? query(el) : undefined
 // 渲染组件
 return mountComponent(this, el, hydrating)
 }


调用mountComponent渲染组件


export function mountComponent (
vm: Component,
el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
// 如果没有获取解析的render函数,则会抛出警告
// render是解析模板文件生成的
if (!vm.$options.render) {
     vm.$options.render = createEmptyVNode
 if (process.env.NODE_ENV !== 'production') {
 /* istanbul ignore if */
 if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
         vm.$options.el || el) {
 warn(
 'You are using the runtime-only build of Vue where the template ' +
 'compiler is not available. Either pre-compile the templates into ' +
 'render functions, or use the compiler-included build.',
           vm
         )
       } else {
 // 没有获取到vue的模板文件
 warn(
 'Failed to mount component: template or render function not defined.',
           vm
         )
       }
     }
   }
 // 执行beforeMount钩子
 callHook(vm, 'beforeMount')
 let updateComponent
 /* istanbul ignore if */
 if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
     updateComponent = () => {
 const name = vm._name
 const id = vm._uid
 const startTag = `vue-perf-start:${id}`
 const endTag = `vue-perf-end:${id}`
 mark(startTag)
 const vnode = vm._render()
 mark(endTag)
 measure(`vue ${name} render`, startTag, endTag)
 mark(startTag)
       vm._update(vnode, hydrating)
 mark(endTag)
 measure(`vue ${name} patch`, startTag, endTag)
     }
   } else {
 // 定义更新函数
     updateComponent = () => {
 // 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
       vm._update(vm._render(), hydrating)
     }
   }
 // we set this to vm._watcher inside the watcher's constructor
 // since the watcher's initial patch may call $forceUpdate (e.g. inside child
 // component's mounted hook), which relies on vm._watcher being already defined
 // 监听当前组件状态,当有数据变化时,更新组件
 new Watcher(vm, updateComponent, noop, {
     before () {
 if (vm._isMounted && !vm._isDestroyed) {
 // 数据更新引发的组件更新
 callHook(vm, 'beforeUpdate')
       }
     }
   }, true /* isRenderWatcher */)
   hydrating = false
 // manually mounted instance, call mounted on self
 // mounted is called for render-created child components in its inserted hook
 if (vm.$vnode == null) {
     vm._isMounted = true
 callHook(vm, 'mounted')
   }
 return vm
 }


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

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

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

render的作用主要是生成vnode

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


// 定义vue 原型上的render方法
Vue.prototype._render = function (): VNode {
const vm: Component = this
// render函数来自于组件的option
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
        vm.$scopedSlots = normalizeScopedSlots(
            _parentVnode.data.scopedSlots,
             vm.$slots,
             vm.$scopedSlots
         )
     }
 // set parent vnode. this allows render functions to have access
 // to the data on the placeholder node.
     vm.$vnode = _parentVnode
 // render self
 let vnode
 try {
 // There's no need to maintain a stack because all render fns are called
 // separately from one another. Nested component's render fns are called
 // when parent component is patched.
         currentRenderingInstance = vm
 // 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode
         vnode = render.call(vm._renderProxy, vm.$createElement)
     } catch (e) {
 handleError(e, vm, `render`)
 // return error render result,
 // or previous vnode to prevent render error causing blank component
 /* istanbul ignore else */
 if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
 try {
                 vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
             } catch (e) {
 handleError(e, vm, `renderError`)
                 vnode = vm._vnode
             }
         } else {
             vnode = vm._vnode
         }
     } finally {
         currentRenderingInstance = null
     }
 // if the returned array contains only a single node, allow it
 if (Array.isArray(vnode) && vnode.length === 1) {
         vnode = vnode[0]
     }
 // return empty vnode in case the render function errored out
 if (!(vnode instanceof VNode)) {
 if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
 warn(
 'Multiple root nodes returned from render function. Render function ' +
 'should return a single root node.',
                 vm
             )
         }
         vnode = createEmptyVNode()
     }
 // set parent
     vnode.parent = _parentVnode
 return vnode
 }


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

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


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)
     }
 restoreActiveInstance()
 // update __vue__ reference
 if (prevEl) {
       prevEl.__vue__ = null
     }
 if (vm.$el) {
       vm.$el.__vue__ = vm
     }
 // if parent is an HOC, update its $el as well
 if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
       vm.$parent.$el = vm.$el
     }
 // updated hook is called by the scheduler to ensure that children are
 // updated in a parent's updated hook.
   }


二、结论

  • new Vue的时候调用会调用_init方法
  • 定义 $set$get$delete$watch 等方法
  • 定义 $on$off$emit$off等事件
  • 定义 _update$forceUpdate$destroy生命周期
  • 调用$mount进行页面的挂载
  • 挂载的时候主要是通过mountComponent方法
  • 定义updateComponent更新函数
  • 执行render生成虚拟DOM
  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中
相关文章
|
5天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
5天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
5天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
5天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
4天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
6天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
4天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
19天前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
6天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
11天前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发