vuex原理以及實現

vuex官方文檔

Vuex是什么?

Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件的狀態,并以相應的規則保證狀態以一種可預測的方式發生變化

每一個 Vuex 應用的核心就是 store(倉庫)。“store”基本上就是一個容器,它包含著你的應用中大部分的狀態 (state)。Vuex 和單純的全局對象有以下兩點不同:

  1. Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那么相應的組件也會相應地得到高效更新。
  2. 你不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。這樣使得我們可以方便地跟蹤每一個狀態的變化,從而讓我們能夠實現一些工具幫助我們更好地了解我們的應用。

實現簡易版的vuex

先來看下vuex的基本用法

import Vue from 'vue';
import Vuex from 'vuex';
// 1.Vue.use(Vuex);  Vuex是一個對象 install方法
// 2.Vuex中有一個Store類 
// 3.混入到組件中 增添store屬性Vue.use(Vuex); // 使用這個插件  內部會調用Vuex中的install方法const store = new Vuex.Store({state:{ // -> dataage:10},getters:{ // 計算屬性myAge(state){return state.age + 20}},mutations:{ // method=> 同步的更改state  mutation的參數是狀態changeAge(state,payload){state.age += payload; // 更新age屬性}},actions:{ // 異步操作做完后將結果提交給mutationschangeAge({commit},payload){setTimeout(() => {commit('changeAge',payload)}, 1000);}}
});
export default store;

通過用法可以知道:

  1. Vuex是一個對象,它作為vue的插件,必然有install方法;
  2. Vuex中有一個Store類,在使用的時候有使用new;
  3. 需要將store混入到組件中。

于是可以梳理好入口文件

vuex/index.js

import { Store, install } from './store'; // 這個文件是入口文件,核心就是導出所有寫好的方法
export default {Store,install
}

store文件

vuex/store.js

export let Vue;export class Store {}// _vue 是Vue的構造函數
export const install = (_vue) => {// 需要保存Vue,用戶傳入的Vue構造函數Vue = _vue; 
}

接下來就是把store掛載到每個組件上面,這樣數據才能互通共享,很顯然,通過Vue.mixin 在Vue生命周期beforeCreate 可以為每個組件注入store;

import applyMixin from "./mixin";
export let Vue;export class Store {}// _vue 是Vue的構造函數
export const install = (_vue) => {// 需要保存Vue,用戶傳入的Vue構造函數Vue = _vue;// 需要將根組件中注入的store 分派給每一個組件 (子組件) Vue.mixinapplyMixin(Vue);
}

vuex/mixin.js

export default function applyMixin(Vue) {// 父子組件的beforecreate執行順序Vue.mixin({ // 內部會把生命周期函數 拍平成一個數組 beforeCreate: vuexInit});
}// 組件渲染時從父=》子function vuexInit() {// 給所有的組件增加$store 屬性 指向我們創建的store實例const options = this.$options; // 獲取用戶所有的選項if (options.store) { // 根實例(只有根實例才會有store屬性)this.$store = options.store;} else if (options.parent && options.parent.$store) { // 兒子 或者孫子....// 后面的每一個都從其父組件拿到storethis.$store = options.parent.$store;}
}

接下來就是處理state,getters,mutations,actions

state實現

