【手写 Vue2.x 源码】第二十七篇 - Vue 生命周期的实现

624 阅读7分钟

一,前言

上篇,主要介绍了数组依赖收集的实现:

  • 对象依赖收集的总结;
  • 数组依赖收集的位置;
  • 数组和对象本身做依赖收集;
  • 数组中嵌套对象(对象或数组)的递归处理;

本篇,Vue 生命周期的实现


二,Vue.mixin 介绍

1,mixin 简介

Vue2中,能够通过Vue.mixin方法,对框架进行功能的扩展;

比如:在实际开发中,使用mixin为所有的组件增加一些生命周期;

常见面试题:Vue的生命周期是如何实现的?

2,mixin 使用

beforeCreate为例:在new Vue初始化时,可以通过beforeCreate生命周期函数,指定实例创建前要执行的逻辑;

如果需要对这部分逻辑继续进行扩展,就可以使用Vue.mixin;

备注: 在实际执行时,多个beforeCreate函数会进行合并;

3,生命周期的用法

// 使用 Vue.mixin 进行全局扩展
Vue.mixin({
  beforeCreate(){
    console.log("全局:mixin-beforeCreate")
  }
})

let vm = new Vue({
  el: '#app',
  
  // 用法一:
  // beforeCreate(){},
  
  // 用法二:数组写法:逻辑较多或需要进行分类时,可以拆分为多个函数
  beforeCreate:[
    function(){
      console.log("局部:new Vue-beforeCreate 1") // A 模块初始化
    },
    function(){
      console.log("局部:new Vue-beforeCreate 2") // B 模块初始化
    }
  ]
});

三,Vue 的 Global API

1,全局 api 和 实例 api 的使用

// 全局 api:对所有组件生效
Vue.component()

// 实例 api:仅对当前组件生效
new Vue({
  component:{}
})

全局 api 和 实例 api 的区别:

  • 全局 api,对所有组件生效;
  • 实例 api,仅对当前组件生效;

2,全局 api 的实现原理

通过Vue.mixinVue.componentVue.filterVue.component声明的全局属性,将全部被存放到全局属性Vue.options中,当每一个组件,通过new Vue进行初始化时,Vue.options中的全局属性会与每一个组件中声明的实例 api进行合并,从而实现全局 api效果;


四,Vue.mixin 实现

1,添加 mixin 方法

创建Vue全局api模块:src/global-api

新建src/global-api/index.js,为Vue添加mixin静态方法:

//src/global-api/index.js

export function initGlobalAPI(Vue) {
  Vue.mixin = function (options) {
    
  }
}

src/index.js中,即Vue初始化阶段调用,执行Vue Global Api功能的初始化:

// src/index.js

import { initGlobalAPI } from "./global-api";
import { initMixin } from "./init";
import { lifeCycleMixin } from "./lifecycle";
import { renderMixin } from "./render";

function Vue(options){
  this._init(options);
}

initMixin(Vue)
renderMixin(Vue)
lifeCycleMixin(Vue)
initGlobalAPI(Vue)   // 初始化 global Api,扩展 Vue.mixin 功能

export default Vue;

2,实现 Global API

在全局属性Vue.options中,存放属性提供全局使用:

// src/global-api/index.js

export function initGlobalAPI(Vue) {
  // 全局属性:Vue.options
  // 功能:存放 mixin, component, filter, directive 属性
  Vue.options = {}; 
  
  Vue.mixin = function (options) {}
  Vue.component = function (options) {}
  Vue.filter = function (options) {}
  Vue.directive = function (options) {}
}

3,多个 Vue.mixin 的合并策略

同一个生命周期钩子,可以通过Vue.mixin进行多次全局声明:

Vue.mixin({
  beforeCreate(){
    console.log("全局:mixin-beforeCreate 1")
  }
})

Vue.mixin({
  beforeCreate(){
    console.log("全局:mixin-beforeCreate 2")
  }
})

此时,需要对全局声明进行合并:

