一,前言
上篇,介绍了 Vue.extend 实现,主要涉及以下几个点:
- Vue.extend 简介;
- Vue.extend 实现的分析与代码框架;
- 组件初始化的分析与实现;
- 子类继承父类的分析与实现;
- Vue.extend 的功能测试;
- 组件的选项合并;
- Vue.extend 的补充说明;
本篇,组件部分 - 组件合并的实现;
二,组件注册流程梳理
- 通过
Vue.component
声明全局组件; - 组件定义
definition
为对象时,通过Vue.extend
处理为组件的构造函数Sub
; - 保存到全局
Vue.options.components
上; - 通过
new Sub
创建组件实例时,就会执行_init
组件的初始化流程;
测试组件的初始化流程(组件注册和实例化):
// dist/index.html
<script>
Vue.component('my-button', {
name:'my-button',
template:'<button>Hello Vue 全局组件</button>'
})
</script>
// src/global-api/index.js
export function initGlobalAPI(Vue) {
Vue.component = function (id, definition) {
let name = definition.name || id;
definition.name = name;
if(isObject(definition)){
definition = Vue.extend(definition)
}
Vue.options.components[name] = definition;
// new Sub 执行组件初始化
debugger
new Vue.options.components['my-button'];
}
Vue.extend = function (extendOptions) {
const Super = this;
const Sub = function (options) {
debugger
this._init(options);
}
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.options = mergeOptions(Super.options, extendOptions);
return Sub;
}
}
执行过程分析:
- 使用
Vue.component
声明全局组件,如果definition
为对象,进入Vue.extend
处理; Vue.extend
生成子类Sub
,继承 Vue 并且把全局属性合并到子类中;
当前
mergeOptions
方法中,尚未对组件的合并进行处理;(应该合并到components
对象中,目前是按照对象进行合并的)
- 组件的构造函数被保存到全局
Vue.options.components
中: - 模拟创建组件实例,会执行
_init
组件的初始化流程: - 在
_init
方法中,会再次执行mergeOptions
进行合并;
本篇,主要就来介绍在 mergeOptions
中,组件合并策略的实现;(全局组件和局部组件的合并)
Vue.component
的作用:定义全局组件,并维护到全局Vue.options.components
中;Vue.extend
的作用:根据入参extendOptions
对象,创建并返回一个继承自Vue
的子类,并将全局选项合并到子类上;
三,组件合并的分析
1,组件合并的必要性
实现了组件核心的两步 Vue.component
和 Vue.extend
之后,接下来,执行 new Vue
初始化;
组件合并的场景:声明一个全局组件和一个局部组件,且两个组件同名:
<script>
Vue.component('my-button', {
name:'my-button',
template:'<button>Hello Vue 全局组件</button>'
})
new Vue({
el: "#app",
components:{
'my-button':{
template:'<button>Hello Vue 局部组件</button>'
}
}
});
</script>
当执行 new Vue
时,入参 options.components
中定义的局部组件,有需要执行组件的初始化流程,即调用 Vue
原型上的 _init
方法:
添加注释的代码:
// src/init.js#initMixin
Vue.prototype._init = function (options) {
const vm = this; // this 指向当前 vue 实例
// ************** 合并 **************
// 此时,需要使用全局 options 与组件 options 再做一次合并
// 全局 options:即 vm.constructor.options(Vue.options 与 mixin 合并后的全局 options)
// 组件 options:声明组件时,用户传入的 options
// 实现效果:优先查找到局部组件,若不存在,再去全局查找
vm.$options = mergeOptions(vm.constructor.options, options);
// ************** 状态的初始化 **************
// 当前,在 vue 实例化时,传入的 options 中,只有 el 和 data 两个参数
initState(vm);
// ************** 挂载 **************
if (vm.$options.el) {
// 将数据挂在到页面上(此时,数据已经被劫持)
vm.$mount(vm.$options.el)
}
}
在 mergeOptions
方法中,将 vm.constructor.options
和 options
进行合并:
- 使用
Vue.component
完成全局组件的声明后,在vm.constructor.options
选项中,包含全局组件; - 当执行
new Vue
时,在用户传入的options
选项中,包含局部组件;
// vm.constructor.options:包含全局组件 components;
// options:用户传入的选项,包含局部组件 components;
vm.$options = mergeOptions(vm.constructor.options, options);
根据组件的优先级,当遇到同名组件时,优先查找局部组件,若未找到则继续沿着链向上查找全局组件;
所以,在组件的初始化时,需要对全局组件和局部组件进行一次合并操作;
2,组件合并的问题(函数 or 对象)
1)提出问题
通过 debugger
查看 mergeOptions
操作前,vm.constructor.options
和 options
的状态,如下:
- 在全局组件
vm.constructor.options.components
中,存放的组件是函数(即组件的构造函数); - 在局部组件
options.components
中,存放的组件并不是函数,而是对象!
问题:由于全局组件和局部组件需要进行合并,而此时,它们一个是函数一个是对象,会有问题吗?
2)原因分析
- 在全局组件定义
Vue.component
中,传入的第二个参数:组件定义definition
是一个对象,内部会被Vue.extends
包裹并处理为组件的构造函数; - 在局部组件定义
options.components
中,传入的是对象,内部并不会被Vue.extends
所处理,所以此处依然是一个对象;
<script>
// 全局组件-内部通过 Vue.extends 处理为构造函数
Vue.component('my-button',{
name:'my-button',
template:'<button>Hello Vue 全局组件</button>'
})
// 局部组件-不会被 Vue.extends 处理,仍是对象
new Vue({
el: "#app",
components:{
'my-button':{
template:'<button>Hello Vue 局部组件</button>'
}
}
});
</script>
备注:所有的组件,不管是全局组件还是局部组件,最终都会被
Vue.extends
处理成为组件的构造函数,只不过局部组件目前还没有被处理而已;(后续会介绍)
四,组件合并的实现
1,生命周期的合并策略-策略模式回顾
之前,在实现 mixin
生命周期的合并时,在核心方法 mergeOptions
中,就用到了策略模式:
- 针对不同的生命周期钩子,声明各自的合并策略;
- 在执行合并操作时,优先通过策略模式查找预置的合并策略,若没有找到,则使用默认策略:新值覆盖老值;
Vue.mixin
生命周期合并的核心实现:
// 存放生命周期的合并策略
let strats = {};
// 生命周期集合
let lifeCycle = ['beforeCreate', 'created', 'beforeMount', 'mounted'];
// 创建各种生命周期的合并策略
lifeCycle.forEach(hook => {
strats[hook] = function (parentVal, childVal) {
// 在 strats 策略对象中,定义并保存各种生命周期对应的合并策略
// 具体实现,省略...
}
})
/**
* 对象合并:将 childVal 合并到 parentVal 中
*
* @param {*} parentVal 父值-老值
* @param {*} childVal 子值-新值
*/
export function mergeOptions(parentVal, childVal) {
let options = {};
for(let key in parentVal){
mergeFiled(key);
}
for(let key in childVal){
// 当新值存在,老值不存在时:添加到老值中
if(!parentVal.hasOwnProperty(key)){
mergeFiled(key);
}
}
// 合并当前 key
function mergeFiled(key) {
// 策略模式:获取当前 key 的合并策略
let strat = strats[key];
if(strat){
options[key] = strat(parentVal[key], childVal[key]);
}else{ // 默认合并策略:新值覆盖老值
options[key] = childVal[key] || parentVal[key];
}
}
return options;
}
2,组件合并策略的实现
与 Vue.mixin
相似,组件的合并也可以通过策略模式,在 strats.component
中配置相应的合并策略,在 mergeOptions
方法中执行组件的合并操作时,按照预置的策略对新值、老值进行合并;
将 vm.constructor.options
(全局组件)和 options
(当前组件)进行合并,实现优先查找局部组件,找不到再查找全局组件;
// 注意:parentVal 为函数;childVal 为对象;
strats.component = function (parentVal, childVal) {
// 继承:使子类可以沿着链找到父类的属性 childVal.__proto__ = parentVal
let res = Object.create(parentVal); // Object.create 原型继承
// 如果儿子有值,全部合并到 res 对象上
if(childVal){
for (let key in childVal) {
res[key] = childVal[key];
}
}
return res;
}
合并完成后,res
上就有了儿子的属性,并且还可以通过链找到父亲;
实现思路:
- 新生成一个 res 对象;(不影响原来的对象)
- 将儿子上的属性合并到
res
对象上(优先找到儿子)- 让
res
继承父亲;(当儿子找不到时,可以通过链找到父亲)
五,组件合并的测试
测试组件合并:
<script>
// 全局组件
Vue.component('my-button',{
name:'my-button',
template:'<button>Hello Vue 全局组件</button>'
})
// 局部组件
new Vue({
el: "#app",
components:{
'my-button':{
template:'<button>Hello Vue 局部组件</button>'
}
}
});
</script>
在 mergeOptions
方法中,会找到预置的组件合并策略函数:
组件合并:此时,
- 参数
parentVal
(父亲)是一个函数f
; - 参数
childVal
(儿子)是一个对象{...}
;
生成的新对象 res
,可以在链上拿到 parentVal
中的全局组件:
继续,再将儿子也全部合并到新生成的res
对象上:
这样,就完成了组件的合并:在 res
上查找组件时,会优先查找局部组件,若没有找到,则会继续通过链向上查找全局组件;
注意:在这里,全局定义的组件是函数,局部定义的组件是对象;
所以,在所有的子组件中都能够访问到全局组件的原因:在组件初始化时,会将全局组件和当前组件通过 mergeOptions
进行合并;核心代码就是这一句:
// 全局定义的内容,会被混合在当前实例上
vm.$options = mergeOptions(vm.constructor.options, options);
备注:Vue 的全局指令、过滤器等实现原理也完全一样;都是将全局定义的内容,混合到当前实例上完成的;
六,结尾
本篇,介绍了组件部分-组件合并的实现,主要涉及以下几个点:
- 组件注册流程梳理;
- 组件合并的分析;
- 组件合并的必要性;
- 组件合并的问题;
- 组件合并的实现;
- 生命周期的合并策略-策略模式回顾;
- 组件合并策略的实现;
- 组件合并的测试;
下一篇,组件部分-组件的编译;
维护日志
- 20210812:
- 添加了少量的三级标题,使内容分布更加清晰;
- 微调了部分描述与代码注释,有助于更好的理解;
- 部分内容进行规整,形成无需列表,使思路的表述更加清晰易懂;
- 添加了部分图示与实际测试结果;
- 20230224:
- 添加了内容中的代码和关键字高亮;
- 优化了部分内容描述,使表述更加准确易懂;
- 20230226:
- 修正了在
_init
方法中,关于mergeOptions
组件合并描述不够准确; - 添加了衔接上下文逻辑的提问;
- 调整了部分内容,使描述更加准确;
- 修正了在
- 20230303:
- 重新阅读本篇,梳理并调整了部分内容的描述,使表述和语义更加准确,更好理解;
- 20230307:
- 重新梳理相关知识点,并对本篇进行了重写;
- 添加了若干截图和代码注释,使语义表述尽可能清晰准确;
- 更新了文章摘要;