重学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
,返回了 Array
,也就是返回了一个 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
合并比较简单,所以它的合并更快。