手写Vue2源码(九)—— 混入原理与生命周期

342 阅读5分钟

前言

通过手写Vue2源码,更深入了解Vue;

在项目开发过程,一步一步实现Vue核心功能,我会将不同功能放到不同分支,方便查阅;

另外我会编写一些开发文档,阐述编码细节及实现思路;

源码地址:手写Vue2源码

Mixin混入原理

Vue.mixin可以往全局options中混入一些配置。 先思考一下,下列代码中的beforeCreate会如何合并?

Vue.mixin({
    beforeCreate() {
        console.log('beforeCreated1')
    }
})
Vue.mixin({
    beforeCreate() {
        console.log('beforeCreated2')
    }
})
let vm = new Vue({
    el: '#root',
    beforeCreate() {
        console.log('beforeCreated3')
    },
})

答案是:Vue.mixin会把options混入到全局的Vue.options;vm实例中的options会和全局/父类的options进行合并。最后的结果是{el: '#root', beforeCreate:[()=>{console.log('beforeCreated1')},()=>{console.log('beforeCreated2')},()=>{console.log('beforeCreated3')}]}生命周期会合并成一个数组。

下面我将一步一步实现options的合并以及生命周期的调用。

Vue.mixin()

思考一下实现思路:

  1. Vue有一个全局的配置,Vue.mixin(options)会把options与全局的配置进行合并
  2. 合并时包含生命周期、data、methods、components、computed等的合并,它们的合并方法可能不尽相同,需要考虑如何进行合并,以及代码的可扩展性
  3. 组件实例需要和全局options进行合并

先创建全局的options以及定义Vue.mixin()方法:

// src/index.js
import { initGlobalApi } from "./global-api/index";
function Vue(options) {
  this._init(options);
}
initGlobalApi(Vue);
export default Vue;
// src/global-api/index.js
import initMixin from "./mixin";
export function initGlobalApi(Vue) {
    // 每个组件初始化的时候都会和Vue.options选项进行合并
    Vue.options = {}; // 用来存放全局属性,例如Vue.component、Vue.filter、Vue.directive
    initMixin(Vue);
}
// src/global-api/mixin.js
import { mergeOptions } from '../util/index'
export default function initMixin(Vue) {
    Vue.mixin = function(mixin) {
        // this 指向 VUe,this.options即Vue.options
        // 将mixin合并到Vue.options中,而组件会和Vue.options合并,所以最后会把mixin合并到组件中
        this.options = mergeOptions(this.options,mixin)
        return this;
    }
}

总结一下:

  1. 全局的配置为 Vue.options,初始状态是空对象
  2. Vue.mixin()中调用mergeOptions(this.options,mixin)将mixin与全局的options进行合并

options合并的核心方法是mergeOptions()

mergeOptions()

直接看代码吧:

// src/util/index.js
const strategies = {} // 存放各种合并策略
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed'
]
LIFECYCLE_HOOKS.forEach((hook) => {
  strategies[hook] = mergeHook
})
// 生命周期的合并方式(合并结果是数组)
function mergeHook(parentVal, childVal) {
  if (childVal) {
    if (parentVal) {
      return parentVal.concat(childVal)
    } else {
      // 第一次合并结果是数组;因为第一次合并时(在定义Vue.mixin时),Vue.options是空对象{},parentVal是undefined,会走这一步
      return [childVal]
    }
  } else {
    return parentVal
  }
}
// 配置合并方法(将child合并到parent中)
export function mergeOptions(parent, child) {
  const options = {} // 合并后的结果
  for (let k in parent) {
    mergeFiled(k)
  }
  for (let k in child) {
    // 对child中有,parent中没有的属性进行合并
    if (!parent.hasOwnProperty(k)) {
      mergeFiled(k)
    }
  }

  function mergeFiled(key) {
    let parentVal = parent[key]
    let childVal = child[key]
    // 1. 使用策略模式合并生命周期
    if (strategies[key]) {
      options[key] = strategies[key](parentVal, childVal)
    } else {
      // 生命周期以外的属性合并,例如computed、data、methods、watch等
      if (isObject(parentVal) && isObject(childVal)) {
        options[key] = {...parentVal, ...childVal}
      } else {
        // 如果合并的一方为function或基本数据类型,儿子有则以儿子为准,否则以父亲为准
        options[key] = childVal || parentVal
      }
    }
  }
  return options
}

