在初识创建Vue实例流程中,对创建一个Vue实例已经有了一个大概的了解。由于组件实例和new Vue实例的创建会走不到不同的分支,所以这一节会忽略组件实例初始化的部分,在patch阶段再回过头来讲解组件实例的创建过程。
准备阶段
// html片段 <div id="app"></div>
// index.js
new Vue({
el: '#app'
})
现在我们创建一个Vue实例,在上一节的基础上细化分析。我们已经知道Vue构造函数接受一个options对象,并执行_init(options)方法初始化对象。其中第一步就是先合并配置。
合并配置
// src/core/instance/index.js
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
合并配置的逻辑有两个分支,这里可以明确的告诉大家,组件实例的options上才会有_isComponent属性,我们的例子并没有传递这个属性。所以这里走到了下面的分支。调用mergeOptions函数合并配置,它接受三个参数,resolveConstructorOptions函数的返回值,传入的options和当前实例vm。那这里就只有一个参数不明确就是resolveConstructorOptions的返回值。那么它返回什么呢?
resolveConstructorOptions
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
if (Ctor.super) {
// 递归的返回父类的options
const superOptions = resolveConstructorOptions(Ctor.super)
// 缓存的父类options
const cachedSuperOptions = Ctor.superOptions
// 如果发生了改变,如使用Mixin改变了Vue的options
if (superOptions !== cachedSuperOptions) {
Ctor.superOptions = superOptions
const modifiedOptions = resolveModifiedOptions(Ctor)
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
// 更新修改了的配置
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
我们使用的是new Vue的方式,所以构造函数上并不存在super属性,也不会走到if分支。resolveConstructorOptions最终就是返回构造函数上的options。Vue构造函数的options在认识Vue构造函数时提到过,如下
options: {
components: {
KeepAlive,
transition,
transitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
},
现在所有的参数,已经知道了,下面就开始合并属性
mergeOptions
// src/core/util/options.js
// 空对象
const strats = config.optionMergeStrategies
strats.data = function() {}
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (typeof child === 'function') {
child = child.options
}
// 格式化props
// 所有写法转化为一个对象,并且每个属性都是一个包含type属性的对象
normalizeProps(child, vm)
// 所有写法转化为一个对象,并且每个属性都是一个包含from属性的对象
normalizeInject(child, vm)
// 格式化指令
normalizeDirectives(child)
// 只在Vue实例,不是其他mergeOptions结果的对象上合并extends和mixins
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
// strats合并策略对象,根据不同的配置完成不同的合并策略
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
首先需要了解的是,在执行mergeOptions之前,会初始化一个空对象strats,它保存了每种属性(如data, computed)对应的合并方法。
合并流程
- 格式化props,inject,directive,最终都会转会为对象
- 递归调用mergeOptions合并child(传入的options)中的extends和mixins
- 不同属性根据不同的合并规则合并
如果格式化和各个属性的合并规则全部都贴一遍代码,工作量太大,同时意义也不大。格式化的代码我会考虑另外写一篇总结性的文章来讨论,而合并的规则使用流程图的方式
需要注意的是data和provide因为其特殊性,合并返回的并不是一个对象,而是一个方法,合并发生在初始化的过程。合并的规则可以理解成递归的合并,同名的属性将会覆盖默认值(mixin等混入的值)。watch的合并规则和生命周期钩子函数类似,最后都会被转化为数组,并且优先执行默认的(mixin等混入的值)
初始化生命周期
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
// 找到第一个非抽象组件作为父组件,并向父组件中添加本实例
// 如果是new构造的实例,parent就是为空
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
// 生命周期相关的属性
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
合并生命周期,就相对的简单一些,定义一些属性,记录实例的状态,如是否挂载,父组件实例,子组件实例等等。需要注意的是,在记录父组件实例的时候会跳过抽象组件,如keep-alive。
如何调用生命周期钩子函数
在初始化过程中,通过callHook函数调用了beforeCreate和created钩子函数,那么它是怎么执行我们编写的钩子函数?
// src/core/instance/lifecycle.js
export function callHook (vm: Component, hook: string) {
// 更新全局的target置空
// 防止钩子函数执行过程修改属性,造成死循环(个人理解,有待考证)
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
// 父组件在标签上使用@hooks="xxx"的方式
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
// src/core/util/error.js
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
// 如果是promise
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
在合并配置阶段,生命周期钩子函数已经被转化为数组,在callHook中,根据传入的生命周期钩子函数名,循环执行各个生命周期函数。
结尾
events,Render,inject,provide这部分在使用new Vue涉及的并不多,所以打算在创建组件实例的时候再回过头来看。下一篇来讨论状态(initState)的初始化。