组件化-合并配置
在之前的文章中我们讲过,初始化 Vue 实例有两种方法,一种是手动调用 new Vue(options) 的方法,二是上节讲到的调用 new vnode.componentOptions.Ctor(options) 的方式初始化。
无论哪种方式都会执行到 _init(options) 方法,然后执行 merge options 的逻辑,相关代码定义在 src/core/instance/init.js 中:
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
);
}
// ...
};
下面我们通过一个 🌰 来看一下两种场景的不同合并过程:
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");
},
});
new Vue({
el: "#app",
render: (h) => h(childComp),
});
外部调用场景
一: 首先执行
Vue.mixin({
created() {
console.log("parent created");
},
});
Vue.mixin 方法是在 initGlobalAPI 方法中给 Vue 构造函数赋值上的,具体代码为:
import { mergeOptions } from "../util/index";
export function initMixin(Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin);
return this;
};
}
可以看到 Vue.mixin 接收一个 Object,然后调用 mergeOptions 合并配置。
二:
执行 new Vue(options),调用 this._init(options),然后就执行 _init 函数里的如下逻辑来合并 options:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
mergeOptions 方法实际上就是传入两个 Object,然后做合并。resolveConstructorOptions(vm.constructor) 暂时不考虑,在这里它只是返回了 vm.constructor.options,相当于是 Vue.options,那么这个值是什么,其实它也是在 initGlobalAPI 方法执行的时候定义的,代码如下:
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);
// ...
}
这段代码的结果用代码表示就是:
Vue.options = {
components: {
KeepAlive: {
name: "keep-alive",
// ...
},
},
created: [],
directives: {},
filters: {},
_base: Vue,
};
另外通过 extend(Vue.options.components, builtInComponents) 把 Vue 内置的组件(如 KeepAlive )扩展到 Vue.options.components 中。这也是我们后面使用 KeepAlive 不用注册的原因,关于组件注册后面再看。
回到 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;
}
// 标准化 props、inject、directive 选项
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);
}
}
// 合并字段,child 选项将覆盖子选项
function mergeField(key) {
// strats 或者 defaultStrat 是个合并策略,即到底用父的还是用子的
const strat = strats[key] || defaultStrat;
// 优先使用 child 子选项的值
options[key] = strat(parent[key], child[key], vm, key);
}
return options;
}
mergeOptions 主要做了几件事:
- 递归的把
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;
}
[
"beforeCreate",
"created",
"beforeMount",
"mounted",
"beforeUpdate",
"updated",
"beforeDestroy",
"destroyed",
"activated",
"deactivated",
"errorCaptured", // 2.5.0 新增的钩子
].forEach((hook) => {
strats[hook] = mergeHook;
});
这里定义了 Vue 中的所有生命周期钩子函数,mergeHook 执行的结果就是把 child 和 parent 中定义的比如 created(){} 合并为一个数组并返回。
其他策略定义都可以在 src/core/util/options.js 看到,这里不一一细看了。
因此,我们最后执行完合并配置后:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
vm.$options 的值应该为:
vm.$options = {
components: {}
created: [
function created() {
console.log('parent created')
}
]
directives: {}
el: "#app"
filters: {}
render: function render(h) {}
_base: function Vue(options) {}
}
组件调用场景
我们在之前了解到组件的构造函数是通过 Vue.extend 继承自 Vue 的,具体代码定义在 src/core/global-api/extend.js 中:
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 就是我们定义的组件对象 let childComp = { //... },他会和 Vue.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);
}
在这里 vnode.componentOptions.Ctor = Sub,所以执行的下一步就是调用 _init(options) 方法。因为 _isComponent = true,所以合并配置走到 initInternalComponent(vm, options),initInternalComponent 定义在 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;
}
}
initInternalComponent 主要做了下面几件事:
- 执行
const opts = vm.$options = Object.create(vm.constructor.options),也就是vm.$options = Object.create(Sub.options) - 把实例化子组件传入的子组件
父 VNode实例parentVnode、子组件的父 Vue 实例parent保存到vm.$options中 - 保留了
parentVnode配置中的如propsData等其它的属性
并没有像 mergeOptions 一样递归、合并策略等逻辑。
因此执行完 initInternalComponent 之后,vm.$options 的值应该为:
vm.$options = {
parent: Vue, // 父Vue实例
propsData: undefined,
_componentTag: undefined,
_parentListeners: undefined,
_renderChildren: undefined,
_parentVnode: VNode, // 父VNode实例
__proto__: {
components: {},
created: [
function created() {
console.log("parent created");
},
function created() {
console.log("child created");
},
],
data() {
return {
msg: "Hello Vue",
};
},
directives: {},
filters: {},
mounted: [
function mounted() {
console.log("child mounted");
},
],
template: "<div>{{msg}}</div>",
_Ctor: {},
_base: function Vue(options) {
//...
},
},
};
总结
本文总结了两种初始化 Vue 的合并配置过程,我们只要知道对于 options 有两种合并方式,组件通过调用 initInternalComponent 比外部调用 Vue 初始化的过程要快,合并完的结果存放在 vm.$options 中。
纵观一些库、框架的设计几乎都是类似的,自身定义了一些默认配置,同时又可以在初始化阶段传入一些定义配置,然后去 merge 默认配置,来达到定制化不同需求的目的。只不过在 Vue 的场景下,会对 merge 的过程做一些精细化控制,虽然我们在开发自己的 JSSDK 的时候并没有 Vue 这么复杂,但这个设计思想是值得我们借鉴的。 - Vue.js 技术揭秘
参考:Vue.js 技术揭秘