簡易vuex4核心原理及實現(xiàn)源碼分析
前言
Vuex 是一個專為 Vue.js 應用程序開發(fā)的 狀態(tài)管理模式 。它借鑒了Flux、redux的基本思想,將共享的數(shù)據(jù)抽離到全局,同時利用Vue.js的 響應式 機制來進行高效的狀態(tài)管理與更新。想要掌握了解基礎知識可以查閱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值,標識 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
需要導出createStore
,用于創(chuàng)建store
,接收一個options
對象,vuex4
需要導出useStore
,用于在組件中使用store
store
是一個全局狀態(tài)庫,并且是響應式的,可以在各個組件中使用store
中的狀態(tài)- 可以創(chuàng)建多個
store
實例,通過key
標識來區(qū)分不同的store
實現(xiàn)一個簡易版的 vuex
首先不考慮 modules
、插件、嚴格模式、動態(tài)模塊等功能,實現(xiàn)一個簡易版的vuex; 該版本包含的功能有:
store
的派發(fā)和注冊state
的響應式getters
、mutations
、actions
、commit
、dispatch
- 通過
key
標識多個store
實現(xiàn) store 的派發(fā)和注冊、響應式、injectKey
- 通過
provide/inject
實現(xiàn)store
的派發(fā)和注冊 - 通過
reactive
實現(xiàn)state
的響應式 - 通過在
provide/inject
時傳入injectKey
,來標識不同的store
import { inject, reactive } from "vue"; const storeKey = "store"; class Store { constructor(options) { const store = this; // state 響應式 // 做狀態(tài)持久化時需要整體替換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); }
實現(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
內部的this
指向store
,并傳入?yún)?shù)store.state
和payload
;actions
的實現(xiàn)類似。commit
和dispatch
的實現(xiàn):它們是一個函數(shù),通過傳入的type
和payload
匹配并執(zhí)行對應的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; } }
源碼解析
當項目變得復雜,我們就不得不使用 modules
讓項目結構更清晰,更具可維護性;同時引入嚴格模式、插件系統(tǒng)、動態(tài)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: {}, }, }, };
實現(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. 如果不是根模塊,則設置父模塊的 _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
中的數(shù)據(jù),其結構如下 :
{ 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,【遞歸】執(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是響應式的,通過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) 不可行,因為如果直接使用模塊自己的狀態(tài),此狀態(tài)不是響應式的 }; }); // 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ù)響應式,并創(chuàng)建getters
// vuex/store.js function resetStoreState(store, state) { // 由于state在狀態(tài)持久化的時候可能會整體替換,為了維持響應式,給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ù)響應式、創(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
在沒有設置命名空間的情況下,模塊內部的 action
、 mutation
和 getters
是注冊在全局命名空間的,這樣可能會導致多個模塊對同一個 action
或 mutation
作出響應。啟用命名空間會讓模塊內部的狀態(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
中子模塊相關的路徑:
// 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
只能執(zhí)行同步操作 - 修改
store
的狀態(tài)只能在mutation
中進行
實現(xiàn)嚴格模式的原理:
- 設置一個初始狀態(tài)
_commiting
為 false;當執(zhí)行fn回調時,將_commiting
設為true
,最后將_commiting
設為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, () => { // 當?shù)谝粋€參數(shù)是false是,會打印出警告 console.assert( store._commiting, "do not mutate vuex store state outside mutation handlers" ); }, { deep: true, flush: "sync" } // watch 默認是異步的,這里改成同步(狀態(tài)改變立刻執(zhí)行回調)監(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)); } // 每當狀態(tài)變化(執(zhí)行了mutation),就會執(zhí)行subscribe的回調 store.subscribe((mutation, state) => { // 緩存狀態(tài) localStorage.setItem("VUEX:STATE", JSON.stringify(state)); }); } export default createStore({ plugins: [persistedStatePlugin], })
該插件有幾個重點:
- vuex插件本質上是一個函數(shù),接收一個參數(shù)
store
store.replaceState()
方法會替換掉state
- 每當通過
mutation
修改了狀態(tài),都會執(zhí)行store.subscribe(fn)
里的回調函數(shù)(發(fā)布訂閱模式)
具體實現(xiàn):
// vuex/store.js export default class Store { constructor(options) { // ...略 // 執(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內部創(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. 在原有模塊基礎上新增加一個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核心原理及實現(xiàn)源碼分析的詳細內容,更多關于vuex4核心原理的資料請關注腳本之家其它相關文章!
相關文章
Vue3中Element Plus Table(表格)點擊獲取對應id方式
這篇文章主要介紹了Vue3中Element Plus Table(表格)點擊獲取對應id方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-10-10解決修復報錯Error in render:TypeError:Cannot read&n
這篇文章主要介紹了解決修復報錯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