组件化的合并配置 - 学习vue源码系列 3.2
决定跟着黄轶老师的 vue2 源码课程好好学习下vue2
的源码,学习过程中,尽量输出自己的所得,提高学习效率,水平有限,不对的话请指正~
将vue 的源码clone 到本地,切换到分支2.6
。
Introduction
组件化渲染的那块,我个人觉得挺复杂的,我还没有捋顺,我决定先往后看,看完整体,再来磕硬骨头。
其实 vue 也有点配置为王的感觉,它的配置就是options
。
后期组件的各种相关操作,都围绕options
展开。
本篇着重看看,vue
是怎么处理options
的,最最重点的是,怎么合并各个options
的。
先看 demo
尝试先自己想想,这些钩子函数里的这些打印顺序如何。
<div id="app"></div> <script src="/Users/zhm/mygit/vue/dist/vue.js"></script> <script> const log = console.log; // 全局mixin Vue.mixin({ created() { log("mixin created"); }, }); // 子组件实例 let childCompInstance = null; // 子组件 let childComp = { name: "MSG", template: "<div>{{msg}}</div>", created() { childCompInstance = this; log("child created"); }, data: () => ({ msg: "Hello Vue" }), }; // 根组件实例 let vueInstance = new Vue({ el: "#app", created() { log("parent created"); }, render: (h) => h(childComp), }); log("Vue构造器上的options", Vue.options); log("Vue实例的options", vueInstance, vueInstance.$options); log( "VueComponent实例的options", childCompInstance, childCompInstance.$options, childCompInstance.$options.__proto__ ); </script>
公布答案:
mixin created parent created mixin created child created
其实定义在 Vue 的 mixin 里的options
,会合并Vue
构造器的的options
里面。
用构造器创建实例的时候,构造器上面的options
会和实例的options
再次合并。
组件构造器是 Vue 的子类,合并的时候,主要的options
在childCompInstance.$options.__proto__
里。
看下,例子里后面的打印:
Vue.mixin 和 Vue 构造器的 options 合并
先说下Vue.mixin
是个函数,其传入的options
,首先和 Vue 构造器上面的options
合并。
Vue.mixin
执行之后,Vue 构造器上面的options
就有了其对应的值。
先来个超简单的示意:
/** vue源码 开始 */ function Vue(options) { } Vue.options = {}; // 将两个options合并成一个options const mergeOptions = (parentOptions, childOptions) => { let options = {}; // 生命周期的钩子合并的时候都是数组 for (let key in childOptions) { if (key === "created") { const parentCreated = Array.isArray(parentOptions.created) ? parentOptions.created : parentOptions.created ? [parentOptions.created] : []; const childCreated = Array.isArray(childOptions.created) ? childOptions.created : [childOptions.created]; options.created = [...parentCreated, ...childCreated]; } } return options; }; Vue.mixin = function mixin(options) { this.options = mergeOptions(Vue.options, options); return this }; /** vue源码 结束 */ // 使用的例子 Vue.mixin({ created() { console.log("mixin created"); }, }); // {created:[x]} console.log(Vue.options);
总的来说就是做着上面的事情,然后看 vue 的真正源码
// src/core/global-api/mixin.js export function initMixin(Vue: GlobalAPI) { Vue.mixin = function (mixin: Object) { // mixin就是{ created(){} }, this指的是Vue构造器,因为使用的时候是Vue.mixin this.options = mergeOptions(this.options, mixin); return this; }; }
// src/core/util/options.js export function mergeOptions( parent: Object, child: Object, vm?: Component ): Object { // parent就是Vue构造器的options,child就是{ created(){} } if (process.env.NODE_ENV !== "production") { checkComponents(child); } if (typeof child === "function") { child = child.options; } normalizeProps(child, vm); normalizeInject(child, vm); normalizeDirectives(child); // 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); } } } const options = {}; let key; // parent就是Vue构造器上面的options,key就是每项键,key不一样合并的策略不一样,先处理parent for (key in parent) { mergeField(key); } // child就是 { created(){} },在处理child里的(parent没有的)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; }
Vue 实例的时候
Vue 构造器上面的options
会和实例里参数options
合并。 当然合并的逻辑是相似的。 拿这个例子来说,会变成created:[fn1,fn2]
,注意构造器的在前面,参数的在后面。
// options就是类似例子的{el:'#app',....} Vue.prototype._init = function (options) { var vm = this; // 将各种选项合并,合并之后就是{created:[fn1,fn2]} vm.$options = mergeOptions( // 这里vm.constructor就是Vue,Vue.options{created:[fn1]} resolveConstructorOptions(vm.constructor), // {created:fn2} options || {}, vm ); };
这里可以简单看下Vue上面的静态属性options
const ASSET_TYPES = [ 'component', 'directive', 'filter' ] 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 // 将内置组件扩展到Vue.options.components上,keepAlive transition extend(Vue.options.components, builtInComponents) // ... }
VueComponent实例的时候
由于组件的构造函数是通过Vue.extend
继承自Vue
的。
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 }
很显然,组件类的options
是组件对象和Vue
的options
合并的。
组件初始化的时候,
// options有以下属性 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
就是指向 Vue.extend
的返回值 Sub
。执行new
的话,就会再次执行this._init(options)
.
export function initInternalComponent (vm: Component, options: InternalComponentOptions) { // 组件类的options和Vue类的options并不相同。 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
方法执行,相当于 vm.$options = Object.create(Sub.options)
。 此时,vm.$options
如下:
vm.$options = { parent: Vue /*父Vue实例*/, propsData: undefined, _componentTag: undefined, _parentVnode: VNode /*父VNode实例*/, _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.js
3处打个断点:
Vue.mixin
打个断点Vue.prototype._init
里options
处打个断点function mergeOptions
打个断点
先看看mixin:
Vue.mixin
之后,Vue的options合并了Vue.mixin
的参数,变成这样:
{ components: {} created: [ƒ] directives: {} filters: {} _base: ƒ Vue(options) }
new Vue(options)
会再和Vue的options
合并,VueComponent
的options
也会和Vue的options
合并:
vue实例的$options
是用户传的options
和Vue
的options
的合并,此时vue实例的$options
:
{ components: {} created: (2) [ƒ, ƒ] directives: {} el: "#app" filters: {} render: (h) => h(childComp) _base: ƒ Vue(options) _isVue: true _uid: 0 }
VueComponent组件实例的options是和Vue的options合并:
显然组件的合并是先走了Vue.extend
,然后走了initInternalComponent
,最后,组件实例的options就是
_proto__:{ components: {MSG: ƒ} created: (2) [ƒ, ƒ] data: () => ({ msg: "Hello Vue" }) directives: {} filters: {} name: "MSG" template: "<div>{{msg}}</div>" _Ctor: {0: ƒ} } parent: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …} propsData: undefined render: ƒ anonymous( ) staticRenderFns: [] _componentTag: undefined _parentListeners: undefined _parentVnode: VNode {tag: "vue-component-1-MSG", data: {…}, children: undefined, text: undefined, elm: div, …}
总结
Vue 初始化阶段对于 options 的合并有 2 种方式:
- 外部初始化 Vue 通过
mergeOptions
的过程,合并完的结果保留在vm.$options
中。 - 子组件初始化过程通过
initInternalComponent
方式,比mergeOptions
的方式要快
纵观一些库、框架的设计几乎都是类似的,
- 自身先定义了一些默认配置,
- 同时又可以在初始化阶段传入一些自定义配置,
- 然后
merge
配置,来达到定制化不同需求的目的。