重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
正文
合并配置(mergeOptions)在两个地方出现,一个是代码主动调用new Vue的时候,一个是创建子组件调用 new Vue的时候,它们都会执行 _init(options) 方法,:
来看下 _init 的逻辑:
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 {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...
}
可以看到两种方式是不一样的,创建子组件的时候用 initInternalComponent, 另一个用 mergeOptions,先来看下第二种:
调用了 resolveConstructorOptions(vm.constructor), 方法:
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
这里传入的Ctor参数是Vue,所以它没有 super,最后返回了 Vue 的 options。
所以在调用 mergeOptions 的时候,传入的第一个参数就是大 Vue 的 options,第二个 options 就是在代码中写 new Vue({}) 的时候传入的参数(比如render,el等),它们两个通过 mergeOptions 合并到了一起,赋值给 vm.$options ,来看下这个 mergeOptions 的逻辑,它在 src/core/util/options.js:
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
}
它其实就是把参数 parent 和参数 child 合并,先递归把 extends 和 mixins 合并到 parent 上,然后遍历 parent,调用 mergeField ,然后再遍历 child,如果 key 不在 parent 上,就调用 mergeField。
在 mergeField 中就是调用 strats 方法,根据传入的 key 的不同得到不同的 strat,如果没有就是 defaultStrat:
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
可见 defaultStrat 优先是 parent,然后是 child (一个简单的合并策略)。接着上面的 strats,它其实就是定义了很多合并策略,strats 最开始的定义是:
const strats = config.optionMergeStrategies
optionMergeStrategies 的定义是一个空对象: Object.create(null),也就是说这个 strats 主要是用来扩展的,后面接着在 strats 上扩展了很多属性,比如data:
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
又比如 component,filter:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null)
if (childVal) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
这里主要说下生命周期是如何合并的:
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
可以看到 parentVal 和 childVal 可以传 Function 或者 Array<Function>,返回了 Array<Function>,也就是返回了一个 Function 类型的数组,返回值逻辑:
if(子有) {
if(父有){
return 合并父子
}else{
if(子是数组){
return 子
}else{
return [子]
}
}
}else{
return 父
}
知道了 mergeOptions 的逻辑之后,再看下它的第一个参数:resolveConstructorOptions,这个返回 Vue 的 options,这个 Vue 的 options 定义在 src/core/global-api/index.js:
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)
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
// ASSET_TYPES 定义在constance.js里
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
在 Vue 初始化的时候定义了一个空对象 options, ASSET_TYPES 也都扩展到这个 options 里面,接着用 builtInComponents 扩展了一些内置组件(比如transition,keepAlive),把它们都添加到 _init 中的 vm.$options 上。
而在mixin模块:
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
这是个合并全局options的过程,同样的把全局传入的mixin对象,通过 mergeOptions 把传入的对象,也混入到 Vue 的 options 上,这些就是在执行 new Vue 的时候进行的合并。
另一个合并在子组件在初始化的时候,先回忆一下组件的构造函数过程:
/**
* 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 对应的就是前面定义的组件对象,它会和 Vue.options 合并到 Sub.opitons 中。
接着回忆一下子组件的初始化过程:
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)
}
vnode.componentOptions.Ctor 指向的是 Vue.extend 的返回值 Sub,所以在执行它的时候,会接着执行子组件的 this._init(options)。
此时合并过程走到了 initInternalComponent 方法:
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
}
}
注意调用它的时候,传入的 vm 是子组件实例,所以里面的 vm.constructor.options 就是子组件构造器上的 options,子组件构造器是在 Vue.extend 的时候拿到的 options,所以这里的 vm.constructor 就是子组件构造函数的 Sub,相当于 vm.$options = Object.create(Sub.options),而这里用了 Object.create 方式创建,所以相当于:vm.$options.__proto__ = 子组件实例合并的options。
接着又把实例化子组件传入的子组件父 VNode 实例 parentVnode、子组件的父 Vue 实例 parent 保存到 vm.$options 中,另外还保留了 parentVnode 配置中的如 propsData 等其它的属性。
举个例子把上面的过程捋一遍(重点看多个created是如何合并的):
import Vue from 'vue'
let childComp = {
template: '<div>{{msg}}</div>',
created() {
console.log('child created')
},
mounted() {
console.log('child mounted')
},
data() {
return {
msg: '123'
}
}
}
Vue.mixin({
created() {
console.log('parent created')
}
})
let app = new Vue({
el: '#app',
render: h => h(childComp)
})
在 new Vue 之前,会先执行 Vue.mixin,也就是合并全局的 options,也就是 Vue.options,所以第一次走到 mergeOptions 的时候,参数 parent 上没有的,参数 child 上是 created,接着进行 mergeHook 的时候,参数 parenVal 是没有的(因为new Vue({})上没有created),参数 childVal 就是 Vue.mixin 上的 created ,然后返回一个 [created(){}],此时执行 mergeField 的 options 上多了一个 created 属性,值是 [created(){}],到此 Vue.mixin 的合并结束。
接着执行 new Vue 逻辑,在执行 _init 的时候,会执行:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
此时 resolveConstructorOptions(vm.constructor) 返回的就是大 Vue 的 options,接着执行到 mergeOptions 的时候,parent 就是大 Vue 的 options,此时的 parent 多了一个 created,这个 created 是刚才 Vue.mixin 添加进去的,而此时的 child 就是 #app 了,这一步到最后会赋值给 vm.$options。
再往后,又会继续走 mergeOptions,这次是 Vue.extend 创建子组件构造器的时候执行的:
Sub.options = mergeOptions(
Super.options,
extendOptions
)
因为子组件构造器是继承大 Vue 的,所以这里的 Super.options 就是大 Vue 的 options,把它和子组件自定义的对象(例子中的childComp),也就是对子组件定义的配置进行合并,所以在执行到这一步的 mergeOptions 的时候,该方法的参数 parent 就是前面合并之后的大 Vue 的 options,而参数 child 就是子组件定义的配置(例子中的childComp),注意:参数 child 上有个 created,而参数 parent 上也有 created,接着在执行到 mergeHook 的时候,就会执行:
if(子有) {
if(父有){
return 合并父子
}else{
if(子是数组){
return 子
}else{
return [子]
}
}
}else{
return 父
}
此时会走到合并父子,也就是代码中的 parentVak.concat(childVal),也就是 先父后子。
然后 mergeField 返回的就是通过 mergeOptons 合并之后的 options,也就是子组件构造器的options,也就是 Sub.options,此时里面的 created 属性就是 [created(){}, created(){}]。
到这里 new Vue 的合并就结束了,后面在执行子组件的初始化的时候,会执行 this._init:
const Sub = function VueComponent (options) {
this._init(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 {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
此时就会执行 initInternalComponent:
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
}
}
这里的 vm.constructor.options 就是刚才合并了的 options,然后把它通过 Object.create 创建,并赋值给 vm.$options,这样的话 vm.$options 就会有一个 __proto__ 属性,它的值就是 options 的内容。
接着把 子组件的父vnode(options._parentVnode) 和 子组件的父Vue实例(options.parent),都赋值到 vm.$options 上,然后把组件创建时候的一些配置也赋值给 vm.$options,最终合并出来的 vm.$options 就有了:
vm.$options = {
parent: Vue /*父Vue实例, options.parent*/,
propsData: undefined,
_componentTag: undefined,
_parentVnode: VNode /*父VNode实例, options._parentVnode*/,
_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: '123'
}
},
template: '<div>{{msg}}</div>'
}
}
总结
对于 new Vue 是通过 mergeOption 合并的,对于组件是通过 initInternalComponent 合并的,而 initInternalComponent 合并比较简单,所以它的合并更快。