简易vuex4核心原理及实现源码分析
作者:驺虞
前言
Vuex 是一个专为 Vue.js 应用程序开发的 状态管理模式 。它借鉴了Flux、redux的基本思想,将共享的数据抽离到全局,同时利用Vue.js的 响应式 机制来进行高效的状态管理与更新。想要掌握了解基础知识可以查阅Vuex官网,本篇主要是对 vuex4.x版本的源码 进行研究分析。
Vuex 核心原理
使用方式
创建 store
import { createStore } from "@/vuex"; const store = createStore({ state: { count: 0, }, getters: { double: (state) => { return state.count * 2; }, }, mutations: { add(state, payload) { state.count += payload; }, }, actions: { asyncAdd({ commit }, payload) { return new Promise((resolve, reject) => { setTimeout(() => { commit("add", payload); resolve(); }, 1000); }); }, }, }); export default store;
引入 store
import store from "./store"; // 传入key值,标识 store createApp(App).use(store, "my").mount("#app");
使用 store
<template> <div> count:{{ count }} <hr /> getter:{{ double }} <hr /> <button @click="$store.state.count++">直接修改state</button> <button @click="add">同步修改</button> <button @click="asyncAdd">异步修改</button> </div> </template> <script> import { computed } from "vue"; import { useStore } from "@/vuex"; export default { name: "App", setup() { // 传入 key 使用特定的 store const store = useStore("my"); function add() { store.commit("add", 1); } function asyncAdd() { store.dispatch("asyncAdd", 1).then(() => { console.log("ok"); }); } return { count: computed(() => store.state.count), double: computed(() => store.getters.double), add, asyncAdd, }; }, }; </script>
vuex 运行流程
Vuex 的运作流程如下图所示:
核心原理
vuex4
是一个插件,所以创建的store
实例需要实现一个install
方法vuex4
需要导出createStore
,用于创建store
,接收一个options
对象,vuex4
需要导出useStore
,用于在组件中使用store
store
是一个全局状态库,并且是响应式的,可以在各个组件中使用store
中的状态- 可以创建多个
store
实例,通过key
标识来区分不同的store
实现一个简易版的 vuex
首先不考虑 modules
、插件、严格模式、动态模块等功能,实现一个简易版的vuex; 该版本包含的功能有:
store
的派发和注册state
的响应式getters
、mutations
、actions
、commit
、dispatch
- 通过
key
标识多个store
实现 store 的派发和注册、响应式、injectKey
- 通过
provide/inject
实现store
的派发和注册 - 通过
reactive
实现state
的响应式 - 通过在
provide/inject
时传入injectKey
,来标识不同的store
import { inject, reactive } from "vue"; const storeKey = "store"; class Store { constructor(options) { const store = this; // state 响应式 // 做状态持久化时需要整体替换state,为了保持state的响应式,用data进行包裹 store._state = reactive({ data: options.state }); } // 代理 store._state.data 到 store.state 上 get state() { return this._state.data; } install(app, injectKey) { // 全局暴露一个变量,暴露的是store实例 app.provide(injectKey || storeKey, this); // this 指向 store 实例 // 设置全局变量 $store app.config.globalProperties.$store = this; } } export function createStore(options) { return new Store(options); } export function useStore(injectKey = storeKey) { return inject(injectKey); }
实现 getters、mutations、actions、commit、dispatch
getters
的实现:将options.getters
代理到store.getters
,并传入参数store.state
;在vue3.2以上版本,可以使用computed
实现getters
的缓存。mutations
的实现:将options.mutations
代理到store._mutations
上,将mutation
内部的this
指向store
,并传入参数store.state
和payload
;actions
的实现类似。commit
和dispatch
的实现:它们是一个函数,通过传入的type
和payload
匹配并执行对应的mutation
和action
// 遍历 obj,对每一项执行 fn(obj[key], key) export function forEachValue(obj, fn) { Object.keys(obj).forEach((key) => fn(obj[key], key)); } class Store { constructor(options) { const store = this; store._state = reactive({ data: options.state }); /** * 实现getters */ const _getters = options.getters; // {getter1: fn1, getter2: fn2} store.getters = {}; forEachValue(_getters, function (fn, key) { Object.defineProperty(store.getters, key, { get: computed(() => fn(store.state)), // 用 computed 对 getters 进行缓存 }); }); /** * 实现 mutation 和 actions */ store._mutations = Object.create(null); store._actions = Object.create(null); const _mutations = options.mutations; const _actions = options.actions; forEachValue(_mutations, (mutation, key) => { store._mutations[key] = (payload) => { mutation.call(store, store.state, payload); }; }); forEachValue(_actions, (action, key) => { store._actions[key] = (payload) => { action.call(store, store, payload); }; }); } /** * 实现 commit 和 dispatch * commit、dispatch必须写成箭头函数,来保证commit、dispatch里面的this指向store实例 */ commit = (type, payload) => { this._mutations[type](payload); }; dispatch = (type, payload) => { this._actions[type](payload); }; get state() { return this._state.data; } install(app, injectKey) { app.provide(injectKey || storeKey, this); app.config.globalProperties.$store = this; } }
源码解析
当项目变得复杂,我们就不得不使用 modules
让项目结构更清晰,更具可维护性;同时引入严格模式、插件系统、动态modules等功能。
ModuleCollection
modules
包含 rootModule
以及 options.modules
中的各个子模块,我们 期望将用户传入的所有 module
转化成以下树状结构,并存放到 store._modules
变量中 :
root = { _raw: rootModule, state: rootModule.state, _children: { aCount: { _raw: aModule, state: aModule.state, _children: { cCount: { _raw:cModule, state: cModule.state, _children:{} } }, }, bCount: { _raw: bModule, state: bModule.state, _children: {}, }, }, };
实现方式:
// vuex/store.js import { storeKey } from "./injectKey"; import ModuleCollection from "./module/module-collection"; export default class Store { constructor(options) { const store = this; // 1. modules 数据格式化 store._modules = new ModuleCollection(options); } install(app, injectKey) { app.provide(injectKey || storeKey, this); app.config.globalProperties.$store = this; } }
// module/module-collection.js import Module from "./module"; import { forEachValue } from "../utils"; export default class ModuleCollection { constructor(rootModule) { this.root = null; this.register(rootModule, []); } register(rawModule, path) { const newModule = new Module(rawModule); // 1. 如果是根模块 if (path.length === 0) { this.root = newModule; } else { // 2. 如果不是根模块,则设置父模块的 _children 属性 const parent = path.slice(0, -1).reduce((module, current) => { return module.getChild(current); }, this.root); // key 为 path 的最后一位 parent.addChild(path[path.length - 1], newModule); } // 递归处理 modules if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(rawChildModule, path.concat(key)); }); } } }
// module/module.js import { forEachValue } from "../utils"; export default class Module { constructor(rawModule) { this._raw = rawModule; this.state = rawModule.state; this._children = {}; } addChild(key, module) { this._children[key] = module; } getChild(key) { return this._children[key]; } forEachChild(fn) { forEachValue(this._children, fn); } }
installModule
另外,当我们取子 module
中的 state
时,采用的方式是:store.state.moduleA.count
,是直接从store.state
上链式获取的。我们 期望在 store._state
上包含所有 modules
中的数据,其结构如下 :
{ count: 0, moduleA: { count: 0 moduleC: { count: 0 } }, moduleB: { count: 0 } }
所以我们首先需要将 store._modules.root.state
插入各个模块的 state
之后,改造成上述结构:
// vuex/store.js function installModule(store, rootState, path, module) { let isRoot = !path.length; if (!isRoot) { let parentState = path .slice(0, -1) .reduce((state, key) => state[key], rootState); parentState[path[path.length - 1]] = module.state; } // 【遍历】 module._children,【递归】执行 installModule module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child); }); } export default class Store { constructor(options) { const store = this; store._modules = new ModuleCollection(options); // 2. 改造 store._modules.root.state const state = store._modules.root.state; // 根状态 installModule(store, state, [], store._modules.root); } }
resetStoreState
创建 store._wrappedGetters
、store._mutations
、store._actions
用来存储所有模块的 getters
、mutations
、actions
,期望的格式如下:
store: { // actions 和 mutations 都是数组格式 _actions: { 'moduleB/asyncAdd': [ ƒ ] }, _mutations: { 'moduleA/add': [ ƒ ] 'moduleA/moduleC/add': [ ƒ ] 'add': [ ƒ ] 'moduleB/add': [ ƒ ] } _wrappedGetters: { 'moduleB/plus': () => (...) 'double': () => (...) } }
具体实现:
// vuex/store.js // 根据路径,获取store上面的最新状态(因为store.state是响应式的,通过store.state.xx.xx获取的也是响应式的) function getNestedState(state, path) { return path.reduce((state, key) => state[key], state); } function isPromise(val) { return val && typeof val.then === "function"; } function installModule(store, rootState, path, module) { // 略... // getters module._raw.getters module.forEachGetter((getter, key) => { store._wrappedGetters[key] = () => { return getter(getNestedState(store.state, path)); // getter(module.state) 不可行,因为如果直接使用模块自己的状态,此状态不是响应式的 }; }); // mutation:{add: [mutation1,mutation2], double: [mutation3]} 不同modules中的同名mutation放到同一个数组中 module.forEachMutation((mutation, key) => { const entry = store._mutations[key] || (store._mutations[key] = []); entry.push((payload) => { // 也通过 getNestedState(store.state, path) 获取module的最新状态 mutation.call(store, getNestedState(store.state, path), payload); }); }); // action:【action执行完返回一个Promise】 module.forEachAction((action, key) => { const entry = store._actions[key] || (store._actions[key] = []); entry.push((payload) => { let res = action.call(store, store, payload); if (!isPromise(res)) { return Promise.resolve(res); } return res; }); }); // 【遍历】 module._children,【递归】执行各个module 的 installModule module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child); }); } export default class Store { constructor(options) { const store = this; // 在store上定义变量,用来存储getters、mutations、actions store._wrappedGetters = Object.create(null); store._mutations = Object.create(null); store._actions = Object.create(null); } }
// module/module.js import { forEachValue } from "../utils"; export default class Module { // ...略 forEachGetter(fn) { if (this._raw.getters) { forEachValue(this._raw.getters, fn); } } forEachMutation(fn) { if (this._raw.mutations) { forEachValue(this._raw.mutations, fn); } } forEachAction(fn) { if (this._raw.actions) { forEachValue(this._raw.actions, fn); } } }
然后执行 resetStoreState
,实现数据响应式,并创建getters
// vuex/store.js function resetStoreState(store, state) { // 由于state在状态持久化的时候可能会整体替换,为了维持响应式,给state包一层data属性 store._state = reactive({ data: state }); store.getters = {}; forEachValue(store._wrappedGetters, (getter, key) => { Object.defineProperty(store.getters, key, { enumerable: true, get: () => getter(), // 在vue3.2版本后,可以用 computed 对 getter 值进行缓存 }); }); } export default class Store { constructor(options) { const store = this; // 在store上定义变量,用来存储getters、mutations、actions store._wrappedGetters = Object.create(null); store._mutations = Object.create(null); store._actions = Object.create(null); store._modules = new ModuleCollection(options); const state = store._modules.root.state; installModule(store, state, [], store._modules.root); // state数据响应式、创建store.getters resetStoreState(store, state); } get state() { return this._state.data; } }
实现 commit
和 dispatch
:
export default class Store { // ...略 commit = (type, payload) => { const entry = this._mutations[type] || []; entry.forEach((handler) => handler(payload)); }; dispatch = (type, payload) => { const entry = this._actions[type] || []; // action 返回的是一个 Promise return Promise.all(entry.map((handler) => handler(payload))); }; }
namespaced
在没有设置命名空间的情况下,模块内部的 action
、 mutation
和 getters
是注册在全局命名空间的,这样可能会导致多个模块对同一个 action
或 mutation
作出响应。启用命名空间会让模块内部的状态拥有私有局部空间,不受其他模块影响。 首先修改 Module
类,增加一个 namespaced
属性:
// vuex/module/module.js export default class Module { constructor(rawModule) { this._raw = rawModule; this.state = rawModule.state; this._children = {}; this.namespaced = rawModule.namespaced; } }
然后创建 store._modules
实例的 getNamespaced
方法,用来获取 namespaced
路径,形如 moduleA/moduleC/
// vuex/module/module-collection.js export default class ModuleCollection { // ...略 // 获取 namespaced 的路径,形如 moduleA/moduleC/ getNamespaced(path) { let module = this.root; return path.reduce((namespacedStr, key) => { module = module.getChild(key); return namespacedStr + (module.namespaced ? key + "/" : ""); }, ""); } }
最后修改 store._mutations
、store._actions
、store.__wrappedGetters
中子模块相关的路径:
// vuex/store.js function installModule(store, rootState, path, module) { // 略... const namespaced = store._modules.getNamespaced(path); // getters module.forEachGetter((getter, key) => { store._wrappedGetters[namespaced + key] = () => { return getter(getNestedState(store.state, path)); }; }); // mutation module.forEachMutation((mutation, key) => { const entry = store._mutations[namespaced + key] || (store._mutations[namespaced + key] = []); entry.push((payload) => { mutation.call(store, getNestedState(store.state, path), payload); }); }); // action module.forEachAction((action, key) => { const entry = store._actions[namespaced + key] || (store._actions[namespaced + key] = []); entry.push((payload) => { let res = action.call(store, store, payload); if (!isPromise(res)) { return Promise.resolve(res); } return res; }); }); // ...略 }
严格模式
用户在 options
中通过 strict: true
开启严格模式;
- 在严格模式中,
mutation
只能执行同步操作 - 修改
store
的状态只能在mutation
中进行
实现严格模式的原理:
- 设置一个初始状态
_commiting
为 false;当执行fn回调时,将_commiting
设为true
,最后将_commiting
设为false
;如果fn
是同步的,那么在fn
中获取到的_commiting
就为true
,否则 在fn
中获取到的_commiting
为false
; - 如果没有通过
mutation
修改数据,那么_commiting
依然为初始值false
;
具体实现:
// vuex/store.js import { watch } from "vue"; function resetStoreState(store, state) { // ...略 if (store.strict) { enableStricMode(store); } } function enableStricMode(store) { // 监控数据变化 // 1. 如果是mutation同步修改数据,则 store._commiting 为 true,不会报错 // 2. 如果是mutation异步修改数据、或通过其它方式修改数据,则store._commiting 为 false,会报错 watch( () => store._state.data, () => { // 当第一个参数是false是,会打印出警告 console.assert( store._commiting, "do not mutate vuex store state outside mutation handlers" ); }, { deep: true, flush: "sync" } // watch 默认是异步的,这里改成同步(状态改变立刻执行回调)监听 ); } export default class Store { // 先把 this._commiting 改为 true,执行fn后,再将 this._commiting 改回去;如果fn是同步的,则在fn中this._commiting为true。 _withCommit(fn) { const commiting = this._commiting; this._commiting = true; fn(); this._commiting = commiting; } constructor(options) { // ...略 this.strict = options.strict || false; this._commiting = false; } commit = (type, payload) => { const entry = this._mutations[type] || []; this._withCommit(() => { entry.forEach((handler) => handler(payload)); }); }; }
插件系统
手写一个状态持久化插件:
// vuex插件就是一个函数 // 实现一个数据持久化插件 function persistedStatePlugin(store) { // 从缓存中读取数据,并替换store中的state let local = localStorage.getItem("VUEX:STATE"); if (local) { store.replaceState(JSON.parse(local)); } // 每当状态变化(执行了mutation),就会执行subscribe的回调 store.subscribe((mutation, state) => { // 缓存状态 localStorage.setItem("VUEX:STATE", JSON.stringify(state)); }); } export default createStore({ plugins: [persistedStatePlugin], })
该插件有几个重点:
- vuex插件本质上是一个函数,接收一个参数
store
store.replaceState()
方法会替换掉state
- 每当通过
mutation
修改了状态,都会执行store.subscribe(fn)
里的回调函数(发布订阅模式)
具体实现:
// vuex/store.js export default class Store { constructor(options) { // ...略 // 执行插件(本质是一个函数) store._subscribers = []; options.plugins.forEach((plugin) => plugin(store)); } subscribe(fn) { this._subscribers.push(fn); } replaceState(newState) { // 直接修改state会报错,所以使用 _withCommit 包裹一下 this._withCommit(() => { this._state.data = newState; }); } commit = (type, payload) => { const entry = this._mutations[type] || []; this._withCommit(() => { entry.forEach((handler) => handler(payload)); }); // 每次 commit 的时候执行所有的 subscribers this._subscribers.forEach((sub) => sub({ type, payload }, this.state)); }; }
store.registerModule
vuex 可以使用store.registerModule 动态注册modules,使用方式如下:
import { createStore } from "@/vuex"; const store = createStore({ // ...略 }) // 在moduleA内部创建一个moduleC store.registerModule(["moduleA", "moduleC"], { namespaced: true, state: { count: 0 }, mutations: { add(state, payload) { state.count += payload; }, }, }); export default store;
具体实现:
- 创建
store.registerModule
方法
export default class Store { registerModule(path, rawModule) { const store = this; if (typeof path === "string") { path = [path]; } // 1. 在原有模块基础上新增加一个module const newModule = store._modules.register(rawModule, path); // 2. 再把模块安装上 installModule(store, store.state, path, newModule); // 3. 重置容器 resetStoreState(store, store.state); } }
修改 ModuleCollection
的 register
方法,返回新的 newModule
export default class ModuleCollection { // ... register(rawModule, path) { const newModule = new Module(rawModule); // ...略 return newModule; } // ... }
在 installModule
中设置 parentState
的 state
时,使用 store._withCommit()
进行包裹,否则会警告(严格模式下)
function installModule(store, rootState, path, module) { if (!isRoot) { let parentState = path .slice(0, -1) .reduce((state, key) => state[key], rootState); store._withCommit(() => { parentState[path[path.length - 1]] = module.state; }); module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child); }); } }
最后
本篇主要是对 vuex4.0 源码的学习总结,源代码仓库可以查看 mini-vuex4。
以上就是简易vuex4核心原理及实现源码分析的详细内容,更多关于vuex4核心原理的资料请关注脚本之家其它相关文章!