Skip to main content

通过手写一个简易的 vuex 理解 vuex 原理

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:子 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 树

绑定 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就实现了这一过程

dispathcommit的作用相同,不同的是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)
})
})
}

commitdispath中,都执行了各自的订阅函数集合_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

打开控制台,来查看效果

参考文章

Vuex 框架原理与源码分析