vuex 源码短小精悍,很短时间就可以大致浏览一遍,值得学习。在后续看其他框架的过程中,也发现很多框架都有参考 vuex(比如 herbjs 的插件体系)。
猜想
- 一定有一个地方集中存放所有的数据(state),可以挂载到 window 上面(污染全局),或者挂载到 vue 的原型上面。
- mutation action getter 这些方法,其实都是获取或者修改 state 的值,这些方法和 state 刚好可以组成一个 类(class),类封装对 state 状态的操作。
如何安装
4.0 版本适配了 composition api,提供了导出方法 createStore,通过它可以创建 store。
源码:
export function createStore (options) {
return new Store(options)
}
可以看到,这里只是简单的对 store 进行了一次 new 操作,返回了 store 这个类。
进一步查看,我们看到 install 方法,通过暴露install方法,按照 vue3 的插件规则进行安装,也就是挂载到了 vue3 的原型上面。
export class Store {
// 暴露给 vue3 进行安装
install (app, injectKey) {
app.provide(injectKey || storeKey, this)
app.config.globalProperties.$store = this
}
}
store 初始化
constructor (options = {}) {
// webpack.DefinePlugin 配置
if (__DEV__) {
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
assert(this instanceof Store, `store must be called with the new operator.`)
}
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)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._makeLocalGettersCache = Object.create(null)
// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
// 绑定 dispatch 指向自己
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)
}
// strict mode
this.strict = strict
// 定义 根state
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
// 模块入口,内部注册模块,注册子模块
installModule(this, state, [], this._modules.root)
// initialize the store state, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
// 初始化 state
resetStoreState(this, state)
// apply plugins
// 一般都是通过 store.subscribe 进行订阅,在 mutation 之后执行
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : /* Vue.config.devtools */ true
if (useDevtools) {
devtoolPlugin(this)
}
}
在构造函数中,定义了一系列的参数,参数内容基本都是纯粹空 object(Object.create(null)),可以理解成在构造函数中,对所有的 commit,dispatch等进行了一次收集,触发时候,直接从这边取。
dispatch commit,是外界直接调用的函数,一般使用方式this.$store.dispatch('increment',5);
,为了确保指向store本身,这里用 call
做了一次 this 绑定,否则这里就会指向了 this
也就是 vue 本身了。
模块化
commit
commit (_type, _payload, _options) {
// check object-style commit
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
// 取出对应的 mutations
const entry = this._mutations[type]
if (!entry) {
// 没有entry,开发模式报错
if (__DEV__) {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
this._withCommit(() => {
// 执行entry,这里为啥用 foreach,会存在 entry 数组的情况?
// 测试之后,这里应该只有1个数组内容的情况,不会出现多个数组元素
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
}
commit 部分看起来比较简单,通过函数参数获取 type、payload、options、,取出mutation,执行mutation。
这里有个疑问,为什么entry是forEach执行的,这里一般情况下,应该entry内部只有一个元素,[mutation],这样的形式。
entry.forEach(function commitIterator (handler) {
handler(payload)
})
dispatch
dispatch(_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type]
try {
// before
this._actionSubscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
}
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
// dispatch 最终返回的是一个 promise
return new Promise((resolve, reject) => {
result.then(res => {
try {
// after
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
}
resolve(res)
}, error => {
try {
this._actionSubscribers
.filter(sub => sub.error)
.forEach(sub => sub.error(action, this.state, error))
} catch (e) {
}
reject(error)
})
})
}
dispatch 相对 commit 就要复杂的多了。