Vue.mixin = function (options) {
    // 将每次传入的 options 与全局属性 Vue.options 进行合并
    // merge...
}

合并策略:

第一次合并:
parentVal:{}
childVal:{ beforeCreate:fn1 }
合并结果:{ beforeCreate:[fn1] }

第二次合并:
parentVal:{ beforeCreate:[fn1] }
childVal:{ beforeCreate:fn2 }
合并结果:{ beforeCreate:[fn1,fn2] }

每次合并时,需要循环父亲(老值)和儿子(新值),依次进行合并

src/utils.js,添加工具方法mergeOptions

// src/utils.js

/**
 * 对象合并:将 childVal 合并到 parentVal 中
 * @param {*} parentVal   父亲-老值
 * @param {*} childVal    儿子-新值
 */
export function mergeOptions(parentVal, childVal) {
  let options = {};
  
  // 1,先合并父亲中已存在的老 key
  for(let key in parentVal){
    mergeFiled(key);  // 父亲、儿子的相同 key 进行合并
  }
  
  // 2,再处理儿子中新添加的新 key
  for(let key in childVal){
    // 当新值存在,老值不存在时:添加到老值中
    if(!parentVal.hasOwnProperty(key)){
      mergeFiled(key);
    }
  }
  
  function mergeFiled(key) {
    // 默认合并策略:优先使用新值覆盖老值
    options[key] = childVal[key] || parentVal[key]
  }
  
  return options;
}

合并逻辑:

  • 1,先合并父亲中已存在的老key
  • 2,再处理儿子中新添加的新key;
  • 默认合并策略:优先使用新值覆盖老值

4,生命周期合并策略的实现

生命周期可能存在很多种,对于不同生命周期的合并,不能单靠if(key == 'beforeCreate') 判断来进行区分...

这里,可以使用策略模式:当不同生命周期进行合并时,使用不同的策略进行区分;

// src/utils.js

// 存放所有策略
let strats = {};  

// 全部生命周期
let lifeCycle = [ 
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted'
  ...
];

// 遍历执行每一种生命周期的合并
lifeCycle.forEach(hook => {
  // 为不同生命周期,创建合并策略
  strats[hook] = function (parentVal, childVal) {
    // ******* 1,儿子有值,需要进行合并 *******
    if(childVal){ 
      // ---> 1)父亲儿子都有值:父亲有值就一定是数组,将儿子合入父亲即可
      if(parentVal){
        return parentVal.concat(childVal);  
      // ---> 2)儿子有值,父亲没有值:儿子放入新数组中
      }else{
        // 如果传入的生命周期函数是数组,则无需再包装成数组
        if(Array.isArray(childVal)){
          return childVal;
        }else{
          return [childVal];
        }
      }
    // ******* 2,儿子没有值,无需合并 *******
    }else{
      return parentVal; // 返回父亲即可
    }
  }
})

initGlobalAPI初始化全局 API,混入Vue.mixin函数中,调用mergeOptions合并方法,返回this提供链式调用;

// src/global-api/index.js

export function initGlobalAPI(Vue) {

  // 全局属性:Vue.options
  // 功能:存放 mixin, component, filter, directive 属性
  Vue.options = {}; 
  
  Vue.mixin = function (options) {
    this.options = mergeOptions(this.options, options);
    console.log("打印mixin合并后的options", this.options);
    
    return this;  // 返回this,提供链式调用
  }
  
  Vue.component = function (options) {}
  Vue.filter = function (options) {}
  Vue.directive = function (options) {}
}

5,测试多个 Vue.mixin 合并

测试Vue.mixin中的生命周期合并结果:

image.png

第一次合并了 1 个,第二次又合并了 1 个,共 2 个;


五,全局与实例的生命周期合并

全局生命周期合并完成后,还要继续与new Vue初始化中的局部声明(用户传入的 options选项)再进行一次合并,从而实现全局 api 与实例 api 的合并;

new Vue初始化时,会进入Vue的原型方法_init

// src/init.js#initMixin

