vuex 源码分析——注册 store 实例对象

966 阅读2分钟

如何使用Vuex

虽然大家对于如何使用Vuex都不陌生,但是在分析源码是如何注册store之前,我们还是来简单回顾一下。

  1. 安装 vuex :npm install vuex --save

  2. 在 vue 项目中的 src 目录下创建 store 目录,并在该目录下新建文件——index.js,用于创建 store 实例。

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

Vue.use(Vuex) // 安装Vuex插件

const state = {}
const mutations = {}
const actions = {}
const getters = {}

// 创建 store 实例对象并导出。仅为举例,就定义了些空对象。
export default new Vuex.Store({
    state,
    mutations,
    actions,
    getters
})
  1. 在 main.js 入口文件中引入
import Vue from 'vue'
import App from './App.vue'
import store from './index'

new Vue({
  el: '#app',
  // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
  store,
  render: h => h(App)
})

经过回顾,大家必然都有了印象,接下来就步入正题吧。

注册 store

使用过 Vuex 的同学们都知道:Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex))。而通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。

为了弄明白原理,我们需要先了解一下 Vue.use

Vue.use 定义

引用自官方定义:

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

该方法需要在调用 new Vue() 之前被调用。 当 install 方法被同一个插件多次调用,插件将只会被安装一次。

从上面的定义中,我们不难明白。要安装 Vue 插件,则此插件必须提供 install 方法或本身是一个函数。所以,我们在使用 Vue.use(Vuex) 安装 Vuex 插件时,实际上调用的是其内部的 install 方法。

Vue.use 方法

Vue.use 是 vue 源码中定义的方法,用于安装 Vue 插件。

export function initUse (Vue: GlobalAPI) {
 // plugin是对象或函数
 Vue.use = function (plugin: Function | Object) {
   // 定义存储插件的数组变量
   const installedPlugins = (this._installedPlugins || (this._installedPlugins =
[]))

   // 判断vue是否已经注册过这个插件
   if (installedPlugins.indexOf(plugin) > -1) {
     return this
   }
   
   // toArray,是 vue 源码中定义的一个工具方法
   // toArray(arguments, 1) 会把 arguments(类数组) 转为数组,同时会把
   // 除第一个参数外(即插件plugin)的其他参数全都存储到一个数组中。
   const args = toArray(arguments, 1)
   
   // 将 vue 对象插入到 args 数组的第一位
   args.unshift(this)
   
   // 判断插件是否有 install 方法。
   // 如果有,调用 install 方法并将参数数组传入,改变 this 指针为该组件
   //如果没有,则直接调用,改变 this 指针为 null
   if (typeof plugin.install === 'function') {
     plugin.install.apply(plugin, args)
   } else if (typeof plugin === 'function') {
     plugin.apply(null, args)
   }
   
   // 将 plugin 添加到 installedPlugins 数组中,用于检测是否安装过此插件
   installedPlugins.push(plugin)
   return this
 }
}

toArray 方法

toArray 是 vue 源码中定义的一个工具方法。

// 将一个类似数组的对象转换为一个真正的数组
export function toArray (list: any, start?: number): Array<any> {
  start = start || 0
  let i = list.length - start
  const ret: Array<any> = new Array(i)
  while (i--) {
    ret[i] = list[i + start]
  }
  return ret
}

从 Vue.use 的源码中我们可以看到:plugin 若是'object',则调用其内的 install 方法,若是 'function',则将自身作为 install 方法调用。现在,想必大家已经理清了 Vue.use 的源码实现,接下来,就是 Vuex 源码中暴露的 install 方法了。

Vuex 中的 install

install 方法的核心在于调用 applyMixin 方法。

// 在 Vuex 中的文件路径:src/store.js
export function install(_Vue) {
  // 避免重复安装
  if (Vue && _Vue === Vue) {
    if (__DEV__) {
      console.error(
        // vuex已经安装了。Vue.use(Vuex)应该只调用一次。
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 首次加载,将 _Vue 变量赋值给 Vue,并用于检测是否重复安装
  Vue = _Vue
  
  // applyMixin 是将 store 实例注入到根组件下的所有子组件中的关键所在
  applyMixin(Vue) 
}

applyMixin

applyMixin,真正实现将 store 实例注入到根组件下的所有子组件中的方法。并对 vue1.0 和 2.0 版本做了不同的处理。

// 在 Vuex 中的文件路径:src/mixin.js
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0]) // 获取 vue 版本号
  if (version >= 2) {
    // Vue.mixin 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
    // 插件作者可以使用混入,向组件注入自定义的行为。不推荐在应用代码中使用。
    
    // 将 vuexInit 混入到 Vue 的 beforeCreacte 钩子中。
    // 这样,就会在每个 Vue 实例中执行 vuexInit 方法。
    Vue.mixin({
      beforeCreate: vuexInit
    })
  } else {
    // 覆盖初始化并注入 vuex 初始化过程
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init ? [vuexInit].concat(options.init) :  vuexInit
      _init.call(this, options) 
    }
  }

  // Vuex 初始化钩子,注入到每个实例的初始化钩子列表中
  function vuexInit() {
    const options = this.$options
    // store 注入,若 options.store 存在,则当前组件为根组件,否则为子组件
    if (options.store) { 
      this.$store = typeof options.store === 'function' ?
        options.store() : options.store
    } else if (options.parent && options.parent.$store) { 
      // 子组件引用其父组件中的 $store 。这样,在任意组件中通过 this.$store
      // 就能访问到根实例的 store
      this.$store = options.parent.$store
    }
  }
}

结束语

对于解析源码这种事,本人也是新手上路。若是有说错或不严谨的地方,希望小伙伴们能够指出,以便大家一起愉快的掉头发。同时,建议初学的小伙伴,在阅读源码前,最好已学习并使用过Vuex。只有如此,才能更加清晰地理解它的工作流程和原理。