9

手写Vue源码(四) - 生命周期

 3 years ago
source link: https://zhuanlan.zhihu.com/p/343298126
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

手写Vue源码(四) - 生命周期

Enjoy what you are doing!

源码地址:传送门

Vue为用户提供了许多生命周期钩子函数,可以让用户在组件运行的不同阶段书写自己的逻辑。

那么Vue内部到底是如何处理生命周期函数的呢?Vue的生命周期究竟是在代码运行的哪个阶段执行呢?本文将实现Vue生命周期相关代码的核心逻辑,从源码层面来理解生命周期。

Vue.mixin

在介绍生命周期之前,我们先来看下Vue.mixin

Vue.mixinVue的全局混合器,它影响Vue创建的每一个实例,会将mixin 中传入的配置项与组件实例化时的配置项按照一定规则进行合并。对于生命周期钩子函数,相同名字的生命周期将会合并到一个数组中,混合器中的钩子函数将会先于组件中的钩子函数放入到数组中。在特定时机时,从左到右执行数组中的每一个钩子函数。

<div id="app">
</div>
<script>
  // 生命周期:
  Vue.mixin({
    created () {
      console.log('global created');
    }
  });
  const vm = new Vue({
    el: '#app',
    data () {
    },
    created () {
      console.log('component created');
    }
  });
  // global created
  // component created  
</script>

上述代码会先执行Vue.mixin中的created函数,然后再执行组件中的created函数。下面我们看下Vue.mixin是怎么实现。

// src/global-api/index.js
import mergeOptions from '../shared/merge-options';

export function initGlobalApi (Vue) {
  Vue.options = {};
  Vue.mixin = function (mixin) {
    this.options = mergeOptions(this.options, mixin);
  };
}

// src/index.js
function Vue (options) {
  this._init(options);
}

// 初始化全局api
initGlobalApi(Vue);
export default Vue;

scr/index.js中执行initGlobalApi方法,会为Vue添加optionsVue.mixin属性。

Vue.mixin会将调用该函数时传入的配置项与Vue.options中的选项进行合并:

Vue.options = {};
Vue.mixin = function (mixin) {
  // Vue.options = mergeOptions(Vue.options, mixin)
  this.options = mergeOptions(this.options, mixin);
};

Vue.options中会保存所有全局的配置项,如components,directives等。执行Vue.mixin之后,Vue.options会和Vue.mixin 中的选项进行合并,之后会在组件初始化时将其和组件实例化时传入的选项根据不同的合并策略进行合并,这样会根据最终合并后的全局选项和组件选项来创建Vue 实例:

// src/init.js
function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    const vm = this;
    // 在初始化根组件时: vm.constructor.options -> Vue.options
    // 在初始化子组件时: vm.constructor.options -> Sub.options
    // 将局部选项和全局选项进行合并
    vm.$options = mergeOptions(vm.constructor.options, options);
    // some code ...  
  };
  // some code ...  
}

现在关键逻辑来到了mergeOptions,下面来介绍mergeOptions的整体编写思路以及生命周期的合并过程。

生命周期选项合并

mergeOptions函数完成了组件中选项合并的逻辑:

const strategies = {};

function defaultStrategy (parentVal, childVal) {
  return childVal === undefined ? parentVal : childVal;
}

const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed'
];

function mergeHook (parentVal, childVal) {
  if (parentVal) {
    if (childVal) {
      // concat可以拼接值和数组,但是相对于push来说,会返回拼接后新数组,不会改变原数组
      return parentVal.concat(childVal);
    }
    return parentVal;
  } else {
    return [childVal];
  }
}

LIFECYCLE_HOOKS.forEach(hook => {
  strategies[hook] = mergeHook;
});

function mergeOptions (parent, child) { // 将子选项和父选项合并
  const options = {};

  function mergeField (key) {
    const strategy = strategies[key] || defaultStrategy;
    options[key] = strategy(parent[key], child[key]);
  }

  for (const key in parent) {
    if (parent.hasOwnProperty(key)) {
      mergeField(key);
    }
  }
  for (const key in child) {
    if (child.hasOwnProperty(key) && !parent.hasOwnProperty(key)) {
      mergeField(key);
    }
  }

  return options;
}

export default mergeOptions;

对于不同的选项,Vue会采取不同的合并策略。也就是为strategies添加Vue的各个选项作为key,其对应的合并逻辑是一个函数,为strategies[key]的值。如果没有对应key 的话,会采用默认的合并策略defaultStrategy来处理默认的合并逻辑。

这样可以让我们不用再用if else来不停为每一个选项进行判断,使代码更加简洁。并且在之后如果需要添加新的合并策略时,只需要添加类似如下代码即可,更易于维护:

function mergeXXX (parentVal, childVal) {
  return result
}

strategies[xxx] = mergeXXX

对于生命周期,我们会将每个钩子函数都通过mergeHook合并为一个数组:

function mergeHook (parentVal, childVal) {
  if (parentVal) {
    if (childVal) {
      // concat可以拼接值和数组,但是相对于push来说,会返回拼接后新数组,不会改变原数组
      return parentVal.concat(childVal);
    }
    return parentVal;
  } else {
    return [childVal];
  }
}

Vue.mixin中提到例子的合并结果如下:

现在我们已经成功将生命周期处理成了数组,接下来便到了执行数组中的所有钩子函数的逻辑了。

调用生命周期函数

完成上述代码后,我们已经成功将所有合并后的生命周期放到了vm.$options中对应的生命周期数组中:

vm.$options = {
  created: [f1, f2, f3],
  mounted: [f4, f5, f6]
  // ...
}

想要执行某个生命周期函数,可以用它的名字从vm.$options找到其对应的函数执行。为了方便生命周期的调用,封装了一个callHook函数来帮我们做这些操作:

// src/lifecycle.js
export function callHook (vm, hook) {
  const handlers = vm.$options[hook];
  if (handlers) {
    handlers.forEach(handler => handler.call(vm));
  }
}

对于目前我们已经完成的代码,可以在如下位置添加生命周期钩子函数的调用:

此时,用户在使用时传入的beforeCreate,created,beforeMount,Mounted钩子函数就可以正确执行了。

  • beforeCreate:在组件初始化状态initState之前执行,此时不能访问props,methods,data,computed等实例上的属性
  • created:组件初始化状态后执行,此时props,methods,data等选项已经初始化完毕,可以通过实例来直接访问
  • beforeMount: 组件过载之前执行
  • mounted: 组件挂载之后执行,即使用实例上最新的data生成虚拟DOM,然后将虚拟DOM挂载到真实DOM之后执行。

生命周期函数本质上就是我们在配置项中传入回调函数,Vue会将我们传入的配置项收集到数组中,然后在特定时机统一执行。

Vue的生命周期从定义到执行一共经历了如下几个步骤:

  1. 在组件实例化时作为选项传入
  2. 首先将Vue.mixin中传入的配置项和Vue.options中的生命周期函数合并为一个数组
  3. 将组件实例化时传入的选项和Vue.options中的生命周期继续进行合并
  4. 封装callHook函数,从vm.$options中找到指定生命周期函数对应的数组
  5. 在特定时机执行特定的生命周期函数

希望在阅读完本文后,能够帮小伙伴们明白,在使用Vue时定义的生命周期函数到底是如何被处理和执行的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK