您的位置 首页 > 腾讯云社区

Vue源码探秘(十一)(合并options)---前端森林

引言

在上一篇文章的结尾,我们提到在 _init 的最初阶段执行的就是 merge options 的逻辑:

// src/core/instance/init.js // 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 ); }

可以看到,合并 options 分两种情况,它们的区别是什么呢。

区别就是在执行用户编写的 new Vue(options) 时就会执行 else 逻辑,而执行内部的 new Vue(options)(比如创建子组件实例)时就会走 if 逻辑。

这一节我们就围绕下面这个例子来研究这两种情况下合并 options 分别是怎么执行的:

import Vue from "vue"; let childComponent = { 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"); } }); let app = new Vue({ el: "#app", render: h => h(childComponent) });

例子中使用了Vue.mixin函数,是因为mixin本身就是合并 options 的过程,来看 Vue.mixin 的定义:

// src/core/global-api/mixin.js 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 的内部实现就是调用了 mergeOptions 函数,把 mixin 中的内容合并到 Vue.options 上。

外部调用场景

我们先分析执行外部 new Vue(options) 时的情况,这时会走 else 逻辑:

vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm );

这里调用了 resolveConstructorOptions 函数并传递了 vm.constructor 作为参数。resolveConstructorOptions 函数定义在 src/core/instance/init.js 文件中:

// src/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; 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; }

这里的 if 语句通过 Ctor.super 判断 Ctor 是 Vue 还是 Vue 的子类,显然在我们的例子中是 Vue ,因此 if 中的逻辑不会执行。所以 resolveConstructorOptions 函数直接返回 Vue.options 。

那这个 Vue.options 又是从哪里来的呢,实际上它在 initGlobalAPI 函数内被定义:

// src/core/global-api/index.js 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 后遍历 ASSET_TYPES 数组往 Vue.options 添加属性,ASSET_TYPES 定义如下:

export const ASSET_TYPES = ["component", "directive", "filter"];

之后又添加了 _base 属性。此时 Vue.options 大概是这个样子的:

Vue.options = { components: {}, directives: {}, filters: {}, _base: function Vue(options) {} };

最后通过 extend(Vue.options.components, builtInComponents)把一些内置组件扩展到 Vue.options.components 上,Vue 的内置组件目前有 <keep-alive>、<transition> 和 <transition-group> 组件,这也就是为什么我们在其它组件中使用 <keep-alive> 组件不需要注册的原因,这块儿后续我们介绍 <keep-alive> 组件的时候会详细讲。

了解完 resolveConstructorOptions 后,我们分段来分析 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); // ... }

mergeOptions 函数的 child 参数对应的就是用户编写的 options 。这里首先调用 checkComponents(child) 来检查 options.components 组件名称是否合法:

/** * Validate component names */ function checkComponents(options: Object) { for (const key in options.components) { validateComponentName(key); } }

然后执行一系列 normalize 函数进行规范化操作。这一段代码不是本节重点,在这里不会细讲。接着看下一段:

export function mergeOptions( parent: Object, child: Object, vm?: Component ): Object { // ... // Apply extends and mixins on the child options, // but only if it is a raw options object that isn't // the result of another mergeOptions call. // Only merged options has the _base property. 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); } } } // ... }

最外层的 if 语句表明这是对未合并的 options 的处理,因为注释提到了只有已合并的 options 才有 _base 属性。

if 中的逻辑就是递归调用 mergeOptions 函数,将 parent 分别和 child.extends、child.mixins 合并,最后的结果赋给 parent。

可以看到上面这两段代码都是在处理 parent 和 child 参数,mergeOptions 函数核心逻辑是接下来这一段:

export function mergeOptions( parent: Object, child: Object, vm?: Component ): Object { // ... 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 对象的属性并调用 mergeField 函数,然后又遍历了 child 对象的属性,如果 child 对象的属性在 parent 中没有定义,同样也要调用 mergeField 函数。

mergeField 函数首先定义了 strat , strat 实际上也是个函数,它的取值有两个来源,我们先看这个 defaultStrat 的定义:

// src/core/util/options.js const defaultStrat = function(parentVal: any, childVal: any): any { return childVal === undefined ? parentVal : childVal; };

defaultStrat 的逻辑很简单,有 childVal 就用 childVal ,没有就用 parentVal 。我们再来看 strats 的定义:

/** * Option overwriting strategies are functions that handle * how to merge a parent option value and a child option * value into the final value. */ const strats = config.optionMergeStrategies;

这里 strats 的值是全局配置对象 config 的 optionMergeStrategies 属性,其实就是个空对象。从注释我们可以看出来,strats 就是各种选项合并策略函数的集合,用来合并父 options 和子 options。

我们先来分析一下生命周期函数的合并策略:

// src/shared/constants.js export const LIFECYCLE_HOOKS = [ "beforeCreate", "created", "beforeMount", "mounted", "beforeUpdate", "updated", "beforeDestroy", "destroyed", "activated", "deactivated", "errorCaptured", "serverPrefetch" ];// src/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; } LIFECYCLE_HOOKS.forEach(hook => { strats[hook] = mergeHook; });

可以看到,LIFECYCLE_HOOKS 定义了所有生命周期函数名,这些都会作为 strats 的属性名,所有属性对应的属性值都是 mergeHook 这个函数。

在mergeHook的最后对res还调用dedupeHooks进行了处理,来看下dedupeHooks函数:

// src/core/util/options.js 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; }

其实就是数组去重处理,也就是将res中相同的钩子函数去掉。

到这里也就印证了我们上面的猜测:strats 就是各种选项合并策略函数的集合。回到 mergeOptions 函数:

export function mergeOptions( parent: Object, child: Object, vm?: Component ): Object { // ... 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; }

mergeField 函数的下一步操作是将 parent 和 child 中的 key 合并到 options 中,值是调用对应的合并策略返回的结果。合并完成后 mergeOptions 函数将 options 返回出去。

这样我们就把合并 options 的 else 逻辑走了一遍。回顾我们在本节中举的例子,在经过合并操作后大概是这样子的:

vm.$options = { components: {}, created: [ function created() { console.log("parent created"); } ], directives: {}, filters: {}, _base: function Vue(options) { // ... }, el: "#app", render: function(h) { //... } };

而我们例子中的组件 childComponent 的 options 合并处理走的是 if 逻辑,接下来我们就来分析这种情况。

执行内部组件构造函数

来看 if 逻辑的代码:

// optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options);

if 逻辑直接调用了 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.$options = Object.create(vm.constructor.options) ,这里的 vm.constructor.options 也就是子构造函数的 options 属性,它是什么时候定义的呢。

回顾Vue源码探秘(九)(createComponent),子构造函数是通过 Vue.extend 创建的:

// src/core/global-api/extend.js Vue.extend = function(extendOptions: Object): Function { // ... const Sub = function VueComponent(options) { this._init(options); }; Sub.options = mergeOptions(Super.options, extendOptions); // ... };

可以看到,Sub.options 就是由 Vue.options 和 组件 options 通过 mergeOptions 合并的结果。

接着又把实例化子组件传入的子组件父 VNode 实例 parentVnode、子组件的父 Vue 实例 parent 保存到 vm.$options 中,另外还保留了 parentVnode 配置中的 propsData、listeners 等属性。

所以initInternalComponent 函数的逻辑其实很简单,就是做了一层对象赋值而已。对应我们的例子,在执行了这个 if 逻辑后大概是这样子的:

vm.$options = { parent: app, _parentVnode: VNode, propsData: undefined, _componentTag: undefined, _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: "Hello Vue" }; }, template: "<div>{{msg}}</div>" } };总结

那么到这里,Vue 初始化阶段对于 options 的合并过程就介绍完了,options 的合并有两种方式:

执行外部 new Vue 时,会调用 mergeOptions 函数,并根据不同的选项调用不同的合并策略函数子组件实例化时,会调用 initInternalComponent 函数进行合并

下一节我们一起来看下生命周期部分的源码实现。

---来自腾讯云社区的---前端森林

关于作者: 瞎采新闻

这里可以显示个人介绍!这里可以显示个人介绍!

热门文章

留言与评论(共有 0 条评论)
   
验证码: