【前端词典】从源码解读 Vuex 注入 Vue 生命周期的过程

前言

这篇文章是【前端词典】系列文章的第 13 篇文章,接下的 9 篇我会围绕着 Vue 展开,但愿这 9 篇文章可使你们加深对 Vue 的了解。固然这些文章的前提是默认你对 Vue 有必定的基础。若是一点基础都没有,建议先看官方文档。html

第一篇文章我会结合 Vue 和 Vuex 的部分源码,来讲明 Vuex 注入 Vue 生命周期的过程。前端

说到源码,其实没有想象的那么难。也和咱们平时写业务代码差很少,都是方法的调用。可是源码的调用树会复杂不少。vue

为什么使用 Vuex

使用 Vue 咱们就不可避免的会遇到组件间共享的数据或状态。应用的业务代码逐渐复杂,props、事件、事件总线等通讯的方式的弊端就会愈发明显。这个时候咱们就须要 Vuex 。Vuex 是一个专门为 Vue 设计的状态管理工具。ios

状态管理是 Vue 组件解耦的重要手段。vuex

它借鉴了 Flux、redux 的基本思想,将状态抽离到全局,造成一个 Store。npm

Vuex 不限制你的代码结构,但须要遵照一些规则:redux

  1. 应用层级的状态应该集中到单个 store 对象中
  2. 提交 mutation 是更改状态的惟一方法,而且这个过程是同步的
  3. 异步逻辑都应该封装到 action 里面

Vuex 注入 Vue 生命周期的过程

咱们在安装插件的时候,总会像下面同样用 Vue.use() 来载入插件,但是 Vue.use() 作了什么呢?api

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);
复制代码

Vue.use() 作了什么

安装 Vue.js 插件。若是插件是一个对象,必须提供 install 方法。若是插件是一个函数,它会被做为 install 方法。install 方法调用时,会将 Vue 做为参数传入。缓存

以上是 官方文档 的解释。bash

接下来咱们从源码部分来看看 Vue.use() 都作了什么。

Vue 源码在 initGlobalAPI 入口方法中调用了 initUse (Vue) 方法,这个方法定义了 Vue.use() 须要作的内容。

function initGlobalAPI (Vue) {
  ......
  initUse(Vue);
  initMixin$1(Vue); // 下面讲 Vue.mixin 会提到
  ......
}

function initUse (Vue) {
  Vue.use = function (plugin) {
    var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
    /* 判断过这个插件是否已经安装 */
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    var args = toArray(arguments, 1);
    args.unshift(this);
    /* 判断插件是否有 install 方法 */
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args);
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args);
    }
    installedPlugins.push(plugin);
    return this
  };
}
复制代码

这段代码主要作了两件事情:

  1. 一件是防止重复安装相同的 plugin
  2. 另外一件是初始化 plugin

插件的 install 方法

看完以上源码,咱们知道插件(Vuex)须要提供一个 install 方法。那么咱们看看 Vuex 源码中是否有这个方法。结果固然是有的:

/* 暴露给外部的 install 方法 */
function install (_Vue) {
  /* 避免重复安装(Vue.use 内部也会检测一次是否重复安装同一个插件)*/
  if (Vue && _Vue === Vue) {
    {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      );
    }
    return
  }
  Vue = _Vue;
  /* 将 vuexInit 混淆进 Vue 的 beforeCreate(Vue2.0) 或 _init 方法(Vue1.0) */
  applyMixin(Vue);
}
复制代码

这段代码主要作了两件事情:

  1. 一件是防止 Vuex 被重复安装
  2. 另外一件是执行 applyMixin,目的是执行 vuexInit 方法初始化 Vuex

接下来 咱们看看 applyMixin(Vue) 源码:

/* 将 vuexInit 混淆进 Vue 的 beforeCreate */
function applyMixin (Vue) {
  var version = Number(Vue.version.split('.')[0]);
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit });
  } else {
    /* Vue1.0 的处理逻辑,此处省略 */
    ......
  }
  function vuexInit () {
    ......
  }
}
复制代码

从上面的源码,能够看出 Vue.mixin 方法将 vuexInit 方法混淆进 beforeCreate 钩子中,也是由于这个操做,因此每个 vm 实例都会调用 vuexInit 方法。那么 vuexInit 又作了什么呢?

vuexInit()

咱们在使用 Vuex 的时候,须要将 store 传入到 Vue 实例中去。

new Vue({
  el: '#app',
  store
});
复制代码

可是咱们却在每个 vm 中均可以访问该 store,这个就须要靠 vuexInit 了。

function vuexInit () {
    const options = this.$options
    if (options.store) {
      /* 根节点存在 stroe 时 */
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      /* 子组件直接从父组件中获取 $store,这样就保证了全部组件都公用了全局的同一份 store*/
      this.$store = options.parent.$store
    }
  }
复制代码

根节点存在 stroe 时,则直接将 options.store 赋值给 this.$store。不然则说明不是根节点,从父节点的 $store 中获取。