总结一下做了什么:

  1. mergeOptions()方法中遍历parent与child中所有的属性,对parent中的所有属性,以及parent中没有、child中有的属性,调用 mergeFiled(key) 进行合并
  2. mergeFiled()中使用策略模式合并生命周期,使用覆盖、扩展的方式合并method、computed、watch、data等属性。
    1. 如果命中了策略,则调用不同的策略进行合并
    2. 如果没有命中策略;对于对象则使用对象的合并,对于其他类型则直接采用childVal进行覆盖。

options合并时的策略模式

为什么要使用策略模式?

合并的属性有很多,比如生命周期、methods、data等等,它们的合并方式是不同的,使用策略模式针对不同的属性定义不同的合并策略,方便扩展,低耦合。

使用策略模式合并生命周期:

// src/util/index.js
const strategies = {} // 存放各种合并策略
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed'
]
LIFECYCLE_HOOKS.forEach((hook) => {
  strategies[hook] = mergeHook
})
// 生命周期的合并方式(合并结果是数组)
function mergeHook(parentVal, childVal) {
  if (childVal) {
    if (parentVal) {
      return parentVal.concat(childVal)
    } else {
      // 第一次合并结果是数组;因为第一次合并时(在定义Vue.mixin时),Vue.options是空对象{},parentVal是undefined,会走这一步
      return [childVal]
    }
  } else {
    return parentVal
  }
}

小结:

  1. 定义一个对象 —— strats,存放不同的策略
  2. 往strats添加八种生命周期的策略,都是mergeHook
  3. strats[key]命中了任意策略,则执行相应的方法
  4. 易扩展:如果以后要添加methods的合并策略,只需要在strats中添加methods属性及相应合并方法即可

生命周期合并的流程与结果: 5. Vue.mixin()第一次合并生命周期时,是与Vue.options(是一个空对象)进行合并的,返回的是一个长度为1的数组 6. 当第二次调用Vue.mixin(),并且上一次Vue.mixin()中有定义相同的生命周期,则会进行数组与函数的合并,得到一个长度为2的数组

组件实例的options如何合并

前面Vue.mixin()是与全局options的合并,当我们使用组件时,vm实例需要将全局的options合并到实例中。

// src/init.js
import { mergeOptions } from "./util/index";
Vue.prototype._init = function (options) {
    vm.$options = mergeOptions(vm.constructor.options, options);
}

小结:

  1. 合并方法同样使用的是mergeOptions
  2. 合并的对象是vm.constructor.options而非Vue.options;原因通常情况下vm.constructor就是Vue,但是如果当前组件是使用extends继承而来,则需要与继承的组件进行合并。

vue中的生命周期

生命周期的本质:在不同的代码执行阶段,调用对应的生命周期钩子函数。

先定义一个方法来调用生命周期钩子:

// src/lifecycle.js
export function callHook(vm, hook) {
  // vm.$options[hook]经过mergeOptions合并之后,是一个数组,所以需要遍历数组
  const handlers = vm.$options[hook];
  if (handlers) {
    for (let i = 0; i < handlers.length; i++) {
      handlers[i].call(vm); //生命周期里面的this指向当前实例
    }
  }
}

beforeCreatecreated的调用,在组件的状态初始化过程中:

// src/init.js
import { callHook } from './lifecycle'
Vue.prototype._init = function (options) {
    const vm = this;
    vm.$options = mergeOptions(vm.constructor.options, options);

    callHook(vm, "beforeCreate");
    initState(vm);  // 初始化状态,包括initProps、initMethod、initData、initComputed、initWatch等
    callHook(vm, "created");

    if (vm.$options.el) {
        vm.$mount(vm.$options.el);
    }
};

beforeMountmounted的调用,在组件的挂载过程中:

// src/lifecycle.js
export function mountComponent(vm, el) {
  vm.$el = el;
  callHook(vm, "beforeMount");

  let updateComponent = () => {
    vm._update(vm._render());
  };
  new Watcher(
    vm,
    updateComponent,
    () => {
      console.log('视图更新了')
      callHook(vm, "beforeUpdate");
    },
    true
  );
  
  callHook(vm, "mounted");
}

系列文章