基于 Vuex3.x 版本
#思考
Vuex 是如何引入 Vue 项目的?
1、先安装 Vuex
,再通过 import 引入项目
2、使用 Vue.use(Vuex)
,将 Vuex 作为插件安装
3、实例化 new Vuex.Store({...})
,将其放入 new Vue()
的 options 中
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {},
actions: {},
mutations: {}
})
new Vue({
store,
...
})
可以看到步骤二使用了 Vue 的 Vue.use API,将 Vuex 安装,这个 API 在很多地方都能用到,比如 ElementUI
、AntDesign
这些 UI 库也都使用了它进行安装,在使用它的时候,我觉得 Vue.use
方法即神奇但又让人难以理解,接下来配合 Vuex
的挂载过程去看看它到底做了什么事。
#解析 Vuex 的挂载过程
function initUse(Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// 插件缓存,若这个插件已被安装,那么直接返回Vue构造函数
const installedPlugins = this._installedPlugins || (this._installedPlugins = [])
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// 将arguments类数组从第一项开始转换为普通数组
const args = toArray(arguments, 1)
// 将Vue构造函数插入数组第一项中
args.unshift(this)
// 判断plugin的install是否是函数,如果是执行用户的install方法
// 若不符合条件,那么判断plugin方法是否是函数,如果是则执行用户传入的plugin方法
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
// 将执行完毕的plugin加入到缓存数组中
installedPlugins.push(plugin)
// 返回Vue构造函数
return this
}
}
这是 use
方法初始化的代码,前两行高亮代码,是 use
对于 plugin
之外的参数进行处理,并在 args
中插入 Vue
构造函数,所以用户可以在 install
中拿到 Vue
构造函数。
在这之后就会执行 Vuex
定义的 install
安装方法: plugin.install.apply(plugin, args)
,将 this
指向 plugin
本身,并将 args
数组传入。
来到 Vuex
的 install
方法:
let Vue
function install (_Vue) {
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// 使用Vue.use的时候会将全局变量Vue指向_Vue构造函数,防止Vuex被重复安装
Vue = _Vue
applyMixin(Vue)
}
install
里面就执行了一个 applyMixin
方法,我们进去看看:
function applyMixin(Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
...此处省略1.x安装方法
}
function vuexInit () {
const options = this.$options
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
可以看到执行了 Vue.mixin
方法,将 vuexInit
这个方法放到了 beforeCreate
生命周期中传入了 mixin 方法中,这个方法会调用 mergeOptions
方法将其合并到 Vue.options
中,类似这样的代码:
Vue.options = {
beforeCreate: [function vuexInit(){}],
...
}
当 new Vue 实例的时候就会执行 Vue 的 _init
方法进行初始化操作,对于 Vuex 的初始化有两处重点:
- 将我们在 new Vue 的时候传入的
store
实例挂载到 vm.$options 上 - 执行了
beforeCreate
钩子
beforeCreate 钩子会在 _init
方法中执行。为什么会选择在 beforeCreate 中执行?主要的原因还是在于 beforeCreate 的时候 Vue
的 Options API
如 data
选项都还没被初始化,若在其他的 hooks
中安装 Vuex
,那么可能会导致需要用到 Vuex
中的 state
的数据的时候但 Vuex
还没有安装的情况出现,所以这样做避免了出现数据错误的情况。
vuexInit 的执行,首先就是将 this.$options 赋值给了 options,这里其实有个问题,这个 this
是谁?我们知道 js 的 this 指向是通过运行时的环境决定的,所以我们需要知道 vuexInit 在哪里被执行,通过前面的分析是在 beforeCreate hook 执行的时候,来到代码中:
function callHook(vm: Component, hook: string) {
pushTarget();
const handlers = vm.$options[hook];
const info = `${hook} hook`;
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info);
}
}
if (vm._hasHookEvent) {
vm.$emit("hook:" + hook);
}
popTarget();
}
function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
代码块中高亮的那一行执行了 invokeWithErrorHandling
方法,我们可以在这个方法中看到 handler
就是我们的 vuexInit
方法,可以看到的是它们在被调用的时候用 apply/call
方法将 vm
实例传入,所以这个时候 vuexInit
方法中的 this 指向的是当前 Vue
的实例对象。回到 vuexinit 方法中:
function vuexInit() {
const options = this.$options
if (options.store) {
this.$store = typeof options.store === 'function' ? options.store() : options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
前面提到这时的 $options 已经有了Store实例,那么有两个 if 分支,里面的逻辑大致相同,都是为了让 vm.$store
指向 Store 实例
。回想一下我们是怎么向 Vuex 派发内容的?
this.$store.commit('type', playload)
是通过 this.$store
向 Vuex
派发内容的,解释一下两个 if 分支的逻辑:
- 如果当前是根组件,就把传入的 Store 实例挂在根节点 vm 上
- 如果当前组件是子组件,就从 options 中的 parent 找到 Store 实例挂载子组件的 vm 上,这里注意是引用赋值,因此每个子组件都可以访问构造
VueComponent
实例上的$store
#总结
之前在 Vue-cli 初始化的项目中总是看到在 new Vue 中传入 store,对这个做法其实一直不是很理解,为什么要这么做,现在明白了是为了将 Store 实例挂载到 options 上再经过 vuexInit 的初始化将 Store 实例放在 Vue 实例的 $store 上,这样就能够让用户在全局调用 Store 实例。