决定跟着黄轶老师的 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.js3处打个断点:
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配置,来达到定制化不同需求的目的。