我们可通过options去配置vue的功能,那么options到了vue内部到底作何处理?
⚠️ options在vue内部进行相当复杂的合并和初始化操作,采用化整为零的方式,将这一大块拆成两部分讲解,将在第二章(本章)和第三章进行分析,本章重点讲解option的合并过程。
引言:试想一个场景,在开发项目过程中往往会用到Vue的全局api - mixin,很神奇的是全局混入的属性我们可以在任意一个组件中通过
this.属性名
进行访问,就好像这些属性定义到了子组件中,那么vue是如何达到这种效果的呢?合并!
如果组件内部混入了一些属性要如何和组件本身的options进行合并呢(局部混入)?同样也是通过合并。
用语声明:
- 占位节点:下列代码中cpn就是一个占位节点,这个节点接受父子通讯的数据。
- 渲染节点:占位节点将渲染式的数据传递给渲染节点,渲染节点就是将要渲染到真实dom中的样子。
- prop属性:v-bind绑定的属性将会解析成$props
- attr属性:没有被v-bind绑定的属性将会被解析成$attrs
- native方法:被native修饰的方法
- emit方法:组件上未添加native修饰符的方法
<template>
<cpn :propName='' attrName='' @click.native = '' @touchend='' />
</template>
<script>
import cpn from './component/Cpn'
export {
component:{
cpn
}
}
<script>
从传入opts到合并opts共经历以下几步:
- Vue 构造器
- Vue.prototype._init()
- mergeOptions()
- initInternalComponent()
- resolveConstructorOptions()
Vue构造器
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);
}
Vue的构造器及其简单,首先通过instanceof
进行原型链检查,判断当前是不是通过new vue调用的function Vue ,如果不是就在开发环境下提示用户必须要new Vue,检查过后直接执行原型方法_init(options)
_init() 处理vm实例
上一章中说过,_init()不单单是处理用户new Vue,也处理了vue内部创建子组件的过程,
这个方法包含了opts处理的很多步骤,包括了处理从父组件传递下来的props listener等等数据,合并处理全局资源或者是通过Vue.mixin混入的全局资源,为数据注册响应式等等操作。接下来对这几个步骤进行一一分析。
mergeOptions() 合并配置 核心方法
首先明确mergeOptions的目的:兼容并包,将Vue.options作为根合并到每个组件构造器上,将全局混入的属性为每个子组件可用,讲局部混入的属性为当前组件可用。
function mergeOptions(parent, child, vm) {
if (typeof child === 'function') {
child = child.options;
}
normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);
如果当前传入的child事function类型的,就代表传入了一个构造器,直接去取其options属性作为child
parent
待合并的options,将会合并到新对象上去。child
待合并的options,将会合并到新对象上去。normalize[asssetsName](child)
标准化资源名,以props为例,在子组件定义props的时候,可以定义数组也可以定义对象,而vue内部在使用的时候是通过同一种处理方式做处理的,正是因为有这一步,用户可以使用更灵活的api去定义props。vue在很多地方都使用了这种方式,像是插槽,用户手写的render函数等等,都是为了用户使用起来更方便。
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm);
}
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
}
child._base
其实就是Vue构造器 这个_base属性会在mergeOptions之后被添加到child上边去。这里主要是为了排除已经mergeOption之后的child不再去进行混入内容的合并了。- 当子组件内部定义了minxins的时候,会优先把混入的内容添加到parent中去,这么做的目的是降低mixin的优先级,因为假设mixin中定义了data,组件本身也定义了data,vue希望做到的是组件内定义的data数据优先级更高,去覆盖掉mixin的data,所以先要把mixin的数据添加到parent上边去。(递归)
var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField(key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}
var options = {}
可以看到这声明了一个新的对象options并返回它,parent和child上边的属性都将合并到上边去。for(key in ...)
然后循环把parent和child上边的属性都完成一次合并,这里做了一层筛选,如果在parent中遍历过的就不会在child中遍历了。mergeField(key)
内部属性大多都有自己的合并策略,像是钩子函数的合并,data的合并,props的合并等等,没有的就会采用默认策略defalutStart
,将所有的属性遍历完之后就完成了整个options的合并。- 这里还有一个小细节,在Vue官方的api中提供了了
Vue.config.optionMergeStrategies
,这个api可以来指定一些自定义属性是如何合并的,这一点在vuex中有应用(官网有提示)。
⚠️ 浅谈合并策略 此部分不是主干内容 不感兴趣可以跳过
strats.data = strats.provide
:
//mergeDataOrFn
mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
// mergeData-code
if (!hasOwn(to, key)) { // to就代表childData
set(to, key, fromVal); // fromVal就代表parentData中的某个属性值
} else if (
toVal !== fromVal &&
isPlainObject(toVal) && // toVal就代表childData中的某个属性值
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal); // 如果两个值都是对象 就进行深层的合并
}
data可能传入方法或者对象,如果是方法就得到方法返回的的对象后进行mergeData操作。mergeData操作是首先判断子data上边有没有该属性,如果没有就直接设置当前属性到子data上去,如果子上边已有当前属性就判断当前属性是不是对象类型的,如果是对象类型的就进行深层递归合并。(provide和data合并策略一致,只不过少了一步function类型的校验)
strats.props = strats.methods = strats.inject = strats.computed
:
var ret = Object.create(null);
extend(ret, parentVal) // 简单的合并 parentVal上边的属性添加到ret
if (childVal) { extend(ret, childVal); } // 同名属性覆盖
同名属性child覆盖parent。
strats.watch
:
if (!childVal) { return Object.create(parentVal || null) }
if (!parentVal) { return childVal }
var ret = {};
extend(ret, parentVal);
for (var key$1 in childVal) {
var parent = ret[key$1];
var child = childVal[key$1];
if (parent && !Array.isArray(parent)) { // 创建数组
parent = [parent];
}
ret[key$1] = parent
? parent.concat(child) // child-wathcer添加到child后边
: Array.isArray(child) ? child : [child];
}
return ret
如果没有child-watcher就通过原型链去继承parent-wathcer并返回继承后的空对象。如果没有parent-watcher就直接返回child-watcher。如果两个都有,首先创建一个空对象将parent-watcher添加其中,接下来遍历child-watcher,把同名wathcer都添加到一个数组中,值得注意的是parent-wathcer在前,child-wathcer在后。在添加child-wathcer到最终数组ret的时候还有一个细节,使用的[].concat
方法而不是[].push
,因为在定义watcher的时候可以传如一个数组,里面包裹着属性变化后需要触发的方法列表,如果push还需要深度遍历,这样做的话就是一个一维数组。
strats[hook]
:生命周期钩子,
var res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal;
return res
? dedupeHooks(res) // 进行方法去重
: res
首先判断有没有child,没有直接取parent。如果存在child钩子,就通过concat进行合并,parent在前,child在后,如果没有parent就直接用数组包裹child。最终通过dedupeHooks进行同名钩子去重就是当前钩子合并的结果。
strats.components = strats.filters = strats.directives
。
var res = Object.create(parentVal || null);
if (childVal) {
return extend(res, childVal)
} else {
return res
}
组件,指令,过滤器都算是静态资源,他们采取同样的合并策略。首先通过原型链继承去创建一个空对象讲parent中的资源继承过来,接下来开始child的遍历,简单吧child中的属性复制到这个空对象上,最终返回这个对象
其他属性
:
var defaultStrat = function (parentVal, childVal) {
return childVal === undefined
? parentVal
: childVal
};
默认策略,如果child传了取child丢弃parent,否则取parent。
Vue.extend 和 Vue.mixin 对全局options的操作
上文提到,在处理用户传入的options的时候要与组件构造器的option进行一次合并。那么组件构造器的option是怎么来的呢?这里提前了解一点(后续创建组件vm的过程中会详细解释整个流程),vue内部在创建子组件vm实例的时候必先要通过extend去创建子组件构造器,这就需要去看Vue.extend的方法实现。
Vue.extend = function (extendOptions) { // 这里的options就是单文件组件中export defalut的options
extendOptions = extendOptions || {};
var Super = this;
var Sub = function VueComponent(options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype); // 通过原型链继承Super 就可以访问到Vue的原型方法了 $emit $on之类的
Sub.prototype.constructor = Sub;
Sub.options = mergeOptions( // ⚠️ 就是这里将全局或者父options和用户传入的options进行合并操作的
Super.options,
extendOptions
);
Sub['super'] = Super; // 持有父构造器的引用
Sub.extend = Super.extend; // 子构造器也可以继续创建子子构造器
Sub.mixin = Super.mixin;
Sub.use = Super.use;
ASSET_TYPES.forEach(function (type) { // 直接吧静态资源拷贝过来
Sub[type] = Super[type];
});
Sub.superOptions = Super.options; // 记录本次构造时父构造器传入的options 后续在进行option更新会有用处 resolveConstructorOptions方法中
Sub.extendOptions = extendOptions; // 记录本次构造时用户传入的options
Sub.sealedOptions = extend({}, Sub.options);
return Sub
};
- 整个Vue.extend过程中最重要的步骤就是mergeOptions,使得子构造器持有了全局/父级的options也持有了用户传入的options。
- 另外注意一点,为什么说子构造器持有了全局***或者父级***呢?这里有一句代码
Sub.extend = Super.extend
,很明显子构造器把Vue.extend复制来了,所以子构造器Sub也可以通过extend方法区创建属于自己的子构造器了,也就是孙子构造器,这样一来就又合并了一次options,所以孙子构造器的Super很明显不是Vue,而是Sub,就这样子子孙孙无穷无尽… 这一点在更新全局options要格外注意,是一个深度遍历和回溯的过程(在resoloveConstructorOptions过程中)。当然在常规开发中,大家很少用子构造器再去搞一些新的孙子出来,vue提供这种强大的功能供一些ui库去使用。
在谈论完Vue.extend就可以看看Vue.mixin了,它很简单。看如下代码
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options, mixin);
return this
};
短短两行代码,就通过mergeOptions完成了mixin操作,不过要特别注意,mixin把this.options(也就是全局options)和用户传入需要混入的options进行了一次合并,这样也就解释了为什么全局混入的属性可以在每个子组件中可以调用到了。
- 说到这里,简单梳理一下整个options链条关系。
- 首先Vue.mixin 改变了全局的options。
- 全局options和组件内部export的options合并到子组件构造器的options上。
- 子组件构造器的options挂到vm. o p t i o n s 的 原 型 链 上 。 至 此 , 便 可 以 在 开 发 时 候 通 过 t h i s . options的原型链上。 至此,便可以在开发时候通过this. options的原型链上。至此,便可以在开发时候通过this.options访问到mixin,全局,和组件特有的options了。(当然开发过程中我们不会通过$options的方式去访问属性,直接this调用可以获取到数据的原因是vue内部为我们做了一层代理并注册了响应式。)
initInternalComponent 初始化子组件options
⚠️ 这里直接将initInternalComponent中的内容展开,在后续父子组件创建过程的章节中还会涉及到,到时候可以再翻到这里来看。对于初次new Vue 这里的代码是执行不到的,可以先简单了解一下对于子组件的处理,对比和初次new Vue的区别,带着问题反复阅读源码印象会更更加深刻,不喜欢的道友可以先跳过。
if (options && options._isComponent) {
initInternalComponent(vm, options);
}
查看如上代码中的判断条件 options._isComponent
,这个变量是占位节点创建vm实例时进行赋值的,拥有这个变量的options就代表是组件的options, 是区分当前是用户主动new Vue还是Vue内部创建组件实例的标志,对于内部创建组件,会执行内部代码。最终得到子组件的options。接下来详细解析这个方法:
function initInternalComponent(vm, options) {
var opts = vm.$options = Object.create(vm.constructor.options);
首先为当前自组件的vm通过原型链去创建一个$options
,这个$options.__proto__
指向的是子组件构造器的options。
- 对于子组件构造器的options(
vm.constructor.options
),由两部分mergeOptions
而来(这个合并具体过程在上边已经详细分析过了).- 一是
Vue.options
这是每个vm实例都可以访问到的options,也就是全局options - 二是子组件
export default
导出的这个对象,这个对象是子组件特有的options,(这个对象已经被vue-loader
转化成了一个options对象,vue-loader
将模版部分转化成render函数导出给父组件)。
- 一是
- 然后这个合并后的options就可以被子组件的
vm.$options
访问到,又因为合并的options是Vue.options
和子组件的options
合并来的,所以可以通过$options.__proto__[属性名]
的方式访问到全局Vue.options
和子组件export
出来的options`。
var parentVnode = options._parentVnode;
opts.parent = options.parent;
opts._parentVnode = parentVnode;
这里的options是作为参数传入的,这个options里面的内容是占位节点传递过来的。
_parentVnode
:是子组件在父组件中的占位节点。parent
:是父组件的vm实例
把这两项添加到opts上边去,供子组件去访问。
var vnodeComponentOptions = parentVnode.componentOptions;
opts.propsData = vnodeComponentOptions.propsData;
opts._parentListeners = vnodeComponentOptions.listeners;
opts._renderChildren = vnodeComponentOptions.children;
opts._componentTag = vnodeComponentOptions.tag;
componentOptions
: 承载了父子通讯相关的$props,$attrs,$listener
等等信息。propsData
:从占位节点上获取到的props绑定值listeners
:从占位节点上获取到的监听的非native方法children
:需要渲染的子vnode数组tag
:标签名
if (options.render) {
opts.render = options.render;
opts.staticRenderFns = options.staticRenderFns;
}
}
- 判断调用时的options有没有传入render方法,如果有,就使用传入的render方法进行赋值。
简单总结一下
initInternalComponent
- 有三个options
一是全局options ,
二是子组件export default 的 options ,
三是子组件在创建vm实例的时候占位节点传入的options 。- 在创建子组件vm实例的时候需要用到上述所有信息。这一步相当于一种标准化,为后续内容作准备。
resolveConstructorOptions 获取构造器上的options
⚠️ 本方法也是分为vue内部调用子组件_init和用户使用new Vue(opts)两种情况,对于初次new Vue() 就会判断直接跳过。
这个方法是为了检测用户在vue代码运行期间有没有改变Vue.options上边的全局options,比如下边这种骚操作,定义一个点击就全局混入data的方法,这会改变全局options,这时候就需要对新增的data数据进行处理。
methods: {
clickMix(a,e) {
Vue.mixin({
data(){
return {
test:'111'
}
}
})
}
}
var options = Ctor.options;
if (Ctor.super) { // Vue或者VueComponent
var superOptions = resolveConstructorOptions(Ctor.super);
var cachedSuperOptions = Ctor.superOptions;
if (superOptions !== cachedSuperOptions) {
Ctor.superOptions = superOptions;
var modifiedOptions = resolveModifiedOptions(Ctor); // 获取更新的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.super本质上就是 Vue构造器或者某个组件构造器,上边解释了
Vue.extend
的过程中也把Vue.extend方法也赋予给了子构造器,所以子构造器可以继续构建自己的子构造器,这样就形成了一个链式关系。(上边说了) - 首先通过循环的方式无限向上查找,找到最根部的Vue.options,也就是用户可能操作的options。然后和上次通过
Vue.extend
生成的组件构造器时保存的options进行比对,如果不等,就代表用户一定是改变过Vue.options,接下来要做的就是把变化更新到每一级构造器上。 - 因为首先是通过循环逐级想上寻找,所以接下来就是向下层层回溯,更新每一层
Sub.extend
生成的构造器。 - 通过整个过程保证了每一个构造器上的options都是最新的。保证混入对后续组件中的操作生效。
- resolveModifiedOptions方法很简单,就是把变化了的属性提取出来,最终把变了的属性通过mergeOption合并到子组件export的options上。
结
在进行数据初始化和注册响应式之前现将数据按照优先级合并,最终得到合并对象,这样就完美的处理了options的关系。同时也对可能变化的options进行了精准更新。整个options链复杂又巧妙,值得深入理解和剖析。
《完》