这篇文章,是用来记录阅读源码过程中,对创建Vue应用流程的了解,从初始化,解析,挂载做一个整体上的了解。只有了解整体的流程,每个步骤做了什么事情有一定的了解后,源码阅读才会更轻松。
你将了解
- 创建
Vue应用大致流程 - 异步方法为什么要在created及之后调用
- 为什么在data中可以使用inject透传的值
_init初始化
其实在认识Vue构造函数时,已经知道,创建Vue实例vm,只执行了_init方法一行代码。这个方法是被initMixin函数添加到Vue.prototype上的。initMixin定义在src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = uid++
vm._isVue = true
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
// 将本身代理到_renderProxy上
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化生命周期
initLifecycle(vm)
// 初始化事件中心
initEvents(vm)
// 初始话render函数
initRender(vm)
callHook(vm, 'beforeCreate')
// 初始化inject
initInjections(vm) // resolve injections before data/props
// 初始化状态,props,methods,data...
initState(vm)
// 初始化provide
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// 挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
_init方法还是很清晰的,大致流程如下:
- 合并配置
- 开发环境,将
_renderProxy代理到vm上,就是让其拥有提示的能力。 - 初始化生命周期,定义一堆属性,记录实例
vm的状态(如,父子关系,生命周期的状态)。 - 初始化事件中心(自定义事件)
- 执行
beforeCreate生命周期钩子函数 - 初始化render函数,添加创建
vnode的方法_c和$createElement到实例vm - 初始化inject(在data,props之前,方便在data和props使用)
- 初始化状态(data,props,methods...)
- 初始化provide
- 执行created生命周期钩子函数
- 挂载
小结
- 为什么
beforeCreate不能操作数据?这里就能找到原因。调用beforeCreate钩子函数时data,methods还没有准备好。 - 为什么
data和props可以使用inject透传过来的值?原因就是inject比data,props先初始化。
同时需要注意,options到底是什么?编写*.vue文件时,都会返回一个对象,在vue-loader编译之后转化为这里的options,如下
<template>
</template>
<script>
export default {
name: 'xxx',
data(){
return {}
}
}
</script>
挂载
最后如果options配置了el属性,那么就会调用$mounted把实例挂载到el节点上。在上一篇中,知道$mounted定义在src/platforms/web/runtime/index.js并在web/entry-runtime-with-compiler.js进行重写,使其拥有编译的能力。
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
// 组件
if (template) {
// 指定的是模板的id
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) {
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
}
}
return mount.call(this, el, hydrating)
}
web/entry-runtime-with-compiler.js中重写$mounted,其核心就是找到组件的模板,使用compileToFunctions对其template进行编译最后生成render函数,最后调用原来的mount也就是原来的$mount方法方法挂载。定义在src/platforms/web/runtime/index.js,实际上就是调用mountComponent,这个方法定义在src/core/instance/lifecycle.js中。
// src/platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 创建渲染函数
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponent的核心就是创建vm的渲染函数。每次渲染都会调用_render方法生成对应的vnode,再调用_update方法生成真正的节点,并添加在页面中。