Vue Options及其内部属性详解
在阅读源码的时候,经常被options和$options搞晕,写一篇笔记来记录Vue2.x中options的构造过程及两者之间的区别和联系。
options
- Vue.options: 全局options
- Sub.options: 子类构造函数的options
- vm.$options: 实例对象的options
Vue.options
这是一个全局对象,运用了原型链继承
的方式,构造子类的实例对象时,实例对象的options继承了Vue.options
,Vue源码中提供了不同版本的入口,我们以runtime运行时版本为例,入口文件在项目目录src/platforms/web/entry-runtime.js
下,只关注options的相关代码
构造过程 从入口开始,向上寻找Vue构造函数的初始定义,再反向查看Vue的options发生了哪些变化,下面先不关注Vue的原型链,只关注Vue的引入路线和静态属性options的变化
// 入口 1. src/platforms/web/entry-runtime.js
import Vue from './runtime/index'
export default Vue
// 2. ./runtime/index
import Vue from "core/index";
import platformDirectives from "./directives/index";
import platformComponents from "./components/index";
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);
// 3. core/index
import Vue from "./instance/index";
import { initGlobalAPI } from "./global-api/index";
// 为Vue扩展全局静态方法
initGlobalAPI(Vue);
//4. ./instance/index
function Vue(options) {
if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
warn("Vue is a constructor and should be called with the `new` keyword");
}
this._init(options);
}
我们找到了core/instance/index
中定义了Vue构造函数本身,现在来反向查看Vue静态属性发生的变化,分析一下上面的源码,可以提取三句核心代码,首先调用initGlobalAPI
初始化Vue的全局API,然后调用extend扩展Vue.options.directives
和Vue.options.components
,extend
是一个最简单的属性覆盖函数
// core/index
initGlobalAPI(Vue);
// ./runtime/index
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);
initGlobalAPI
这个函数初始化了Vue的静态方法,比如Vue.use
,Vue.extend
,Vue.mixin
,Vue.nextTick
等……,提取options相关逻辑,global中初始化的options属性都是基本属性,与平台无关
// ASSET_TYPES=['component','directive','filter']
// builtInComponent={keepAlive}
Vue.options = Object.create(null);
ASSET_TYPES.forEach((type) => {
Vue.options[type + "s"] = Object.create(null);
});
Vue.options._base = Vue;
extend(Vue.options.components, builtInComponents);
// Vue.options = {
// components: { keepAlive },
// directives: {},
// filters: {},
// _base: Vue,
// }
注入平台相关的指令和组件
// ./runtime/index
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);
// 全局options
// Vue.options = {
// components: {
// keepAlive,
// Transition,
// TransitionGroup
// },
// directives: {
// modal,
// show
// },
// filters: {},
// _base: Vue,
// }
初始情况下Vue.options是一个很简单的对象,目的是为了储存全局组件、指令等,Vue提供了一些静态方法来扩展基础构造函数的options,也比较好理解,贴上源码
// global-api/assets.js
import { ASSET_TYPES } from "shared/constants";
import { isPlainObject, validateComponentName } from "../util/index";
// ASSET_TYPES=['component','directive','filter']
export function initAssetRegisters(Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach((type) => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + "s"][id];
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && type === "component") {
validateComponentName(id);
}
// 全局注册组件的方法
if (type === "component" && isPlainObject(definition)) {
definition.name = definition.name || id;
// 函数内部的this指向Vue
// 调用Vue.extend(extendOptions)生成子类构造器
definition = this.options._base.extend(definition);
}
// e.g: Vue.directive('show', show)
if (type === "directive" && typeof definition === "function") {
definition = { bind: definition, update: definition };
}
// 把扩展后的对象添加到Vue.options中
this.options[type + "s"][id] = definition;
return definition;
}
};
});
}
验证
Sub.options
子构造函数的options
其实比较好理解,(一般来说)它继承自Vue.options
,源码目录core/global-api/extend
,我只提取部分options相关逻辑,这个函数不复杂,就是构建了一个比较基本的继承关系,并且处理了一些全局函数,感兴趣的可以自己看一看源码
传入的options:一个对象,包含了data
、props
、inject
、provide
、watch
、computed
、components
、directives
等等你在开发时会用到的属性
举个🌰,我们常写这段代码,就是导出了一个options
,Vue内部会调用Vue.extend
,传入这个对象来生成一个构造函数
// a.vue
export default {
name: "AComponents",
data() {},
props: [],
computed: {},
...
}
源码如下:
// core/global-api/extend
Vue.extend = function(expandOptions) {
extendOptions = extendOptions || {};
const Super = this;
const SuperId = Super.cid;
// ...
// 经典的继承
const Sub = function VueComponent(options) {
// 相当于 Vue.call(this)
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.cid = cid++;
// 合并options
Sub.options = mergeOptions(Super.options, extendOptions);
Sub["super"] = Super;
// 把自己也添加到options.components
// 提供组件的递归调用功能
if (name) {
Sub.options.components[name] = Sub;
}
// ...
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = extend({}, Sub.options);
// ...
return Sub
}
mergeOptions
是Vue内部一个比较重要的合并函数,它定义了一系列的合并规则strats
,按照不同的策略去合并不同的参数,并返回一个新的options
// core/util/options
function mergeOptions(parent, child, vm) {
// ...
//返回新的options
const options = {};
let key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
// strats[key] key属性的合并函数
function mergeField(key) {
const strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options;
}
源码中为每个(或每几个)属性都定义了合并函数strat
,这里提取出关于全局属性的合并代码,使用Object.create
函数来实现简单的原型链继承,让子组件也能访问到全局定义的components
、directives
和filters
// core/util/options
// ASSET_TYPES=['component','directive','filter']
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;
}
}
ASSET_TYPES.forEach(function (type) {
strats[type + "s"] = mergeAssets;
});
验证
// 定义另一个子组件,验证components的合并规则
const comp = {
name:"comp"
}
// 子构造函数
const Sub = Vue.extend({
props: ['list', 'type'],
data() {
return {
a: 1
}
},
computed: {
validList() {
return this.list.filter(item => item.visible)
}
},
components: {comp},
directives: {
focus: {
inserted(el){
el.focus()
}
}
}
})
打印结果如下,可以看到Sub.options.components和Sub.options.directives的原型链上都添加了全局定义的属性,props
、data
、computed
等属性的合并规则这里就不贴了,感兴趣可以去阅读源码查看
vm.$options
实例对象的options:实际上由其构造函数的options
以及传入的options
构成
$options的初始化过程:组件的构造函数Sub在实例化时传入一个options参数,会跟父构造函数的options进行合并
// _init函数
Vue.prototype._init = function(options) {
//...
// vm.$options初始化
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
在实例化的过程中又调用了一次mergeOptions
,来合并实例对象与构造函数的options,因为Sub.options是一个静态属性,在new Sub(options)
时不会将options继承给vm,要手动调用mergeOptions
来新建一个实例化对象独有的$options
resolveConstructorOptions
函数会去获取当前vm构造函数的options
进行合并,也就是Sub.options
但是为什么不直接这样写呢:
const Sub = vm.constructor
vm.$options = mergeOptions(
Sub.options,
options || {},
vm
);
一般开发过程中,基本不会有什么问题,但是如果你调用Vue Api去构造组件,这样写就会出问题,举个🌰:
// 先创建构造函数
const Sub = Vue.extend({
data() {},
props: [],
components: {}
...
})
// 然后使用全局混入mixin,mixin也是调用了mergeOptions来合并对象
// Sub并不知道Vue.options改变了,所以这个directives无法应用到Sub上
Vue.mixin({
directives: {
...
}
})
// 再实例化vm,这个实例化也无法应用全局mixin的directives
const vm = new Sub()
应该很少有人写这样的代码,全局混入函数一般在入口文件就会调用,不得不佩服Vue考虑的周全性
总结
理解源码之后会发现Vue.options、Sub.options、vm.$options实际上就是一个继承链的关系,源码中的关键的函数mergeOptions
使用策略模式对不同的属性进行合并,感兴趣的可以看一看完整代码,相信会对你有很大的帮助