簡易vuex4核心原理及實現(xiàn)源碼分析
前言
Vuex 是一個專為 Vue.js 應(yīng)用程序開發(fā)的 狀態(tài)管理模式 。它借鑒了Flux、redux的基本思想,將共享的數(shù)據(jù)抽離到全局,同時利用Vue.js的 響應(yīng)式 機制來進行高效的狀態(tài)管理與更新。想要掌握了解基礎(chǔ)知識可以查閱Vuex官網(wǎng),本篇主要是對 vuex4.x版本的源碼 進行研究分析。
Vuex 核心原理
使用方式
創(chuàng)建 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值,標(biāo)識 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是一個插件,所以創(chuàng)建的store實例需要實現(xiàn)一個install方法vuex4需要導(dǎo)出createStore,用于創(chuàng)建store,接收一個options對象,vuex4需要導(dǎo)出useStore,用于在組件中使用storestore是一個全局狀態(tài)庫,并且是響應(yīng)式的,可以在各個組件中使用store中的狀態(tài)- 可以創(chuàng)建多個
store實例,通過key標(biāo)識來區(qū)分不同的store
實現(xiàn)一個簡易版的 vuex
首先不考慮 modules、插件、嚴(yán)格模式、動態(tài)模塊等功能,實現(xiàn)一個簡易版的vuex; 該版本包含的功能有:
store的派發(fā)和注冊state的響應(yīng)式getters、mutations、actions、commit、dispatch- 通過
key標(biāo)識多個store
實現(xiàn) store 的派發(fā)和注冊、響應(yīng)式、injectKey
- 通過
provide/inject實現(xiàn)store的派發(fā)和注冊 - 通過
reactive實現(xiàn)state的響應(yīng)式 - 通過在
provide/inject時傳入injectKey,來標(biāo)識不同的store
import { inject, reactive } from "vue";
const storeKey = "store";
class Store {
constructor(options) {
const store = this;
// state 響應(yīng)式
// 做狀態(tài)持久化時需要整體替換state,為了保持state的響應(yīng)式,用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 實例
// 設(shè)置全局變量 $store
app.config.globalProperties.$store = this;
}
}
export function createStore(options) {
return new Store(options);
}
export function useStore(injectKey = storeKey) {
return inject(injectKey);
}
實現(xiàn) getters、mutations、actions、commit、dispatch
getters的實現(xiàn):將options.getters代理到store.getters,并傳入?yún)?shù)store.state;在vue3.2以上版本,可以使用computed實現(xiàn)getters的緩存。mutations的實現(xiàn):將options.mutations代理到store._mutations上,將mutation內(nèi)部的this指向store,并傳入?yún)?shù)store.state和payload;actions的實現(xiàn)類似。commit和dispatch的實現(xiàn):它們是一個函數(shù),通過傳入的type和payload匹配并執(zhí)行對應(yīng)的mutation和action
// 遍歷 obj,對每一項執(zhí)行 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 });
/**
* 實現(xiàn)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 進行緩存
});
});
/**
* 實現(xiàn) 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);
};
});
}
/**
* 實現(xiàn) commit 和 dispatch
* commit、dispatch必須寫成箭頭函數(shù),來保證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;
}
}
源碼解析
當(dāng)項目變得復(fù)雜,我們就不得不使用 modules 讓項目結(jié)構(gòu)更清晰,更具可維護性;同時引入嚴(yán)格模式、插件系統(tǒng)、動態(tài)modules等功能。
ModuleCollection
modules 包含 rootModule 以及 options.modules 中的各個子模塊,我們 期望將用戶傳入的所有 module 轉(zhuǎn)化成以下樹狀結(jié)構(gòu),并存放到 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: {},
},
},
};
實現(xiàn)方式:
// vuex/store.js
import { storeKey } from "./injectKey";
import ModuleCollection from "./module/module-collection";
export default class Store {
constructor(options) {
const store = this;
// 1. modules 數(shù)據(jù)格式化
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. 如果不是根模塊,則設(shè)置父模塊的 _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
另外,當(dāng)我們?nèi)∽?module 中的 state 時,采用的方式是:store.state.moduleA.count,是直接從store.state 上鏈?zhǔn)将@取的。我們 期望在 store._state 上包含所有 modules 中的數(shù)據(jù),其結(jié)構(gòu)如下 :
{
count: 0,
moduleA: {
count: 0
moduleC: {
count: 0
}
},
moduleB: {
count: 0
}
}
所以我們首先需要將 store._modules.root.state 插入各個模塊的 state 之后,改造成上述結(jié)構(gòu):
// 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,【遞歸】執(zhí)行 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; // 根狀態(tài)
installModule(store, state, [], store._modules.root);
}
}
resetStoreState
創(chuàng)建 store._wrappedGetters、store._mutations、store._actions 用來存儲所有模塊的 getters、mutations、actions,期望的格式如下:
store: {
// actions 和 mutations 都是數(shù)組格式
_actions: {
'moduleB/asyncAdd': [ ? ]
},
_mutations: {
'moduleA/add': [ ? ]
'moduleA/moduleC/add': [ ? ]
'add': [ ? ]
'moduleB/add': [ ? ]
}
_wrappedGetters: {
'moduleB/plus': () => (...)
'double': () => (...)
}
}
具體實現(xiàn):
// vuex/store.js
// 根據(jù)路徑,獲取store上面的最新狀態(tài)(因為store.state是響應(yīng)式的,通過store.state.xx.xx獲取的也是響應(yīng)式的)
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) 不可行,因為如果直接使用模塊自己的狀態(tài),此狀態(tài)不是響應(yīng)式的
};
});
// mutation:{add: [mutation1,mutation2], double: [mutation3]} 不同modules中的同名mutation放到同一個數(shù)組中
module.forEachMutation((mutation, key) => {
const entry = store._mutations[key] || (store._mutations[key] = []);
entry.push((payload) => {
// 也通過 getNestedState(store.state, path) 獲取module的最新狀態(tài)
mutation.call(store, getNestedState(store.state, path), payload);
});
});
// action:【action執(zhí)行完返回一個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,【遞歸】執(zhí)行各個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);
}
}
}
然后執(zhí)行 resetStoreState ,實現(xiàn)數(shù)據(jù)響應(yīng)式,并創(chuàng)建getters
// vuex/store.js
function resetStoreState(store, state) {
// 由于state在狀態(tài)持久化的時候可能會整體替換,為了維持響應(yīng)式,給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數(shù)據(jù)響應(yīng)式、創(chuàng)建store.getters
resetStoreState(store, state);
}
get state() {
return this._state.data;
}
}
實現(xiàn) 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
在沒有設(shè)置命名空間的情況下,模塊內(nèi)部的 action、 mutation 和 getters 是注冊在全局命名空間的,這樣可能會導(dǎo)致多個模塊對同一個 action 或 mutation 作出響應(yīng)。啟用命名空間會讓模塊內(nèi)部的狀態(tài)擁有私有局部空間,不受其他模塊影響。 首先修改 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;
}
}
然后創(chuàng)建 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 中子模塊相關(guān)的路徑:
// 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;
});
});
// ...略
}
嚴(yán)格模式
用戶在 options 中通過 strict: true 開啟嚴(yán)格模式;
- 在嚴(yán)格模式中,
mutation只能執(zhí)行同步操作 - 修改
store的狀態(tài)只能在mutation中進行
實現(xiàn)嚴(yán)格模式的原理:
- 設(shè)置一個初始狀態(tài)
_commiting為 false;當(dāng)執(zhí)行fn回調(diào)時,將_commiting設(shè)為true,最后將_commiting設(shè)為false;如果fn是同步的,那么在fn中獲取到的_commiting就為true,否則 在fn中獲取到的_commiting為false; - 如果沒有通過
mutation修改數(shù)據(jù),那么_commiting依然為初始值false;
具體實現(xiàn):
// vuex/store.js
import { watch } from "vue";
function resetStoreState(store, state) {
// ...略
if (store.strict) {
enableStricMode(store);
}
}
function enableStricMode(store) {
// 監(jiān)控數(shù)據(jù)變化
// 1. 如果是mutation同步修改數(shù)據(jù),則 store._commiting 為 true,不會報錯
// 2. 如果是mutation異步修改數(shù)據(jù)、或通過其它方式修改數(shù)據(jù),則store._commiting 為 false,會報錯
watch(
() => store._state.data,
() => {
// 當(dāng)?shù)谝粋€參數(shù)是false是,會打印出警告
console.assert(
store._commiting,
"do not mutate vuex store state outside mutation handlers"
);
},
{ deep: true, flush: "sync" } // watch 默認(rèn)是異步的,這里改成同步(狀態(tài)改變立刻執(zhí)行回調(diào))監(jiān)聽
);
}
export default class Store {
// 先把 this._commiting 改為 true,執(zhí)行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));
});
};
}
插件系統(tǒng)
手寫一個狀態(tài)持久化插件:
// vuex插件就是一個函數(shù)
// 實現(xiàn)一個數(shù)據(jù)持久化插件
function persistedStatePlugin(store) {
// 從緩存中讀取數(shù)據(jù),并替換store中的state
let local = localStorage.getItem("VUEX:STATE");
if (local) {
store.replaceState(JSON.parse(local));
}
// 每當(dāng)狀態(tài)變化(執(zhí)行了mutation),就會執(zhí)行subscribe的回調(diào)
store.subscribe((mutation, state) => {
// 緩存狀態(tài)
localStorage.setItem("VUEX:STATE", JSON.stringify(state));
});
}
export default createStore({
plugins: [persistedStatePlugin],
})
該插件有幾個重點:
- vuex插件本質(zhì)上是一個函數(shù),接收一個參數(shù)
store store.replaceState()方法會替換掉state- 每當(dāng)通過
mutation修改了狀態(tài),都會執(zhí)行store.subscribe(fn)里的回調(diào)函數(shù)(發(fā)布訂閱模式)
具體實現(xiàn):
// vuex/store.js
export default class Store {
constructor(options) {
// ...略
// 執(zhí)行插件(本質(zhì)是一個函數(shù))
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 的時候執(zhí)行所有的 subscribers
this._subscribers.forEach((sub) => sub({ type, payload }, this.state));
};
}
store.registerModule
vuex 可以使用store.registerModule 動態(tài)注冊modules,使用方式如下:
import { createStore } from "@/vuex";
const store = createStore({
// ...略
})
// 在moduleA內(nèi)部創(chuàng)建一個moduleC
store.registerModule(["moduleA", "moduleC"], {
namespaced: true,
state: { count: 0 },
mutations: {
add(state, payload) {
state.count += payload;
},
},
});
export default store;
具體實現(xiàn):
- 創(chuàng)建
store.registerModule方法
export default class Store {
registerModule(path, rawModule) {
const store = this;
if (typeof path === "string") {
path = [path];
}
// 1. 在原有模塊基礎(chǔ)上新增加一個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 中設(shè)置 parentState 的 state 時,使用 store._withCommit() 進行包裹,否則會警告(嚴(yán)格模式下)
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 源碼的學(xué)習(xí)總結(jié),源代碼倉庫可以查看 mini-vuex4。
以上就是簡易vuex4核心原理及實現(xiàn)源碼分析的詳細(xì)內(nèi)容,更多關(guān)于vuex4核心原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue3中Element Plus Table(表格)點擊獲取對應(yīng)id方式
這篇文章主要介紹了Vue3中Element Plus Table(表格)點擊獲取對應(yīng)id方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-10-10
解決修復(fù)報錯Error in render:TypeError:Cannot read&n
這篇文章主要介紹了解決修復(fù)報錯Error in render:TypeError:Cannot read properties of undefined(reading ‘ipconfig‘)問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03
在vue中獲取wangeditor的html和text的操作
這篇文章主要介紹了在vue中獲取wangeditor的html和text的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10

