系列文章
- [Vue源码学习] new Vue()
- [Vue源码学习] 配置合并
- [Vue源码学习] $mount挂载
- [Vue源码学习] _render(上)
- [Vue源码学习] _render(下)
- [Vue源码学习] _update(上)
- [Vue源码学习] _update(中)
- [Vue源码学习] _update(下)
- [Vue源码学习] 响应式原理(上)
- [Vue源码学习] 响应式原理(中)
- [Vue源码学习] 响应式原理(下)
- [Vue源码学习] props
- [Vue源码学习] computed
- [Vue源码学习] watch
- [Vue源码学习] 插槽(上)
- [Vue源码学习] 插槽(下)
前言
从上一章节中我们知道,在调用_init方法初始化Vue实例的过程中,首先会进行配置合并的操作:
/* core/instance/init.js */
Vue.prototype._init = function (options?: Object) {
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
)
}
}
可以看到,Vue会根据不同的条件,进行非组件的配置合并和组件的配置合并,那么接下来,我们就分别看看它们内部是如何实现的。
非组件的配置合并
当直接使用new Ctor()的方式创建实例时,就会调用mergeOptions方法,进行非组件的配置合并,代码如下所示:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
可以看到,mergeOptions方法接收三个参数,其中第一个参数是调用resolveConstructorOptions方法返回的,其内部逻辑之后会单独介绍,这里可以简单的看成返回Ctor.options,那么接下来,我们就来看看mergeOptions方法的实现:
/* 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
}
// 1. 规范化props、inject、directives选项
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// 2. 将extends、mixins选项中的配置合并到parent中
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)
}
}
}
// 3. 根据策略函数处理选项
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方法中,首先调用normalize方法规范化props、inject、directives选项,将用户传入的数据处理成规范的格式;然后递归地调用mergeOptions方法,将extends、mixins选项中的配置合并到parent中,相当于对组件做了扩展,同时从这里可以看出,extends和mixins选项的处理逻辑是相同的,mixins相当于包含多次extends;在完成了前两步的初始化工作后,接下来就开始执行真正的配置合并的逻辑。
在合并的时候,首先遍历传入的配置对象,根据不同的选项从strats中取出对应的合并策略函数,然后将parent中的配置和child中的配置通过策略函数进行合并,最后就可以得到此选项合并后的结果了。接下来就来看看常见的选项是如何进行合并的。
lifecycle
/* shared/constants.js */
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
/* core/util/options.js */
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
可以看到,所有生命周期的策略函数都是mergeHook,其代码如下所示:
/* core/util/options.js */
function mergeHook(
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
function dedupeHooks(hooks) {
const res = []
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i])
}
}
return res
}
mergeHook会将parent和child中的生命周期钩子函数做合并操作,返回一个新的数组,其中parent中的钩子函数在前,child中的钩子函数在后,最后使用dedupeHooks方法过滤掉重复的钩子函数。
components、directives、filters
/* shared/constants.js */
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
/* core/util/options.js */
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
可以看到,components、directives、filters选项的策略函数都是mergeAssets,其代码如下所示:
/* core/util/options.js */
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
}
}
mergeAssets首先会以parentVal为原型创建一个新的实例,然后把childVal合并到这个实例上,之所以使用原型继承的方式,是因为components、directives、filters这些资源相关的选项,在多个实例之间是可以复用的,这样就不用把这些数据单独的添加到各个实例上了。
data、provide
/* core/util/options.js */
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
// ...
return mergeDataOrFn(parentVal, childVal, vm)
}
strats.provide = mergeDataOrFn
export function mergeDataOrFn(
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
// ...
return function mergedInstanceDataFn() {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
可以看到,对于非组件的配置合并,调用data选项的策略函数会返回一个新的函数mergedInstanceDataFn,它会在initState的过程中执行。执行时,首先从parent和child中取到各自的数据,然后调用mergeData方法合并这两项数据:
/* core/util/options.js */
function mergeData(to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
const keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
// in case the object is already observed...
if (key === '__ob__') continue
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) {
// 对于to中不存在的数据,从from中取出后添加到to中
set(to, key, fromVal)
} else if (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
// 值是对象,需要递归处理
mergeData(toVal, fromVal)
}
}
return to
}
可以看到,mergeData就是一个深度合并的过程,主要是用来将from中的数据合并到to中,所以首先遍历from对象,如果to中不存在该数据,就将数据直接添加到to中;如果to中存在该数据,并且它们对应的值还是一个对象,就继续调用mergeData,对数据进行深度递归,否则,就不做任何操作,还是以to中的数据为准,最终可以得到一个以to为主的对象,这样就可以把child和parent中的数据合并起来了。
props、methdos、inject、computed
/* core/util/options.js */
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
if (childVal && process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
return ret
}
可以看到,这些合并策略很简单,就是浅合并parentVal和childVal,并且childVal中的值可以覆盖parentVal中的值。
默认策略
对于没有策略函数的选项来说,就会使用默认策略,代码如下所示:
/* core/util/options.js */
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
默认策略就是如果在child中定义了选项,就直接取child中的值,否则,就取parent中的值。
在各种选项经过对应的策略函数处理后,将最终的合并结果赋值给vm.$options,这样就完成了非组件的配置合并。
那么接下来,就来看看对于组件来说,它是如何合并的。
组件的配置合并
一般来说,组件的配置合并首先会发生在创建组件VNode的过程中,在调用createComponent方法时,会调用Vue.extend方法,将组件配置对象转换成组件构造器,其代码如下所示:
/* core/global-api/extend.js */
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
// 首先尝试从缓存中获取组件构造器
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
// 定义组件构造器
const Sub = function VueComponent(options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 配置合并
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// 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)
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
可以看到,extend方法首先会检查缓存中是否存在对应的子构造器,如果存在就直接返回,如果不存在,就先创建了一个子构造器Sub,然后使用原型继承的方式使Sub继承Super,接着调用mergeOptions方法,将Super.options和组件配置进行合并,然后将合并后的结果添加到Sub.options中,合并完成后,将Super上的静态方法添加到Sub上,比如extend、mixin、use、component、directive、filter等,此时,Sub构造器就拥有了类似Super构造器的能力,然后将Sub自己添加到Sub.options.components中,这样在组件中就可以递归调用自己。最后将构造出的子构造器Sub缓存到组件配置对象中,下一次就可以直接从缓存中取出子构造器,而不用重新构建了。
通过Vue.extend创建完子构造器后,此时还没有创建子组件的实例,当父组件执行patch,准备挂载组件的父占位符节点时,会调用createComponentInstanceForVnode方法创建子组件的实例,代码如下所示:
/* 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 // 父vm实例
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
可以看到,在createComponentInstanceForVnode方法中,首先会构建组件的配置选项,_isComponent表示当前是组件的配置合并,其余的_parentVnode和parent分别表示组件的父占位符节点和父vm实例,然后通过vnode.componentOptions.Ctor,也就是上面的Sub构造器,创建子组件的实例,最终还是调用_init方法,只是这次传入的选项是Vue内部构造的,接下来,我们来看看对于组件的配置合并,_init内部是如何工作的:
/* core/instance/init.js */
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)
}
可以看到,对于组件来说,会调用initInternalComponent方法进行配置合并,代码如下所示:
/* 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
}
}
在initInternalComponent方法中,首先以Sub.options为原型创建一个实例,然后将各种与当前实例相关的propsData、listeners、children等属性添加到该实例上,可以看到,在创建子组件实例时,由于没有做mergeOptions操作,所以配置合并的速度是很快的。
resolveConstructorOptions
在前面的两小节中,我们已经知道Vue是如何合并配置的了,其实在它们的内部,都会执行resolveConstructorOptions方法,用来检查构造器上的options是否需要更新,代码如下所示:
/* core/instance/init.js */
export function resolveConstructorOptions(Ctor: Class<Component>) {
let options = Ctor.options
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
// 缓存的配置和实际的配置不同时,需要重新做mergeOptions操作
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
}
在resolveConstructorOptions方法中,如果检测到子构造器中的superOptions与父构造器的options不相同,则说明在创建完子构造器后,父构造器的配置选项进行过更新,所以需要调用mergeOptions方法,重新创建子构造器的options,所以在创建实例之前,通过调用resolveConstructorOptions方法,就可以保证在创建实例时,构造器上的options选项是最新的了。
总结
在Vue中,每个实例的$options不仅仅来自于我们编写的配置,它还会用不同的策略函数,与组件构造器上的配置进行合并,同时,子组件只会在第一次构造组件构造器时执行mergeOptions操作,之后就可以高效的创建子组件的实例了。