案例
<body>
<div id="app"></div>
</body>
<script src="vue.js"></script>
<script>
let childComp = {
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(childComp)
})
</script>
initGlobalAPI
当我们加载 vue 文件的时候,就执行了 initGlobalAPI(Vue) 方法:
function initGlobalAPI(Vue) {
// 像 Vue 定义了只读属性 config 和 util、set、nextTick 等属性
...
Vue.options = Object.create(null);
// 像 options 里添加了 components、directives、filters 三个空对象
ASSET_TYPES.forEach(function (type) {
Vue.options[type + 's'] = Object.create(null);
});
// 将 Vue 初始方法存储起来,在 _render 阶段会用于构造子类构造函数
Vue.options._base = Vue;
// 把一些内置组件 `<keep-alive>`、`<transition>` 和 `<transition-group>` 扩展到 `Vue.options.components` 上
extend(Vue.options.components, builtInComponents);
...
// 将 mixin 的内容合并到配置中
initMixin()
}
合并配置
此时,传入的 options 将作为 mergeOptions 的第二个参数 child 传入,为:
{
el: "#app"
render: h => h(childComp)
}
安装代码的执行,在 _init() 方法中我们会合并配置,不是组件的情况下,会执行下面的方式进行合并:
if (options && options._isComponent) {
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm);
}
resolveConstructorOptions 方法返回的就是我们在 initGlobalAPI 方法中定义的 options 属性:
{
components: {}
created: [ƒ] // console.log('parent created')
directives: {}
filters: {}
_base: ƒ Vue(options)
}
这个将作为 mergeOptions 的第一个参数 parent 传入。
非组件合并
function mergeOptions(parent, child, vm) {
// 格式化 child 的属性,递归把 `extends` 和 `mixins` 合并到 `parent` 上
...
var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
// 对不同的 `key` 有着不同的合并策略
function mergeField(key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options;
}
这段逻辑中比较重要且难以理解的是 mergeField 方法,我们先看一下 strats 对象里面的内容:
components: ƒ mergeAssets(parentVal, childVal, vm, key)
computed: ƒ (parentVal, childVal, vm, key)
created: ƒ mergeLifecycleHook(parentVal, childVal)
data: ƒ (parentVal, childVal, vm)
el: ƒ (parent, child, vm, key)
props: ƒ (parentVal, childVal, vm, key)
watch: ƒ (parentVal, childVal, vm, key)
...
这里面包含了 vue 中可接收的 option 的名称,并且对于不同的 key 有着不同的处理合并的方法,比如生命周期的方法使用的就是 mergeLifecycleHook 方法,component、directives 和 filters 使用 mergeAssets 方法...
mergeLifecycleHook
我们以合并生命周期为例,看看是怎么进行合并的:
function mergeLifecycleHook(parentVal, childVal) {
var res = childVal
? parentVal
? parentVal.concat(childVal)
: isArray(childVal)
? childVal
: [childVal]
: parentVal;
return res ? dedupeHooks(res) : res;
}
parentVal 传入的时候不是 undefined 就是数组,childVal 可能是 undefined、string 或者数组,经过这个合并,如果 parentVal 和 childVal 都有值,则会将两个值合并到一个数组中。
组件合并
此案例中传入 initInternalComponent 方法的 options 为:
parent: Vue, // div#app 父Vue实例
_isComponent: true,
_parentVnode: VNode // vue-component-1 父VNode实例
initInternalComponent 方法首先执行 Object.create 使用现有的对象来作为新创建对象的原型,这里的 vm.constructor 就是子组件的构造函数 Sub。
function initInternalComponent(vm, options) {
var opts = (vm.$options = Object.create(vm.constructor.options));
// 简单一层对象赋值
var parentVnode = options._parentVnode;
opts.parent = options.parent;
opts._parentVnode = parentVnode;
...
}
这里的 vm 是一个 vue组件类型的实例,它的 Prototype 是 Vue 类型的实例,其中很多 options 定义在 Prototype 上,我们访问这些属性的时候由于作用域链,所以和访问 vm 上的属性一致。
最后得到的 vm.$options 的值差不多是如下这样:
vm.$options = {
parent: Vue,
propsData: undefined,
_componentTag: undefined,
_parentVnode: 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>'
}
}