通过手写一个简易的 vuex 理解 vuex 原理
2021年8月31日 · 预计阅读时间: 9 分钟
vuex 介绍
我们先来介绍一下 vuex 中的一些概念
我们根据 vuex 的工作流程来说
- 改变
state
的唯一途径就是提交mutations
- 如果是异步的,就派发 (dispatch)
actions
,其本质还是提交mutations
- 提交
mutations
后,可以动态的渲染组件Vue Components
觉得是不是少了什么,没错,就是getters
下面实现的时候会说
vuex 实现
接下来我们就来仿照 vuex 的源码实现一个简单版本的 vuex 吧
你可以点击这里查看 完整源码,跟着文章一起来看
构建 Store
首先我们需要来创建一个store
,它具有以下属性和方法
定义createStore
方法,这个方法只是创建了一个Store
实例,并传入了options
export function createStore(options) {
return new Store(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))
}
}
收集 modules
通过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
:子 moduelsstate
:传入的原对象的 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 树
绑定 commit 和 dispatch
继续回到 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 安装
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,会有不符合预期的行为
初始化 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
打开控制台,来查看效果