经过这步的操做,咱们就以在任意一个 vm 中经过 this.$store 来访问 Store 的实例。接下来咱们反过来讲说 Vue.mixin()。

Vue.mixin()

全局注册一个混入,影响注册以后全部建立的每一个 Vue 实例。插件做者可使用混入,向组件注入自定义的行为。不推荐在应用代码中使用。

在 vue 的 initGlobalAPI 入口方法中调用了 initMixin$1(Vue) 方法:

function initMixin$1 (Vue) {
  Vue.mixin = function (mixin) {
    this.options = mergeOptions(this.options, mixin);
    return this
  };
}
复制代码

Vuex 注入 Vue 生命周期的过程大概就是这样,若是你感兴趣的话,你能够直接看看 Vuex 的源码,接下来咱们说说 Store。

Store

上面咱们讲到了 vuexInit 会从 options 中获取 Store。因此接下来会讲到 Store 是怎么来的呢?

咱们使用 Vuex 的时候都会定义一个和下面相似的 Store 实例。

import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'

Vue.use(Vuex)

const state = {
    showState: 0,                             
}

export default new Vuex.Store({
    strict: true,
	state,
	getters,
})

复制代码

不要在发布环境下启用严格模式。严格模式会深度监测状态树来检测不合规的状态变动 —— 请确保在发布环境下关闭严格模式,以免性能损失。

state 的响应式

你是否关心 state 是如何可以响应式呢?这个主要是经过 Store 的构造函数中调用的 resetStoreVM(this, state) 方法来实现的。

这个方法主要是重置一个私有的 _vm(一个 Vue 的实例) 对象。这个 _vm 对象会保留咱们的 state 树,以及用计算属性的方式存储了 store 的 getters。如今具体看看它的实现过程。

/* 使用 Vue 内部的响应式注册 state */
function resetStoreVM (store, state, hot) {
  /* 存放以前的vm对象 */
  const oldVm = store._vm 

  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  /* 经过 Object.defineProperty 方法为 store.getters 定义了 get 方法。当在组件中调用 this.$store.getters.xxx 这个方法的时候,会访问 store._vm[xxx]*/
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  const silent = Vue.config.silent
  /* 设置 silent 为 true 的目的是为了取消 _vm 的全部日志和警告 */
  Vue.config.silent = true
  /*  这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  /* 使能严格模式,Vuex 中对 state 的修改只能在 mutation 的回调函数里 */
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    /* 解除旧 vm 的 state 的引用,并销毁这个旧的 _vm 对象 */
    if (hot) {
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}
复制代码

state 的响应式大概就是这样实现的,也就是初始化 resetStoreVM 方法的过程。

看看 Store 的 commit 方法

咱们知道 commit 方法是用来触发 mutation 的。

commit (_type, _payload, _options) {
  /* unifyObjectStyle 方法校参 */
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  /* 找到相应的 mutation 方法 */
  const entry = this._mutations[type]
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
  }
  /* 执行 mutation 中的方法 */
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })
  /* 通知全部订阅者,传入当前的 mutation 对象和当前的 state */
  this._subscribers.forEach(sub => sub(mutation, this.state))

  if (
    process.env.NODE_ENV !== 'production' &&
    options && options.silent
  ) {
    console.warn(
      `[vuex] mutation type: ${type}. Silent option has been removed. ` +
      'Use the filter functionality in the vue-devtools'
    )
  }
}
复制代码

该方法先进行参数风格校验,而后利用 _withCommit 方法执行本次批量触发 mutation 处理函数。执行完成后,通知全部 _subscribers(订阅函数)本次操做的 mutation 对象以及当前的 state 状态。

Vue 相关文章输出计划

最近总有朋友问我 Vue 相关的问题,所以接下来我会输出 9 篇 Vue 相关的文章,但愿对你们有必定的帮助。我会保持在 7 到 10 天更新一篇。

  1. 【前端词典】Vuex 注入 Vue 生命周期的过程
  2. 【前端词典】浅析 Vue 响应式原理
  3. 【前端词典】新老 VNode 进行 patch 的过程
  4. 【前端词典】如何开发功能组件并上传 npm
  5. 【前端词典】从这几个方面优化你的 Vue 项目
  6. 【前端词典】从 Vue-Router 设计讲前端路由发展
  7. 【前端词典】在项目中如何正确的使用 Webpack
  8. 【前端词典】Vue 服务端渲染
  9. 【前端词典】Axios 与 Fetch 该如何选择

建议你关注个人公众号,第一时间就能够接收最新的文章。

若是你想加群交流,也能够添加有点智能的机器人,自动拉你进群:

热门文章传送门

  1. 【前端词典】滚动穿透问题的解决方案
  2. 【前端词典】5 种滚动吸顶实现方式的比较(性能升级版)
  3. 【前端词典】提升幸福感的 9 个 CSS 技巧
  4. 【前端词典】分享 8 个有趣且实用的 API
  5. 【前端词典】从输入 URL 到展示涉及哪些缓存环节(很是详细)