Vue实例挂载的过程

简介: Vue实例挂载的过程

一、思考与分析

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

那么不知道是否思考过new 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


ZVue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
Zconst vm: Component = this
Zconst prevEl = vm.$el
Zconst prevVnode = vm._vnode
Z// 设置当前激活的作用域
Zconst restoreActiveInstance = setActiveInstance(vm)
Z    vm._vnode = vnode
Z// Vue.prototype.__patch__ is injected in entry points
Z// based on the rendering backend used.
Z if (!prevVnode) {
Z // initial render
Z // 执行具体的挂载逻辑
Z       vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
Z     } else {
Z // updates
Z       vm.$el = vm.__patch__(prevVnode, vnode)
Z     }
Z restoreActiveInstance()
Z // update __vue__ reference
Z if (prevEl) {
Z       prevEl.__vue__ = null
Z     }
Z if (vm.$el) {
Z       vm.$el.__vue__ = vm
Z     }
Z // if parent is an HOC, update its $el as well
Z if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
Z       vm.$parent.$el = vm.$el
Z     }
Z // updated hook is called by the scheduler to ensure that children are
Z // updated in a parent's updated hook.
Z   }


二、结论

  • new Vue的时候调用会调用_init方法
  • 定义 $set$get$delete$watch 等方法
  • 定义 $on$off$emit$off等事件
  • 定义 _update$forceUpdate$destroy生命周期
  • 调用$mount进行页面的挂载
  • 挂载的时候主要是通过mountComponent方法
  • 定义updateComponent更新函数
  • 执行render生成虚拟DOM
  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中
相关文章
|
2天前
|
JavaScript 前端开发 测试技术
使用 Vue CLI 脚手架生成 Vue 项目
通过 Vue CLI 创建 Vue 项目可以极大地提高开发效率。它不仅提供了一整套标准化的项目结构,还集成了常用的开发工具和配置,使得开发者可以专注于业务逻辑的实现,而不需要花费大量时间在项目配置上。
54 7
使用 Vue CLI 脚手架生成 Vue 项目
|
3天前
|
JavaScript 算法
“Error: error:0308010C:digital envelope routines::unsupported”启动vue项目遇到一个错误【已解决
“Error: error:0308010C:digital envelope routines::unsupported”启动vue项目遇到一个错误【已解决
8 1
|
3天前
|
JavaScript
error Component name “Login“ should always be multi-word vue/multi-word-component-names【已解决】
error Component name “Login“ should always be multi-word vue/multi-word-component-names【已解决】
9 1
|
5天前
|
JavaScript 前端开发 Java
【vue实战项目】通用管理系统:作业列表
【vue实战项目】通用管理系统:作业列表
19 0
|
5天前
|
JavaScript API
【vue实战项目】通用管理系统:信息列表,信息的编辑和删除
【vue实战项目】通用管理系统:信息列表,信息的编辑和删除
21 2
|
5天前
|
JavaScript API
【vue实战项目】通用管理系统:信息列表,信息录入
【vue实战项目】通用管理系统:信息列表,信息录入
13 3
|
5天前
|
JavaScript 前端开发 API
【vue实战项目】通用管理系统:学生列表
【vue实战项目】通用管理系统:学生列表
19 2
|
5天前
|
缓存 JavaScript
【vue实战项目】通用管理系统:首页
【vue实战项目】通用管理系统:首页
13 2
|
5天前
|
JavaScript 前端开发 数据安全/隐私保护
【vue实战项目】通用管理系统:登录页
【vue实战项目】通用管理系统:登录页
13 2
|
5天前
|
JavaScript 网络架构
Vue中三个点(...)的意思
**孤立元素**:通过扩展运算符(`...`)可以将数组元素打印出来,如 `console.log(...iArray)`。 - **添加元素**:可以使用扩展运算符结合数组合并来添加元素,例如 `[&#39;0&#39;, ...iArray, &#39;4&#39;]` 或者使用 `push` 方法。 - **删除元素**:通过解构赋值取出数组第一个元素,如 `const [first, ...last] = arr`。 - **数组合并**:可以使用扩展运算符将多个数组合并,如 `[...arr1, ...arr2]`。
6 0