合并配置
new Vue的场景有两个,一个是主动调用new Vue(options) 的方式实例化一个 Vue 对象;另一种组件创建过程中内部通过 new Vue(options) 实例化子组件Sub。它们都会执行实例的_init(options)方法,首先会执行merge options,不过这里有一个判断是如果存在options._isComponent,也就是这里分场景去合并了,
Vue.prototype._init = function (options?: Object) {
...
// merge options
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 {
// 合并用户传入的options和vue的构造函数的options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
}一个简单的示例:
import Vue from 'vue'
let childComp = {
template: '<div>{{msg}}</div>',
created() {
console.log('child created')
},
mounted() {
console.log('child mounted')
},
data() {
return {
msg: 'Hello Vue'
}
}
}
Vue.mixin({
created() {
console.log('parent created')
}
})
let app = new Vue({
el: '#app',
render: h => h(childComp)
})外部调用
当执行 new Vue 的时候,在执行 this._init(options) 的时候,就会执行如下逻辑去合并 options,通过调用 mergeOptions 方法来合并,它实际上就是把 resolveConstructorOptions(vm.constructor) 的返回值和 options 做合并,resolveConstructorOptions 的实现先不考虑,在我们这个场景下,它返回的是 vm.constructor.options,相当于 Vue.options,这个值在 initGlobalAPI(Vue) 的时候定义了,之前也分析过
export function initGlobalAPI (Vue: GlobalAPI) {
// ...
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
// ...
}这部分在init一节我们详细分析过,最后通过extend将一些内置组件扩展到Vue.options.components上。
回到mergeOptions,详细看下合并策略。
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, 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)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}mergeOptions主要就是将parent和child两个对象根据合并策略来合并成一个新对象返回。首先将child的extends 和 mixins 合并到 parent 上,然后遍历parent,调用 mergeField,然后再遍历 child,如果 key 不在 parent 的自身属性上,则调用 mergeField。
重点看下mergeField方法,它对于不同的key将采用不同的合并策略。 比如生命周期函数,
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})这其中的 LIFECYCLE_HOOKS 的定义在 src/shared/constants.js 中:
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]这里使用了三层的3元运算符,如果不存在childVal,则直接返回parentVal。如果存在,则继续判断parentVal是否存在,如果存在,合并parentVal和childVal,如果不存在,则保证childVal是一个数组并返回。所以一旦 parent 和 child 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。 最后vm.$options 的值差不多是如下这样:
vm.$options = {
components: { },
created: [
function created() {
console.log('parent created')
}
],
directives: { },
filters: { },
_base: function Vue(options) {
// ...
},
el: "#app",
render: function (h) {
//...
}
}组件 merge
前面说过,组件的构造函数Ctor通过Vue.extend创建,定义在src/core/global-api/extend.js中,
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
// ...
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// ...
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// ...
return Sub
}extendOptions会和Super.options合并到Sub.options中。
当实例化子组件时,接下来我们再看下子组件的初始化过程,代码定义在 src/core/vdom/create-component.js,
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// ...
return new vnode.componentOptions.Ctor(options)
}这个函数是在初始化函数init中调用的的,而init这个钩子函数在创建组件createComponent中安装,createComponent中比较重要的一部分就是安装组件钩子函数。而createComponentInstanceForVnode就是init中调用
init(){
...
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
...
}createComponentInstanceForVnode也定义在src/core/vdom/create-component.js
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// ...
return new vnode.componentOptions.Ctor(options)
}最后返回了new vnode.componentOptions.Ctor(options),vnode.componentOptions.Ctor 就是通过Vue.extend返回的Sub,就会执行 this._init 逻辑再次走到了 Vue 实例的初始化逻辑,
const Sub = function VueComponent (options) {
this._init(options)
}因为 options._isComponent 为 true,那么合并 options 的过程走到了 initInternalComponent(vm, options) 逻辑。分析到这里,不得不感叹一下这缜密的逻辑,一切有迹可循,需要学习的是其中的设计思想。 先来看一下它的代码实现,在 src/core/instance/init.js 中,
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}先执行了const opts = vm.$options = Object.create(vm.constructor.options),vm.constructor就是Sub。 然后vm.$options中保存了父vnode实例包括父vnode的其他配置(propsData,listeners,children,tag),执行完之后, vm.$options中的配置如下:
vm.$options = {
parent: Vue /*父Vue实例*/,
propsData: undefined,
_componentTag: undefined,
_parentVnode: VNode /*父VNode实例*/,
_renderChildren:undefined,
__proto__: {
components: { },
directives: { },
filters: { },
_base: function Vue(options) {
//...
},
_Ctor: {},
created: [
function created() {
console.log('parent created')
}, function created() {
console.log('child created')
}
],
mounted: [
function mounted() {
console.log('child mounted')
}
],
data() {
return {
msg: 'Hello Vue'
}
},
template: '<div>{{msg}}</div>'
}
}