date: 2021-11-15 19:20:44
tags: vuex 源码解析
author: coder@mc
版本说明
此博客主要是针对 vuex@3.6.2做一次源码解析,主要研究以下几点:
vuex的初始化过程dispatch和commit的实现- 辅助函数
mapState,mapGetters,mapActions,mapMutations,createNamespacedHelpers的实现 - 插件的补充
思维导图
一、初始化过程
我们平时使用 vuex 时,可以通过 import {Store} from 'vuex' 或者 import vuex from 'vuex'的方式导入,了解过 ES6 import 导入模块的同学,相信一眼就能想到 vuex 中入口文件必然是使用了默认导出和大括号导出,它的源码在src/index.js:
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
import createLogger from './plugins/logger'
// 对应 import vuex from 'vuex'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
createLogger
}
// 对应 import {Store} from 'vuex'
export {
Store,
install,
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
createLogger
}
其中, Store 是我们重点研究的内容,因为初始化是在 Store 构造函数中实现,另外一个需要关注的是 install函数,因为 vuex 是一个插件,想要在项目中使用,必须通过 Vue.use(vuex) 的方式注册,读过vue官方文档的都知道,Vue.use(option) 传递的参数必须是函数或者是包含 install方法的对象。
1.依赖安装
// Vue.use 安装插件时使用的
export function install(_Vue) {
if (Vue && _Vue === Vue) {
if (__DEV__) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// 缓存 Vue 供全局使用
Vue = _Vue
// 混入`beforeCreate`,使每个组件都能通过 `this.$store` 访问到 `store` 实例
applyMixin(Vue)
}
该源码在src/store.js,做了三件事,首先,判断是否已经依赖过了,依赖过了直接打印错误返回;其次,缓存 Vue 供全局使用;最后,通过混入beforeCreate方式,使每个组件都能通过 this.$store 访问到 store 实例,它的源码在src/mixin.js:
/**
* 分别对 Vue 1 和 2 版本进行处理
* 1. 针对 Vue2 版本,每个组件的 beforeCreate 生命周期都挂载一个 vuexInit 函数
* 2. 针对 Vue1 版本,重写原型 _init 函数
* @param {*} Vue
*/
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
// 将 store 挂载到 $store 上,使得每个组件可以通过 this.$store 访问
function vuexInit () {
const options = this.$options
// store injection
// 根组件挂载
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
}
}
}
2. Store 构造函数
该构造函数是创建vuex实例的核心方法,其源码位于src/store.js,接下来,具体看初始化过程都做了哪些事。
2.1 异常检测
如果是在开发环境,会做一些检查:
// 异常检测
if (__DEV__) {
// 必须通过 Vue.use 调用 Vuex
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
// Promise 必须兼容
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
// 必须通过 new 来实例化 Store
assert(this instanceof Store, `store must be called with the new operator.`)
}
export function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}
2.2 初始化一些内部变量
定义了一些内部变量,绑定在 this 上下文中:
// 初始化内部变量
const {
plugins = [],
strict = false
} = options
// store internal state
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
// ModuleCollection 内部会递归注册所有的子模块
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)
// bind commit and dispatch to self
// 包装 dispatch 和 commit,确保 this 始终指向 store 实例
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch(type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit(type, payload, options) {
return commit.call(store, type, payload, options)
}
// 严格模式
this.strict = strict
其中有两点需要强调一下,下面一一讲解。
2.2.1 构造 modules
this._modules = new ModuleCollection(options)
通过 new ModuleCollection(options)得到modules,其内部会递归注册所有的子模块。最终,得到了一个这样的数据结构:
{
root: {
runtime: false,
state: {...},
_rawModule: {
actions: {...},
getters: {...},
mutations: {...},
plugins: [...],
state: {...}
},
_children: {
subModule1: {runtime: false, state: {...}, _rawModule: {...}, ...},
subModule2: {runtime: false, state: {...}, _rawModule: {...}, ...}
},
namespaced: false
}
}
接下来,详细看看是如何得到这一数据结构的,先来看 ModuleCollection 构造函数,该码在./src/module/module-collection.js中:
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
// 将 module 实例放到 root 属性上,存在子模块则递归调用
register (path, rawModule, runtime = true) {
if (__DEV__) {
assertRawModule(path, rawModule)
}
// 返回一个 {runtime, _children, _rawModule, state}
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
// 根组件挂载
this.root = newModule
} else {
// 子组件放在 module._children 上
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// register nested modules
// 存在子模块,递归调用该函数
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
}
rawRootModule其实就是 new vuex.Store(options)中的options,如果options.modules存在,会递归调用register这个方法。而基础数据结构是在 Module 类中,该构造函数在./src/module/module.js:
// 存储模块的基本数据结构,包与一些属性和方法
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// Store some children item
this._children = Object.create(null)
// Store the origin module object which passed by programmer
this._rawModule = rawModule
const rawState = rawModule.state
// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
addChild (key, module) {
this._children[key] = module
}
}
2.2.2 封装 dispatch 和 commit
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch(type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit(type, payload, options) {
return commit.call(store, type, payload, options)
}
这里多封装一层的目的是为了让this始终指向store实例。
2.3 初始化 module
// 初始化 module,存在子模块则递归调用,收集所有的模块 getters 放进 _wrappedGetters 里
installModule(this, state, [], this._modules.root)
只需要传入上面_modules.root,方法内部会自己检测是否存在子模块,若存在子模块递归初始化,否则只初始化根模块。installModule 的实现可以分为四部分,下面一一介绍。
-
获取到命名空间的名称的名称
namespace,将module映射到store._modulesNamespaceMap[namespace]上:// 通过 getNamespace 拿到命名空间的名称,其实就是在模块名后面加 /,根节点 namespace 是 '', // 例如:subModule/ const namespace = store._modules.getNamespace(path) // 做映射 if (module.namespaced) { if (store._modulesNamespaceMap[namespace] && __DEV__) { console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`) } store._modulesNamespaceMap[namespace] = module } -
通过
Vue.set方法将子模块的state设置到父模块的state上const isRoot = !path.length if (!isRoot && !hot) { const parentState = getNestedState(rootState, path.slice(0, -1)) const moduleName = path[path.length - 1] store._withCommit(() => { if (__DEV__) { if (moduleName in parentState) { console.warn( `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"` ) } } Vue.set(parentState, moduleName, module.state) }) } -
构造当前模块的上下文,也就是
action的ctx参数。const local = module.context = makeLocalContext(store, namespace, path);makeLocalContext的实现很简单,首先,定义了local对象,初始化时根据命名空间的名称定义dispatch和commit两个函数;其次,通过Object.defineProperties方法响应式设置了getters和state属性,其代码如下:function makeLocalContext(store, namespace, path) { const noNamespace = namespace === '' // 据命名空间的名称定义 `dispatch`和`commit`两个函数 const local = { dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options) const { payload, options } = args let { type } = args if (!options || !options.root) { type = namespace + type if (__DEV__ && !store._actions[type]) { console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`) return } } return store.dispatch(type, payload) }, commit: noNamespace ? store.commit : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options) const { payload, options } = args let { type } = args if (!options || !options.root) { type = namespace + type if (__DEV__ && !store._mutations[type]) { console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`) return } } store.commit(type, payload, options) } } // 通过`Object.defineProperties`方法响应式设置了`getters`和`state`属性 Object.defineProperties(local, { getters: { get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace) }, state: { get: () => getNestedState(store.state, path) } }) return local } -
将
mutations的每一项放到store._mutations中,将actions的每一项放到store._actions,将getters的每一项放到store._wrappedGetters:// 将 mutations 的每一项放到 store._mutations 中 module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) }) // 将 actions 的每一项放到 store._actions 中 module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) }) // 将 getters 中的每项放到 store._wrappedGetters 中 module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) })上面的代码其实很简单,以
mutation为例,遍历_rawModule.mutations拿到每项mutation的value和key,调用registerMutation函数,将value做了层封装存储到store._mutations[key]中:// 将 mutation 存放到 store._mutations 里面 function registerMutation(store, type, handler, local) { const entry = store._mutations[type] || (store._mutations[type] = []) entry.push(function wrappedMutationHandler(payload) { handler.call(store, local.state, payload) }) }同理,
actions和getters的处理也与mutations相似。 -
存在子模块递归调用
// 递归处理 modules 里面的数据 module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) })
2.4 初始化state 和 getters
初始化完module以后就会处理state数据将其变为响应式数据,并且也会处理前面经过包装的getters,让其变为computed:
resetStoreVM(this, state)
下面是 resetStoreVM 的实现:
// 1. 处理 state 里面的数据,变成响应式数据
// 2. 处理前面包装的 getter,使它变成类似 computed 意义的东西
function resetStoreVM(store, state, hot) {
const oldVm = store._vm
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
// 遍历 store 里面的 getters 属性,getter 本质其实是 computed
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = partial(fn, store)
// 通过数据劫持,让其访问到 store.state 数据
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// silent 取消 Vue 的所有日志与警告
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
// 对 store._vm 启用严格模式
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
2.4.1 处理 state
通过new Vue实例让 state变为响应式
store._vm = new Vue({
data: {
$$state: state
},
computed
})
// Store 构造函数中有取 state 的代码,其实就是访问的 this._vm._data.$$state 的值
export class Store {
...
get state() {
return this._vm._data.$$state
}
}
2.4.2 处理 getters
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
// 遍历 store 里面的 getters 属性,getter 本质其实是 computed
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = partial(fn, store)
// 通过数据劫持,让其访问到 store.state 数据
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
可以看到遍历getters数据,将getters数据放到computed中,然后通过Object.defineProperty数据劫持,实际访问的是store._vm[key]的值。
2.5 调用所有插件
遍历plugins,依次执行传入过来的插件,并将this当做参数传递。
// 调用所有插件,传入 this
plugins.forEach(plugin => plugin(this))
2.6 浏览器 vuex 的调试模式数据变化
// devtools
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
以上就是 vuex 的初始化的大致过程。
二、dispatch 和 commit
看完了 vuex 的初始化过程,接下来,了解的就是如何改变 state 的数据,看过 Vuex 官方文档的同学,肯定对下图十分了解,这是Vuex 改变数据的官方步骤:
dispatch(actions) -> commit(mutations) -> 改变 state -> 更新视图。
1. dispatch 方法
一般我们会使用下面方式调用dispatch 函数:
this.$store.dispatch('subModule/action', payload);
首先会进入前面看到的包装后的 dispatch 函数:
this.dispatch = function boundDispatch(type, payload) {
return dispatch.call(store, type, payload)
}
然后调用真正的 dispatch 方法:
/**
* 主要做了 三件事
* 1. 解析 dispatch 的参数,并定义 action 对象
* 2. 从 初始化 module 中取出 对应的 action 方法,并执行
* 3. 返回 promise
* @param {*} _type action
* @param {*} _payload payload
*/
dispatch(_type, _payload) {
// _type 可以是对象也可以是字符串,但是必须要有 type 属性,
// 例如:1. _type = {type: 'action', xxx},返回值 type === _type.type, payload === _type
// 2. _type = 'action', 返回值 _type = type, payload === payload
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
// action 最终是对象形式
const action = { type, payload }
// 从 初始化 module 中取出 对应的 action
const entry = this._actions[type]
// 异常检测
if (!entry) {
if (__DEV__) {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
// 调用 浏览器 Vuex 调试模式函数,让插件中的数据改变
try {
this._actionSubscribers
.slice() // 浅拷贝,以防止在订阅者同步调用取消订阅时迭代器失效
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}
// 调用对应的 action 的方法,走 commit 函数
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
// 返回 promise
return new Promise((resolve, reject) => {
result.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
resolve(res)
}, error => {
try {
this._actionSubscribers
.filter(sub => sub.error)
.forEach(sub => sub.error(action, this.state, error))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in error action subscribers: `)
console.error(e)
}
}
reject(error)
})
})
}
2. commit 方法
使用写法:
this.$store.commit('mutation', action);
同 dispatch 一样,commit 也做了一层封装,目的也是让其 this 始终指向 store 实例,其真正调用的方法:
/**
* 主要做了三件事:
* 1. 解析传入的参数,定义 mutation
* 2. 从 初始化 module 中取出 对应的 _mutations 方法
* 3. 因为可能存在同名的 mutation,依次调用它
*/
commit(_type, _payload, _options) {
// 同 dispatch 一样,_type 可以是对象也可以是字符串,_type 为对象时必须包含 type 属性
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
// 定义 mutation
const mutation = { type, payload }
// 获取 _mutations 数组
const entry = this._mutations[type]
// 异常检测,没有存在 commit(action) 对应的 mutation
if (!entry) {
if (__DEV__) {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
// 可能存在多个同名的 mutation,依次调用它
this._withCommit(() => {
entry.forEach(function commitIterator(handler) {
handler(payload)
})
})
// 浏览器存在 devtools 插件,该步骤用于记录
this._subscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.forEach(sub => sub(mutation, this.state))
if (
__DEV__ &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
这段代码中需要注意的是_withCommit这个函数:
_withCommit(fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
看起来十分简单,就是将_committing变为 true并执行传递的函数,执行完后committing再变为初始值。为什么要多此一举,直接执行不好吗?其实是为了在严格模式下,如果没有通过commit函数来改变state直接会报错。
// 对 store._vm 启用严格模式
if (store.strict) {
enableStrictMode(store)
}
// 必须通过 commit 函数来改变 state 否则会报错
function enableStrictMode(store) {
store._vm.$watch(function () { return this._data.$$state }, () => {
if (__DEV__) {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
三、辅助函数
1. mapState
官网描述:该方法为组件创建计算属性以返回 Vuex store 中的状态。该函数的源码位于src/helpers:
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
// 异常检测,mapState 传递的参数必须是 数组 或者 对象
if (__DEV__ && !isValidMap(states)) {
console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
}
// normalizeMap 函数作用是将 map 解析成数组 [{key, val}]
normalizeMap(states).forEach(({ key, val }) => {
// 为什么是函数?因为 computed 的属性可以是函数
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
可以看到 mapState 方法其实是 normalizeNamespace 封装了一层方法,normalizeNamespace方法具体是干什么的?这个方法是解析命名空间的,具体实现:
/**
* 解析命名空间
* @param {Function} fn
* @return {Function}
*/
function normalizeNamespace (fn) {
return (namespace, map) => {
// 没有传入命名空间,会将命名空间置为空,这也就是为什么可以传一个参数的原因
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
}
返回一个函数,这个函数其实就是 mapState 函数,从上可以看出可以传两个参数,但是当传一个参数时,会将命名空间置为'',第一个参数作为map,作为 fn 的参数,fn是上面定义mapState变量的内置函数,该函数做了两件事,一是检测传入的state必须是数组或者对象,二是根据key值映射对象,并返回(组件可以通过{computed: ...mapState(...)}解构的原因)。
2. mapGetters
官网描述:为组件创建计算属性以返回 getter 的返回值。流程与上 mapState一致。
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
if (__DEV__ && !isValidMap(getters)) {
console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
}
normalizeMap(getters).forEach(({ key, val }) => {
// The namespace has been mutated by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
if (__DEV__ && !(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
3. mapActions
官网描述:创建组件方法分发 action。流程与上 mapState一致。
export const mapActions = normalizeNamespace((namespace, actions) => {
const res = {}
if (__DEV__ && !isValidMap(actions)) {
console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
}
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {
// get dispatch function from store
let dispatch = this.$store.dispatch
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
if (!module) {
return
}
dispatch = module.context.dispatch
}
return typeof val === 'function'
? val.apply(this, [dispatch].concat(args))
: dispatch.apply(this.$store, [val].concat(args))
}
})
return res
})
4. mapMutations
官网描述:创建组件方法分发 action。流程与上 mapState一致。
export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {}
if (__DEV__ && !isValidMap(mutations)) {
console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
}
normalizeMap(mutations).forEach(({ key, val }) => {
res[key] = function mappedMutation (...args) {
// Get the commit method from store
let commit = this.$store.commit
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
if (!module) {
return
}
commit = module.context.commit
}
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
}
})
return res
})
5. createNamespacedHelpers
官网描述:创建基于命名空间的组件绑定辅助函数。其返回一个包含 mapState、mapGetters、mapActions 和 mapMutations 的对象。它们都已经绑定在了给定的命名空间上。
export const createNamespacedHelpers = (namespace) => ({
mapState: mapState.bind(null, namespace),
mapGetters: mapGetters.bind(null, namespace),
mapMutations: mapMutations.bind(null, namespace),
mapActions: mapActions.bind(null, namespace)
})
四、插件补充
vuex中可以自定义插件,这个插件其实就是一个函数,它接收 store 作为唯一参数。Vuex 的 store 接受 plugins 选项,这个选项暴露出每次 mutation 的钩子。
const myPlugin = store => {
// 当 store 初始化后调用
store.subscribe((mutation, state) => {
// 每次 mutation 之后调用
// mutation 的格式为 { type, payload }
})
}
然后像这样使用:
const store = new Vuex.Store({
// ...
plugins: [myPlugin]
})
可以看看subscribe方法具体做了什么,
subscribe(fn, options) {
return genericSubscribe(fn, this._subscribers, options)
}
function genericSubscribe(fn, subs, options) {
if (subs.indexOf(fn) < 0) {
options && options.prepend
? subs.unshift(fn)
: subs.push(fn)
}
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}
可以清楚的看到,该方法将传递的fn,根据options配置放到store._subscribers中,并返回一个可以删除该插件的函数。为什么说会暴露出每次 mutation 的钩子,其实是在commit方法中有这一段代码:
this._subscribers
.slice() // 浅层复制
.forEach(sub => sub(mutation, this.state));
到这里vuex 的源码大致走了一遍,有的细节可以自己去看,这篇博客哪里写的有问题欢提出,共同学习~