Vue.prototype._init = function (options) {
    
    const vm = this;
    
    // Vue 初始化时(这里有可能是子组件),用户传入的 options 需要与父类合并
    // vm.constructor:获取到构造函数(这里的构造函数可能是子类的),通过vm.constructor可以获取到子类的构造函数;
    vm.$options = mergeOptions(vm.constructor.options, options);
    
    ...
}

打印vm.$options,查看合并后的结果:

image.png

问题:vm.constructor.optionsVue.options的区别?

这里的vm,可能是Vue的子类,对Vue进行过增强,比如:子组件会继承自Vue

  • Vue.options:就是Vueoptions选项;
  • vm.constructor:是指子类(子组件)的构造函数;

六,生命周期的实现

1,创建生命周期执行函数

src/lifecycle.js生命周期模块中,创建执行生命周期的钩子函数callHook

// src/lifecycle.js

/**
 * 执行生命周期钩子
 *    从$option获取对应生命周期函数(数组)并执行
 * @param {*} vm    vue实例
 * @param {*} hook  生命周期
 */
export function callHook(vm, hook){

  // 获取生命周期对应函数数组
  let handlers = vm.$options[hook];
  
  if(handlers){
    handlers.forEach(fn => {
      fn.call(vm);  // 使生命周期函数中的 this 指向 vm 实例
    })
  }
}

注意:生命周期函数中的 this,必须永远指向 vm 实例;

  • 静态方法 Vue.mixin 一定是父类 Vue 调用的;如果是子组件调用就会写成 Child.mixin;this 的指向是十分明确的;
  • 但是,实例方法,可能是子类调用的,也可能是父类调用的;

2,添加生命周期钩子

在不同生命周期阶段,触发对应的钩子函数:

  • 视图渲染前,调用钩子: beforeCreate
  • 视图更新后,调用钩子: created
  • 视图挂载完成,调用钩子: mounted
// src/lifecycle.js

export function mountComponent(vm) {

  let updateComponent = ()=>{
    vm._update(vm._render());  
  }

  // 当视图渲染前,调用钩子: beforeCreate
  callHook(vm, 'beforeCreate');
  
  new Watcher(vm, updateComponent, ()=>{
    // 视图更新后,调用钩子: created
    callHook(vm, 'created');
  },true)

   // 当视图挂载完成,调用钩子: mounted
   callHook(vm, 'mounted');
}

这里的实现方式,类似发布订阅模式:在流程中的具体位置,调用函数执行;

todo:补充其他生命周期的调用位置

当然还有,生命周期函数的其他调用位置,比如:

  • 执行watcher.run()视图更新前,调用钩子函数: beforeUpdate
  • 视图更新完成后,调用钩子函数: updated;
// src/scheduler.js

/**
 * 刷新队列:执行所有 watcher.run 并将队列清空;
 */
function flushschedulerQueue() {

  // 视图更新前,执行生命周期:beforeUpdate
  
  queue.forEach(watcher => watcher.run()) // 依次触发视图更新
  queue = [];       // reset
  has = {};         // reset
  pending = false;  // reset
  
  // 视图更新完成,执行生命周期:updated
}

3,测试生命周期执行流程

  • Vue.mixin中的2beforeCreate钩子;
  • new Vue中的2beforeCreate钩子;

按照合并后的顺序依次执行完成:

image.png


七,结尾

本篇,介绍了 Vue 生命周期的实现,主要涉及以下几点:

  • Vue.mixin 介绍和使用;
  • Vue 全局 api 和实例 api 的使用和实现;
  • Vue.mixin 的说明和实现;
  • 生命周期的说明和实现;

下篇,diff 算法的流程分析


维护日志:

  • 20210708:修复“四-4,生命周期的合并策略”,当生命周期函数为数组时,无需二次包装;
  • 20210806:修复排版问题;
  • 20230210:更新文章摘要;
  • 20230212:添加了大量的补充说明,添加代码注释,调整了文章排版,添加内容中的代码高亮,更新文章摘要;