export class Store {constructor(options) {const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)this._vm = new Vue({data: { // 屬性如果是通過$開頭的 默認不會將這個屬性掛載到vm上$$store: state}})}get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})return this._vm._data.$$state}}

首先來處理state,options是用戶傳入的,其中有state,getters,mutations,actions,自然可以在options.state中取到,但是此時state還不是響應式,可以借助new Vue中data的數據是響應式處理這個問題,將state掛載到$$state上,這個屬性是不會被vue暴露出去(可能是內部做了處理)。當我們在組件中去獲取值的時候,比如this.store.state.age時候 this.store.state 就走到到了訪問器get state() 就會將整個倉庫的state返回出去,而且數據是響應式的。至于為什么在_vm._data上,需要去看下vue源碼實現。

getters實現

export class Store {constructor(options) {// 1.處理stateconst state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)this._vm = new Vue({data: { // 屬性如果是通過$開頭的 默認不會將這個屬性掛載到vm上$$store: state}})// 2.處理getters屬性 具有緩存的 computed 帶有緩存 (多次取值是如果值不變是不會重新取值)this.getters = {};Object.key(options.getters).forEach(key => {Object.defineProperty(this.getters, key, {get: () => options.getters[key](this.state)})})}get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})return this._vm._data.$$state}}

通過循環用戶傳進來的getters,再通過Object.defineProperty把每一個getter放入store中。不過目前每一次取值都會重新計算,沒有緩存功能,不符合vue計算屬性的用法以及定義。

先來改造下對象遍歷這個方法,因為這個方法后面用的比較多。

vuex/util.js

export const forEachValue = (obj, callback) => {Object.keys(obj).forEach(key => callback(obj[key], key))
}
export class Store {constructor(options) {// 1.處理stateconst state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)this._vm = new Vue({data: { // 屬性如果是通過$開頭的 默認不會將這個屬性掛載到vm上$$store: state}})// 2.處理getters屬性 具有緩存的 computed 帶有緩存 (多次取值是如果值不變是不會重新取值)this.getters = {};forEachValue(options.getters, (fn, key) => {Object.defineProperty(this.getters, key, {get: () => fn(this.state)})})}get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})return this._vm._data.$$state}}

邏輯都是一樣的,接著處理下緩存功能。

export class Store {constructor(options) {// 1.處理stateconst state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)const computed = {};// 2.處理getters屬性 具有緩存的 computed 帶有緩存 (多次取值是如果值不變是不會重新取值)this.getters = {};forEachValue(options.getters, (fn, key) => {// 將用戶的getters 定義在實例上, 計算屬性是如何實現緩存computed[key] = () => fn(this.state);// 當取值的時候執行計算屬性的邏輯,此時就有緩存功能Object.defineProperty(this.getters, key, {get: () => fn(this._vm[key])})})this._vm = new Vue({data: { // 屬性如果是通過$開頭的 默認不會將這個屬性掛載到vm上$$store: state},computed,})}get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})return this._vm._data.$$state}}

computed具有緩存功能,可以在用戶傳入的getters的時候,將用戶的getters 定義在實例上,computed[key] = () => fn(this.state) ,在取值的時候fn(this._vm[key])執行計算屬性的邏輯。vuex的作者真是腦洞大開,鬼才啊,這都能想到。

mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)

  • 對傳入的屬性進行遍歷訂閱

  • 通過commit方法觸發調用。

mutation實現

// 3.實現mutations
this.mutations = {};
forEachValue(options.mutations, (fn, key) => {this.mutations[key] = (payload) => fn(this.state, payload)
})commit = (type, payload) => { //保證當前this 當前store實例this.mutations[type](payload)
}

commit使用箭頭函數是為了保證調用的都是當前實例,一是通過this.commit(type,data),二是在action中被解構使用changeAge({commit},payload){}

actions和dispath也是如此。

完整的Store類

export class Store {constructor(options) {// 1.處理stateconst state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)const computed = {};// 2.處理getters屬性 具有緩存的 computed 帶有緩存 (多次取值是如果值不變是不會重新取值)this.getters = {};forEachValue(options.getters, (fn, key) => {// 將用戶的getters 定義在實例上, 計算屬性是如何實現緩存computed[key] = () => fn(this.state);// 當取值的時候執行計算屬性的邏輯,此時就有緩存功能Object.defineProperty(this.getters, key, {get: () => fn(this._vm[key])})})this._vm = new Vue({data: { // 屬性如果是通過$開頭的 默認不會將這個屬性掛載到vm上$$store: state},computed,})// 3.實現mutationsthis.mutations = {};forEachValue(options.mutations, (fn, key) => {this.mutations[key] = (payload) => fn(this.state, payload)})// 4.實現actionsthis.actions = {};forEachValue(options.actions, (fn, key) => {this.actions[key] = (payload) => fn(this, payload);});}commit = (type, payload) => { //保證當前this 當前store實例this.mutations[type](payload)}dispatch = (type, payload) => {this.mutations[type](payload)}get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})return this._vm._data.$$state}}

完整的store.js

import applyMixin from "./mixin";
import { forEachValue } from './util';
export let Vue;export class Store {constructor(options) {// 1.處理stateconst state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)const computed = {};// 2.處理getters屬性 具有緩存的 computed 帶有緩存 (多次取值是如果值不變是不會重新取值)this.getters = {};forEachValue(options.getters, (fn, key) => {// 將用戶的getters 定義在實例上, 計算屬性是如何實現緩存computed[key] = () => fn(this.state);// 當取值的時候執行計算屬性的邏輯,此時就有緩存功能Object.defineProperty(this.getters, key, {get: () => fn(this._vm[key])})})this._vm = new Vue({data: { // 屬性如果是通過$開頭的 默認不會將這個屬性掛載到vm上$$store: state},computed,})// 3.實現mutationsthis.mutations = {};forEachValue(options.mutations, (fn, key) => {this.mutations[key] = (payload) => fn(this.state, payload)})// 4.實現actionsthis.actions = {};forEachValue(options.actions, (fn, key) => {this.actions[key] = (payload) => fn(this, payload);});}commit = (type, payload) => { //保證當前this 當前store實例this.mutations[type](payload)}dispatch = (type, payload) => {this.mutations[type](payload)}get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})return this._vm._data.$$state}}// _vue 是Vue的構造函數
export const install = (_vue) => {// 需要保存Vue,用戶傳入的Vue構造函數Vue = _vue;// 需要將根組件中注入的store 分派給每一個組件 (子組件) Vue.mixinapplyMixin(Vue);
}

簡易版的vuex到此完成。接下來就是要處理module。

完整版Vuex實現

我們實現了一個簡易版的Vuex,對state,actions,mutations,getters 進行了功能的實現。但是沒有對modules進行處理,其實modules才是Vuex中最核心并且是最難實現的。

Vuex 允許我們將 store 分割成大大小小的對象,每個對象也都擁有自己的 state、getter、mutation、action,這個對象我們把它叫做 module(模塊),在模塊中還可以繼續嵌套子模塊。

  • state: 所有模塊中的state中數據最終都會嵌套在一棵樹上。類似于如下
image
  • 模塊內部的 action、mutation 和 getter 默認可是注冊在全局命名空間的,這樣使得多個模塊能夠對同一 mutation 或 action 作出響應。因此在訂閱mutation 和action時必須存儲在數組中,每次觸發,數組中的方法都要執行。

    image

Vuex中可以為每個模塊添加namespaced: true來標記為當前模塊劃分一個命名空間,接下來看下具體怎么實現一個完整的Vuex。

具體實現

總體思路可以分為以下:

  1. 模塊收集。就是把用戶傳給store的數據進行格式化,格式化成我們想要的結構(樹)
  2. 安裝模塊。需要將子模塊通過模塊名定義在跟模塊上
  3. 把狀態state和getters定義到當前的vm上。

模塊收集

import ModuleCollection from './module/module-collection'
export let Vue;export class Store {constructor(options) {const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)// 1.模塊收集this._modules = new ModuleCollection(options);}
}

ModuleCollection 類的實現

這個類是收集模塊,格式化數據用的,那我們先要知道需要什么樣的格式。

this.root = {_raw: '根模塊',_children:{a:{_raw:"a模塊",_children:{c:{.....}},state:'a的狀態'  },b:{_raw:"b模塊",_children:{},state:'b的狀態'  }},state:'根模塊自己的狀態'}

最終需要的是這樣一個數結構。

export default class ModuleCollection {constructor(options) {// 注冊模塊 需要用到棧結構數據,[根,a],每次循環遞歸的時候將其入棧。這樣每個模塊可以清楚的知道自己的父級是誰this.register([], options)}register(path, rootModule) {// 格式化后的結果let newModule = { _raw: rootModule, // 用戶定義的模塊_children: {}, // 模塊的兒子state: {} // 當前模塊的狀態}if (path.length === 0) { // 說明是根模塊this.root = newModule} // 用戶在模塊中傳了modules屬性if (rootModule.modules) {// 循環模塊 module模塊的定義 moduleName模塊的名字forEachValue(rootModule.modules, (module, moduleName) => {this.register(path.concat(moduleName), module)})}}
}

第一次進來的時候path是空數組,root就是用戶傳進去的模塊對象;如果模塊有modules屬性,需要循環去注冊這個模塊。path.concat(moduleName) 就返回了[a,c]類似的格式。 接下來看下path不為空的時候

if (path.length === 0) { // 說明是根模塊this.root = newModule
} else {// this.register(path.concat(moduleName), module); 遞歸注冊前會把module 的名放在 path的位this.root._children[path[path.length -1]] = newModule
}

path[path.length -1] 可以取到最后一項,也就是模塊的兒子模塊。這里我們用的是this.root._children[path[path.length -1]] = newModule。這樣寫會把有多層路徑的模塊最后一項也提到和它平級,因此需要確定這個模塊的父級是誰,再把當前模塊掛到父級就okl了

if (path.length === 0) { // 說明是根模塊this.root = newModule
} else {// this.register(path.concat(moduleName), module); 遞歸注冊前會把module 的名放在 path的位// path.splice(0, -1) 是最后一項,是需要被掛的模塊let parent = path.splice(0, -1).reduce((memo, current) => {return memo._children[current];}, this.root);parent._children[path[path.length - 1]] = newModule
}

模塊的安裝

將所有module收集后需要對收集到數據進行整理

  • state數據要合并。 通過Vue.set(parent,path[path.length-1],rootModule.state),既可以合并,又能使使 module數據成為響應式數據;
  • action 和mutation 中方法訂閱(數組)
// 1.模塊收集
this._modules = new ModuleCollection(options);// 2.安裝模塊 根模塊的狀態中 要將子模塊通過模塊名 定義在根模塊上
installModule(this, state, [], this._modules.root);

this就是store, 需要完成installModule方法。installModule中傳入的有當前模塊,這個模塊可能有自己的方法。為此先改造下代碼,創建Module類。

import { forEachValue } from '../util';class Module {get namespaced() {return !!this._raw.namespaced}constructor(newModule) {this._raw = newModule;this._children = {};this.state = newModule.state}getChild(key) {return this._children[key];}addChild(key, module) {this._children[key] = module}// 給模塊繼續擴展方法}export default Module;

ModuleCollection中相應的地方稍作修改。

import Module from './module'export default class ModuleCollection {constructor(options) {// 注冊模塊 需要用到棧結構數據,[根,a],每次循環遞歸的時候將其入棧。這樣每個模塊可以清楚的知道自己的父級是誰this.register([], options)}register(path, rootModule) {// 格式化后的結果let newModule = new Module(rootModule)if (path.length === 0) { // 說明是根模塊this.root = newModule} else {// this.register(path.concat(moduleName), module); 遞歸注冊前會把module 的名放在 path的位// path.splice(0, -1) 是最后一項,是需要被掛的模塊let parent = path.splice(0, -1).reduce((memo, current) => {return memo.getChild(current);}, this.root);parent.addChild(path[path.length - 1], newModule)}// 用戶在模塊中傳了modules屬性if (rootModule.modules) {// 循環模塊 module模塊的定義 moduleName模塊的名字forEachValue(rootModule.modules, (module, moduleName) => {this.register(path.concat(moduleName), module)})}}
}
function installModule(store, rootState, path, module) {// 這里我需要遍歷當前模塊上的 actions、mutation、getters 都把他定義在store的_actions, _mutations, _wrappedGetters 中}

installModule 就需要循環對當前模塊處理對應的actions、mutation、getters。為此可以對Module類增加方法,來讓其內部自己處理。

import { forEachValue } from '../util';class Module {constructor(newModule) {this._raw = newModule;this._children = {};this.state = newModule.state}getChild(key) {return this._children[key];}addChild(key, module) {this._children[key] = module}// 給模塊繼續擴展方法forEachMutation(fn) {if (this._raw.mutations) {forEachValue(this._raw.mutations, fn)}}forEachAction(fn) {if (this._raw.actions) {forEachValue(this._raw.actions, fn);}}forEachGetter(fn) {if (this._raw.getters) {forEachValue(this._raw.getters, fn);}}forEachChild(fn) {forEachValue(this._children, fn);}
}export default Module;
function installModule(store, rootState, path, module) {// 這里我需要遍歷當前模塊上的 actions、mutation、getters 都把他定義在store的_actions, _mutations, _wrappedGetters 中// 處理mutationmodule.forEachMutation((mutation, key) => {store._mutations[key] = (store._mutations[key] || [])store._mutations[key].push((payload) => {mutation.call(store, module.state, payload)})})// 處理actionmodule.forEachAction((action, key) => {store._actions[key] = (store._actions[key] || [])store._actions[key].push((payload) => {action.call(store, store, payload)})})// 處理gettermodule.forEachGetter((getter, key) => {store._wrappedGetters[key] = function() {return getter(module.state)}})// 處理childrenmodule.forEachChild((child, key) => {// 遞歸加載installModule(store, rootState, path.concat(key), child)})}

此時,已經把每個模塊的actions、mutation、getters都掛到了store上,接下來需要對state處理。

// 將所有的子模塊的狀態安裝到父模塊的狀態上
// 需要注意的是vuex 可以動態的添加模塊
if (path.length > 0) {let parent = path.slice(0, -1).reduce((memo, current) => {return memo[current]}, rootState)// 如果這個對象本身不是響應式的 那么Vue.set 就相當于  obj[屬性 ]= 值Vue.set(parent, path[path.length - 1], module.state);
}

到此已經完成模塊的安裝,接下里是要把這些放到Vue實例上面

模塊與實例的關聯

constructor(options) {const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)this._mutations = {};this._actions = {};this._wrappedGetters = {};// 1.模塊收集this._modules = new ModuleCollection(options);// 2.安裝模塊 根模塊的狀態中 要將子模塊通過模塊名 定義在根模塊上installModule(this, state, [], this._modules.root);// 3,將狀態和getters 都定義在當前的vm上resetStoreVM(this, state);}
function resetStoreVM(store, state) {const computed = {}; // 定義計算屬性store.getters = {}; // 定義store中的gettersforEachValue(store._wrappedGetters, (fn, key) => {computed[key] = () => {return fn();}Object.defineProperty(store.getters, key, {get: () => store._vm[key] // 去計算屬性中取值});})store._vm = new Vue({data: {$$state: state},computed // 計算屬性有緩存效果});
}

相對應的Store類做以下修改

export class Store {constructor(options) {const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)this._mutations = {};this._actions = {};this._wrappedGetters = {};// 1.模塊收集this._modules = new ModuleCollection(options);// 2.安裝模塊 根模塊的狀態中 要將子模塊通過模塊名 定義在根模塊上installModule(this, state, [], this._modules.root);// 3,將狀態和getters 都定義在當前的vm上resetStoreVM(this, state);}commit = (type, payload) => { //保證當前this 當前store實例this._mutations[type].forEach(mutation => mutation.call(this, payload))}dispatch = (type, payload) => {this._actions[type].forEach(action => action.call(this, payload))}get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})return this._vm._data.$$state}}

命名空間nameSpaced

默認情況下,模塊內部的 action、mutation 和 getter 是注冊在全局命名空間的——這樣使得多個模塊能夠對同一 mutation 或 action 作出響應。

如果希望你的模塊具有更高的封裝度和復用性,你可以通過添加 namespaced: true 的方式使其成為帶命名空間的模塊。當模塊被注冊后,它的所有 getter、action 及 mutation 都會自動根據模塊注冊的路徑調整命名。

[圖片上傳失敗...(image-a03e51-1599730267452)]

平常寫上面基本上都要加上 namespaced,防止命名沖突,方法重復多次執行。現在就算每個 modules 的方法命一樣,也默認回加上這個方法別包圍的所有父結點的 key,核心就是 path 變量,在安裝模塊的時候把path處理下:

// 我要給當前訂閱的事件 增加一個命名空間
let namespace = store._modules.getNamespaced(path); // 返回前綴即可

store._modules就是模塊收集好的模塊,給它增加一個獲取命名空間的方法。

給ModuleCollection類增加一個getNamespaced方法,其參數就是path。

// 獲取命名空間, 返回一個字符串
getNamespaced(path) {let root = this.root; // 從根模塊找起來return path.reduce((str, key) => { // [a,c]root = root.getChild(key); // 不停的去找當前的模塊return str + (root.namespaced ? key + '/' : '')}, ''); // 參數就是一個字符串
}

當然Module類也需要增加一個屬性訪問器

get namespaced() {return !!this._raw.namespaced
}

接下來就是在處理mutation,action,getters的時候key的值加上namespace就可以了。

// 處理mutation
module.forEachMutation((mutation, key) => {store._mutations[namespace + key] = (store._mutations[namespace + key] || [])store._mutations[namespace + key].push((payload) => {mutation.call(store, module.state, payload)})
})// 處理action
module.forEachAction((action, key) => {store._actions[namespace + key] = (store._actions[namespace + key] || [])store._actions[namespace + key].push((payload) => {action.call(store, store, payload)})
})// 處理getter
module.forEachGetter((getter, key) => {store._wrappedGetters[namespace + key] = function() {return getter(module.state)}
})

namespaces 核心就是對數據格式的處理,來進行發布與訂閱。

插件

Vuex 的 store 接受 plugins 選項,這個選項暴露出每次 mutation 的鉤子。Vuex 插件就是一個函數,它接收 store 作為唯一參數

使用的時候:

const store = new Vuex.Store({// ...plugins: [myPlugin]
})

在插件中不允許直接修改狀態——類似于組件,只能通過提交 mutation 來觸發變化

先來看下一個vuex本地持久化的一個插件

function persists() {return function(store) { // store是當前默認傳遞的let data = localStorage.getItem('VUEX:STATE');if (data) {store.replaceState(JSON.parse(data));}store.subscribe((mutation, state) => {localStorage.setItem('VUEX:STATE', JSON.stringify(state));})}
}

插件返回一個函數,函數的參數就是store。其中replaceState, subscribe是關鍵點,也是vuex其中的2個api,接下來實現一下這2個方法。

export class Store {constructor(options) {const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)this._mutations = {};this._actions = {};this._wrappedGetters = {};// 1.模塊收集this._modules = new ModuleCollection(options);// 2.安裝模塊 根模塊的狀態中 要將子模塊通過模塊名 定義在根模塊上installModule(this, state, [], this._modules.root);// 3,將狀態和getters 都定義在當前的vm上resetStoreVM(this, state);// 插件內部會依次執行options.plugins.forEach(plugin=>plugin(this));}commit = (type, payload) => { //保證當前this 當前store實例this._mutations[type].forEach(mutation => mutation.call(this, payload))}dispatch = (type, payload) => {this._actions[type].forEach(action => action.call(this, payload))}get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})return this._vm._data.$$state}}

options.plugins.forEach(plugin=>plugin(this))就是讓所有插件依次執行,參數就是store.

this._subscribes = [];
// ...
subscribe(fn){this._subscribes.push(fn);
}

subscribe就介紹一個函數,放入到一個數組或者隊列中去。

// 處理mutation
module.forEachMutation((mutation, key) => {store._mutations[namespace + key] = (store._mutations[namespace + key] || [])store._mutations[namespace + key].push((payload) => {mutation.call(store, module.state, payload)store._subscribes.forEach(fn => {fn(mutation, rootState)})})
})

相應的在安裝模塊處理mutation的時候,需要讓訂閱的store._subscribes執行。fn的參數就是mutation和根狀態。

replaceState(state){// 替換掉最新的狀態this._vm._data.$$state = state
}

這是最簡單的改變狀態的方法,但此時雖然是ok的,但是mutation提交的還是舊值,mutation.call(store, module.state, payload)這個地方還是有點問題,module.state拿到的不是最新的狀態。

function getState(store, path) { // 獲取最新的狀態 可以保證視圖更新return path.reduce((newState, current) => {return newState[current];}, store.state);
}

可以通過這個方法能獲取到最新的轉態,相應的在處理mutation,getters的地方做相應調整。

// 處理mutation
module.forEachMutation((mutation, key) => {store._mutations[namespace + key] = (store._mutations[namespace + key] || [])store._mutations[namespace + key].push((payload) => {mutation.call(store, getState(store, path), payload)store._subscribes.forEach(fn => {fn(mutation, store.state)})})
})// 處理getter
module.forEachGetter((getter, key) => {store._wrappedGetters[namespace + key] = function() {return getter(getState(store, path))}
})

之前的mutation.state全部替換成getState去獲取最新的值。n(mutation, rootState) 也替換為fn(mutation, store.state),這樣就可以了。當然源碼中并沒有getState去或獲取最新狀態的方法。

Vuex中的輔助方法

所謂輔助函數,就是輔助我們平時使用,說白了就是讓我們偷懶。

我們在頁面組件中可能會這樣使用

<template><div id="app">我的年齡是:{{this.$store.getters.age}}<button @click="$store.commit('changeAge',5)">同步更新age</button><button @click="$store.commit('b/changeAge',10)">異步更新age</button></div>
</template>
<script>export default {computed: {},mounted() {console.log(this.$store);},
};
</script>

this.$store.getters.age這樣用當然是可以,但是就是有點啰嗦,我們可以做以下精簡

computed:{age() {return this.$store.getters.age}
}

this.$store.getters.age 直接替換成 age,效果肯定是一樣的。但是寫了在computed中寫了age方法,感覺還是啰嗦麻煩,那再來簡化一下吧,先看下用法:

computed:{...mapState(['age'])
}

mapState實現

export function mapState(stateArr) {let obj = {};for (let i = 0; i < stateArr.length; i++) {let stateName = stateArr[i];obj[stateName] = function() {return this.$store.state[stateName]}}return obj
}

那如法炮制,mapGetters

export function mapGetters(gettersArr) {let obj = {};for (let i = 0; i < gettersArr.length; i++) {let gettName = gettersArr[i];obj[gettName] = function() {return this.$store.getters[gettName]}}return obj
}

mapMutations

export function mapMutations(obj) {let res = {};Object.entries(obj).forEach(([key, value]) => {res[key] = function (...args) {this.$store.commit(value, ...args)}})return res;
}

mapActions

export function mapActions(obj) {let res = {};Object.entries(obj).forEach(([key, value]) => {res[key] = function (...args) {this.$store.dispatch(value, ...args)}})return res;
}

其中這些方法都是在一個helpers文件中。在vuex/index文件中將其導入。

import { Store, install } from './store';// 這個文件是入口文件,核心就是導出所有寫好的方法
export default {Store,install
}export * from './helpers';

createNamespacedHelpers

可以通過使用 createNamespacedHelpers 創建基于某個命名空間輔助函數。它返回一個對象,對象里有新的綁定在給定命名空間值上的組件綁定輔助函數。

export const createNamespacedHelpers = (namespace) => ({mapState: mapState.bind(null, namespace),mapGetters: mapGetters.bind(null, namespace),mapMutations: mapMutations.bind(null, namespace),mapActions: mapActions.bind(null, namespace)
})

總結

vuex的核心功能基本是完成,也能實現基本功能,不過看源碼對很多細節做了處理,邊界做了判斷。而且其中用到 了很多設計模式以及很多技巧和算法。

通過自己實現一遍vuex,可以加深對vuex的理解和使用。



喜歡的朋友記得點贊、收藏、關注哦!!!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/91371.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/91371.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/91371.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

APIs案例及知識點串講(上)

一.輪播圖專題CSS代碼<style>* {box-sizing: border-box;}.slider {width: 560px;height: 400px;overflow: hidden;}.slider-wrapper {width: 100%;height: 320px;}.slider-wrapper img {width: 100%;height: 100%;display: block;}.slider-footer {height: 80px;backgro…

華大單片機HC32L110燒錄程序方法

1&#xff0c;安裝J-flash工具 從SEGGER官網下載J-flash工具&#xff0c;地址&#xff1a;SEGGER - The Embedded Experts - Downloads - J-Link / J-Trace。按向導安裝完成。 2&#xff0c;使用如下圖JLINK工具SWD模式連接單片機的燒錄接口&#xff08;SWDIO,SWCLK,GND&#…

LeetCode|Day15|125. 驗證回文串|Python刷題筆記

LeetCode&#xff5c;Day15&#xff5c;125. 驗證回文串&#xff5c;Python刷題筆記 &#x1f5d3;? 本文屬于【LeetCode 簡單題百日計劃】系列 &#x1f449; 點擊查看系列總目錄 >> &#x1f4cc; 題目簡介 題號&#xff1a;125. 驗證回文串 難度&#xff1a;簡單 題…

項目學習筆記 display從none切換成block

跟著視頻學做項目的時候&#xff0c;碰到一個多級聯動列表&#xff0c;列表元素的display會從none切換成block&#xff0c;切換過程中可能導致資源渲染過多&#xff0c;從而導致卡頓問題。<div class"all-sort-list2"><div class"item" v-for&quo…

從 “洗澡難” 到 “洗得爽”:便攜智能洗浴機如何重塑生活?

洗澡本應是日常生活的簡單需求&#xff0c;但對于失能老人、行動不便者而言&#xff0c;卻可能成為一項充滿挑戰甚至危險的“艱巨任務”。中國失能、半失能老年人口超過4200萬&#xff0c;傳統助浴方式存在搬運風險高、隱私難以保障、效率低下等問題&#xff0c;使得“洗澡難”…

鷓鴣云重構光伏發電量預測的精度標準

在當今全球能源轉型的大背景下&#xff0c;光伏發電作為一種清潔、可再生的能源形式&#xff0c;正受到越來越多的關注與應用。然而&#xff0c;光伏發電量的精準預測&#xff0c;一直是行業內亟待攻克的關鍵難題。尤其是在面對復雜多變的氣象條件、不同區域的地理環境以及設備…

每日一題(沉淀中)

文章目錄 1、 實現string類的接口&#xff0c;并完成測試&#xff0c;要求利用深拷貝和深賦值實現 MyString.h #pragma once #include<iostream> class MyString { private:char* data;//儲存字符串內容 public://默認構造函數MyString(const char* str nullptr);////拷…

深入淺出Kafka Producer源碼解析:架構設計與編碼藝術

一、Kafka Producer全景架構 1.1 核心組件交互圖 #mermaid-svg-L9jc09hRQCHb0ftl {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-L9jc09hRQCHb0ftl .error-icon{fill:#552222;}#mermaid-svg-L9jc09hRQCHb0ftl .erro…

微軟AutoGen:多智能體協作的工業級解決方案

微軟AutoGen&#xff1a;多智能體協作的工業級解決方案 &#x1f31f; 嗨&#xff0c;我是IRpickstars&#xff01; &#x1f30c; 總有一行代碼&#xff0c;能點亮萬千星辰。 &#x1f50d; 在技術的宇宙中&#xff0c;我愿做永不停歇的探索者。 ? 用代碼丈量世界&#xf…

終端安全管理系統為什么需要使用,企業需要的桌面管理軟件

在當今數字化高度發展的時代&#xff0c;企業和組織的運營計算機等&#xff0c;是企業業務流程的重要節點。終端安全管理系統正揮著至關重要的作用。保障數據安全終端設備往往存儲著企業的核心數據&#xff0c;終端安全管理系統可以保障安&#xff0c;未經授權的人員也無法獲取…

補環境基礎(一) 原型與原型鏈

1.創建對象的幾種方式 1.對象字面量模式 直接使用{}定義鍵值對&#xff1a; const obj { key: value }; 2.Object()構造函數模式 使用內置構造函數&#xff08;較少使用&#xff09;&#xff1a; const person new Object(); console.log(person)//輸出 {}3.構造函數模…

Qt+yolov8目標識別

這是一個基于ONNX Runtime的YOLOv8目標檢測項目&#xff0c;支持CPU和GPU加速&#xff0c;使用Qt框架構建圖形化界面。攝像頭實時畫面識別視頻文件識別&#xff0c;能正常識別目標&#xff1a;紅綠燈&#xff0c;人&#xff0c;公交&#xff0c;巴士&#xff0c;摩托車 等YOLOv…

NLP分詞notes

BPE 貪心提取所有出現頻率高的成為詞。 BPE的訓練流程 1.初始化&#xff1a;將所有單個字符作為初始詞匯表的元素。 2.迭代合并&#xff1a; 統計語料中所有相鄰符號對&#xff08;包括字符和合并后的符號&#xff09;的出現頻率。找到出現頻率最高的符號對&#xff0c;將其合并…

【數據結構】棧和隊列-----數據結構中的雙生花

文章目錄[toc]棧與隊列&#xff1a;數據結構中的雙生花1. 棧&#xff1a;后進先出的有序世界1.1 概念及結構剖析1.2 實現方式深度解析數組 vs 鏈表實現1.3 動態棧實現詳解&#xff08;附程序源碼&#xff09;1.定義一個動態棧2.初始化3.銷毀4.入棧5.出棧6.取棧頂數據7.判空8.獲…

Mybatis-2快速入門

學習主線 必學必會屬于優化的東西。 快速入門需求說明 要求&#xff1a;開發一個MyBatis項目&#xff0c;通過MyBatis的方式可以完成對monster表的crud操作 1.創建mybatis數據庫-monster表 主鍵Primary Key默認非空Not null&#xff0c;就省略了 create database mybatis us…

Web基礎 -java操作數據庫

一、JDBCJDBC&#xff1a;&#xff08;Java DataBase Connectivity&#xff09;&#xff0c;就是使用Java語言操作關系型數據庫的一套API。為了使用JDBC操作數據庫&#xff0c;首先&#xff0c;我們需要在pom.xml文件中引入依賴<dependencies><!-- MySQL JDBC driver …

cell2location復現

https://github.com/BayraktarLab/cell2location/issues/348 根據你已下載的本地 wheel 文件&#xff0c;可以通過以下方式修改安裝命令&#xff0c;優先從本地路徑安裝 jaxlib&#xff0c;同時保持其他依賴的安裝方式不變&#xff1a; 解決方案 # 安裝 jax (從遠程 PyPI 源) p…

什么是 npm、Yarn、pnpm? 有什么區別? 分別適應什么場景?

什么是 npm、Yarn、pnpm? 有什么區別? 分別適應什么場景? 在前端開發中&#xff0c;包管理工具扮演著非常重要的角色。它們幫助開發者高效地管理項目的依賴&#xff0c;確保項目中所需的所有第三方庫和工具都能按時安裝&#xff0c;并且兼容版本。npm、Yarn 和 pnpm 是三款…

深度隱匿源IP:高防+群聯AI云防護防繞過實戰

隱蔽性挑戰 黑客常通過以下手段繞過基礎防護&#xff1a; HTTPS證書嗅探&#xff1a;訪問 https://源站IP&#xff0c;通過證書域名匹配暴露真實IP歷史解析記錄追蹤&#xff1a;從DNS數據庫獲取舊A記錄CDN緩存滲透&#xff1a;利用邊緣節點回源漏洞定位源站 三重防護方案 高防I…

如何加快golang編譯速度

跟著我的步驟來&#xff1a;第一步&#xff1a;(點擊edit)第二步&#xff1a;將go tool arguments設置為-p4&#xff0c;初始值設為4&#xff0c; 代表最多同時編譯4個包&#xff08;非文件&#xff09;。電腦性能好時&#xff0c;可設為CPU最大核心數&#xff08;充分利用多核…