【手写 Vue2.x 源码】第三十七篇 - 组件部分 - 组件合并的实现

204 阅读4分钟

一,前言

上篇,介绍了 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 并且把全局属性合并到子类中; image.png image.png image.png

当前 mergeOptions 方法中,尚未对组件的合并进行处理;(应该合并到 components 对象中,目前是按照对象进行合并的)

  • 组件的构造函数被保存到全局 Vue.options.components 中: image.png
  • 模拟创建组件实例,会执行 _init 组件的初始化流程: image.png
  • _init 方法中,会再次执行 mergeOptions 进行合并; image.png

本篇,主要就来介绍在 mergeOptions 中,组件合并策略的实现;(全局组件和局部组件的合并)

Vue.component 的作用:定义全局组件,并维护到全局Vue.options.components中; Vue.extend 的作用:根据入参 extendOptions 对象,创建并返回一个继承自 Vue 的子类,并将全局选项合并到子类上;


三,组件合并的分析

1,组件合并的必要性

实现了组件核心的两步 Vue.componentVue.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 方法: image.png

添加注释的代码:

// 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.optionsoptions 进行合并:

  • 使用 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.optionsoptions 的状态,如下:

  • 在全局组件 vm.constructor.options.components 中,存放的组件是函数(即组件的构造函数); image.png
  • 在局部组件options.components中,存放的组件并不是函数,而是对象! image.png

问题:由于全局组件和局部组件需要进行合并,而此时,它们一个是函数一个是对象,会有问题吗?

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 方法中,会找到预置的组件合并策略函数: image.png

组件合并:此时,

  • 参数 parentVal(父亲)是一个函数 f
  • 参数 childVal (儿子)是一个对象 {...}image.png

生成的新对象 res,可以在链上拿到 parentVal 中的全局组件: image.png

继续,再将儿子也全部合并到新生成的res对象上: image.png

这样,就完成了组件的合并:在 res 上查找组件时,会优先查找局部组件,若没有找到,则会继续通过链向上查找全局组件;

注意:在这里,全局定义的组件是函数,局部定义的组件是对象;

所以,在所有的子组件中都能够访问到全局组件的原因:在组件初始化时,会将全局组件和当前组件通过 mergeOptions 进行合并;核心代码就是这一句:

// 全局定义的内容,会被混合在当前实例上
vm.$options = mergeOptions(vm.constructor.options, options);

备注:Vue 的全局指令、过滤器等实现原理也完全一样;都是将全局定义的内容,混合到当前实例上完成的;


六,结尾

本篇,介绍了组件部分-组件合并的实现,主要涉及以下几个点:

  • 组件注册流程梳理;
  • 组件合并的分析;
    • 组件合并的必要性;
    • 组件合并的问题;
  • 组件合并的实现;
    • 生命周期的合并策略-策略模式回顾;
    • 组件合并策略的实现;
  • 组件合并的测试;

下一篇,组件部分-组件的编译;


维护日志

  • 20210812:
    • 添加了少量的三级标题,使内容分布更加清晰;
    • 微调了部分描述与代码注释,有助于更好的理解;
    • 部分内容进行规整,形成无需列表,使思路的表述更加清晰易懂;
    • 添加了部分图示与实际测试结果;
  • 20230224:
    • 添加了内容中的代码和关键字高亮;
    • 优化了部分内容描述,使表述更加准确易懂;
  • 20230226:
    • 修正了在_init方法中,关于mergeOptions组件合并描述不够准确;
    • 添加了衔接上下文逻辑的提问;
    • 调整了部分内容,使描述更加准确;
  • 20230303:
    • 重新阅读本篇,梳理并调整了部分内容的描述,使表述和语义更加准确,更好理解;
  • 20230307:
    • 重新梳理相关知识点,并对本篇进行了重写;
    • 添加了若干截图和代码注释,使语义表述尽可能清晰准确;
    • 更新了文章摘要;