在vuex中,我们将从一些概念开始。
这取决于vuex的工作流程。
修改state的唯一方法是提交mutations。
在异步情况下派发(dispatch)动作,其本质仍然是提交mutations。
在提交mutations之后,VueComponents可以动态呈现组件。
感觉有没有减少了什么,没错,当getters下实现时就会说。
下一步,我们将模拟vuex的源代码实现一个简单版本的vuex。
您可点击此处查看完整的源码,并随文章观看。
第一,我们需要创建一个store,该store有下列属性和方法。
将createStore方法定义为简单的创建一个Store实例并传递options。
扩展函数创建存储(options){returnnewStore(options)}
让我们看一下Store类的实现。
export class Store {
constructor(options = {}) {
const plugins = options.plugins || []
this._subscribers = []
this._actionSubscribers = []
this._actions = Object.create(null)
this._mutations = Object.create(null)
this.getters = Object.create(null)
// 收集 modules
this._modules = new ModuleCollection(options)
// 绑定 commit 和 dispatch 到自身
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) {
return commit.call(store, type, payload, options)
}
const state = this._modules.root.state
// 安装 module
installModule(this, state, [], this._modules.root)
// 初始化 state
resetStoreState(this, state)
// 应用插件
plugins.forEach(plugin => plugin(this))
}
}
通过ModuleCollection
收集 modules,生成一棵 module 树
// store.js
export class Store {
constructor(options = {}) {
this._modules = new ModuleCollection(options)
}
}
// module-collection.js
export default class ModuleCollection {
constructor(rawRootModule) {
this.register([], rawRootModule)
}
register(path, rawModule) {}
}
在ModuleCollection
中调用了一个注册函数register
来注册每个 module
其中path
参数指的是当前 moduel 的路劲,可以用来判断层级关系,rawModule
则是原始的 module 对象
register(path, rawModule) {
const newModule = new Module(rawModule)
if (path.length === 0) {
// root 定义为 rawModule
this.root = newModule
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// 如果传入有 modules,递归地注册子模块
if (rawModule.modules) {
Object.keys(rawModule.modules).forEach(key => {
const rawChildModule = rawModule.modules[key]
this.register(path.concat(key), rawChildModule)
})
}
}
在register
中,通过rawModule
我们实例化了一个Module
对象,它具有以下属性
_rawModule
:传入的原对象
_children
:子 moduels
state
:传入的原对象的 state 值
getChild
:获取子 moduel 的方法
addChild
:新增子 module 的方法
export default class Module {
constructor(rawModule) {
this._rawModule = rawModule
this._children = Object.create(null)
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
}
当path.length === 0
时,将该newModule
作为root
的值,也可以叫做父 module
设置完root
后,会继续判断有没有modules
,如果有的话,递归地注册子模块
// 如果传入有 modules,递归地注册子模块
if (rawModule.modules) {
Object.keys(rawModule.modules).forEach(key => {
const rawChildModule = rawModule.modules[key]
this.register(path.concat(key), rawChildModule)
})
}
在这里,将每个 module 各自的 key 设置为 path 用来区分层次,加入到root
中
get(path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
if (path.length === 0) {
// ...
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
最后,我们就得到了这样的一个 module 树
继续回到 store 的构造函数代码
// 绑定 commit 和 dispatch 到自身
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) {
return commit.call(store, type, payload, options)
}
封装替换原型中的 dispatch 和 commit 方法,将 this 指向当前 store 对象。dispatch 和 commit 方法具体实现如下:
commit(type, payload) {
const mutation = { type, payload }
const entry = this._mutations[type]
if (!entry) {
return
}
entry(payload)
this._subscribers.slice().forEach(sub => sub(mutation, this.state))
}
刚开始我们就提到,修改 state 需要提交 mutation,commit
就实现了这一过程
dispath
和commit
的作用相同,不同的是dispath
是派发action
来提交mutation
修改state
,我们通常在action
中执行异步函数
dispatch(type, payload) {
const action = { type, payload }
const entry = this._actions[type]
if (!entry) {
return
}
try {
this._actionSubscribers
.slice()
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (error) {
console.error(e)
}
const result = entry(payload)
return new Promise((resolve, reject) => {
result
.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (error) {
console.error(e)
}
resolve(res)
})
.catch(error => {
try {
this._actionSubscribers
.filter(sub => sub.error)
.forEach(sub => sub.error(action, this.state, error))
} catch (e) {
console.error(e)
}
reject(error)
})
})
}
在commit
和dispath
中,都执行了各自的订阅函数集合_subscribers
和_actionSubscribers
,
_subscribers
的订阅函数中传入了当前的 mutation 对象和当前的 state,这是提供给插件的参数
_actionSubscribers
还将函数分为了before
,after
,error
类型
module 的安装是为了封装 mutations、actions、getters 函数,传入需要的参数
封装 mutation
if (module._rawModule.mutations) {
Object.keys(module._rawModule.mutations).forEach(key => {
const mutation = module._rawModule.mutations[key]
store._mutations[key] = payload =>
// 惰性获取 state
mutation.call(store, getNestedState(store.state, path), payload)
})
}
封装 action
if (module._rawModule.actions) {
Object.keys(module._rawModule.actions).forEach(key => {
const action = module._rawModule.actions[key]
store._actions[key] = payload => {
let res = action.call(
store,
{
dispatch: store.dispatch,
commit: store.commit,
getters: store.getters,
state: getNestedState(store.state, path),
},
payload
)
if (!(res instanceof Promise)) {
res = Promise.resolve(res)
}
return res
}
})
}
封装 getter
if (module._rawModule.getters) {
Object.keys(module._rawModule.getters).forEach(key => {
const getter = module._rawModule.getters[key]
store.getters[key] = () =>
// 惰性获取 state
getter(getNestedState(store.state, path), store.getters)
})
}
最后递归安装子 module
Object.keys(module._children).forEach(key =>
installModule(store, rootState, path.concat(key), module._children[key])
)
需要注意的一点是,获取 state 的时候需要惰性的获取,因为在使用 vuex 的过程中,state 发生会改变,如果封装函数的时候固定 state,会有不符合预期的行为
然后是resetStoreState
函数
export function resetStoreState(store, state) {
store._state = reactive({
data: state,
})
}
将state
变为响应式的,这样就可以在 vuex 修改了 state 之后,更新视图了
关于 vue3 的数据响应式原理可以看我的这篇文章 vue3 数据响应式原理分析
看到这你可能会有点迷惑,怎么实例上的是_state
而不是state
呢?其实还有store
中还有一个 getter 取 state 的值
get state() {
return this._state.data
}
set state(v) {}
这里我们也可以发现,直接设置 state 的值是无效的
我们先来实现一个打印修改 state 前后变化的插件:logger
export const logger = store => {
let prevState = deepClone(store.state)
store.subscribe((mutation, state) => {
const nextState = deepClone(state)
const formattedTime = getFormattedTime()
const message = `${mutation.type}${formattedTime}`
console.log('%c prev state', 'color: #9E9E9E; font-weight: bold', prevState)
console.log('%c mutation', 'color: #03A9F4; font-weight: bold', message)
console.log('%c next state', 'color: #4CAF50; font-weight: bold', nextState)
prevState = nextState
})
}
很简单,监听一下mutation
就可以啦
store
中会注入每个插件
plugins.forEach(plugin => plugin(this)
实验一下效果,如果你 clone 了源码,那么你可以在安装了依赖之后,执行
yarn dev
打开控制台,来查看效果