手寫 Vuex4 源碼

来源:https://www.cnblogs.com/zsxblog/archive/2023/08/08/17615611.html
-Advertisement-
Play Games

> 本文首發於[掘金](https://juejin.cn/post/7264128388288708664),未經許可禁止轉載 Vuex4 是 Vue 的狀態管理工具,Vuex 和單純的全局對象有以下兩點不同: 1. Vuex 的狀態存儲是響應式的 2. 不能直接改變 store 中的狀態。改變 ...


本文首發於掘金,未經許可禁止轉載

Vuex4 是 Vue 的狀態管理工具,Vuex 和單純的全局對象有以下兩點不同:

  1. Vuex 的狀態存儲是響應式的
  2. 不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地 提交 (commit) mutation

本文手寫部分分為八個部分,基本包含了 Vuex 的功能。

  • 實現獲取state並響應式修改state
  • 實現getters
  • 實現 commit 和 dispatch
  • 註冊模塊
  • 註冊模塊上的 getters,mutations,actions 到 store 上
  • 命名空間
  • 嚴格模式
  • 插件模式

準備工作

創建名字叫 vuex_source 的工程

vue-cli3 create vuex_source

上面命令和使用 vue create vuex_source 創建項目是等價的,我電腦安裝了 vue-cli2vue-cli3,在 vue-cli3裡面修改了 cmd 文件,所以可以用上面命令。

image.png

選擇 Vuex,使用空格選擇或取消選擇

image.png

image.png

啟動項目如果如下圖報錯

image.png

可以試試輸入命令

$env:NODE_OPTIONS="--openssl-legacy-provider"

image.png

基本使用

使用 createStore 創建一個 store

import { createStore } from 'vuex'

export default createStore({
  strict:true,
  state: {
    count:1
  },
  getters: {
    double(state){
      return state.count * 2
    }
  },
  mutations: {
    mutationsAdd(state,preload){
      state.count += preload
    }
  },
  actions:{
    actionAdd({commit},preload){
      setTimeout(() => {
        commit('mutationsAdd',preload)
      }, 1000);
    }
  }
})

main.js 中引入

import { createApp } from 'vue'
import App from './App.vue'
import store from './store'

createApp(App).use(store).mount('#app')

在 app.vue 中使用 store

<template>
  數量:{{count}} {{$store.state.count}}
  <br>
  double:{{double}} {{$store.getters.double}}
  <br>
  <!-- 嚴格模式下會報錯 -->
  <button @click="$store.state.count++">錯誤增加</button>
  <br>
  <button @click="mutationsAdd">正確增加mutation</button>
  <br>
  <button @click="actionAdd">正確增加 action</button>
</template>
<script>
import { computed } from "vue";
import { useStore } from "vuex";
export default {
  name: 'App',
  setup(){
    const store = useStore()
    const mutationsAdd = () =>{
      store.commit('mutationsAdd',1)
    }
    const actionAdd = () =>{
      store.dispatch('actionAdd',1)
    }
    return {
      // 來自官網解釋:從 Vue 3.0 開始,getter 的結果不再像計算屬性一樣會被緩存起來。這是一個已知的問題,將會在 3.1 版本中修複。
      // 使用 count:store.state.count 返回的話,模板中的 {{count}}並不是響應式的,這裡必須加上 computed 此時響應式的
      count:computed(() => store.state.count),
      double:computed(() => store.getters.double),
      mutationsAdd,
      actionAdd,
    }
  }
}
</script>

vuex.gif

編寫源碼

實現獲取state並響應式修改state

修改 App.vue 的引用,@/vuex 是需要編寫的源碼的文件夾

import { useStore } from "@/vuex"; // 之前是import { useStore } from "vuex";

修改 store 的引用

import { createStore } from '@/vuex'

在 src 目錄下創建 vuex文件夾,裡面添加 index.js

image.png

在 index.js 中添加 createStore 和 useStore 函數,createStore 用來創建 store,useStore 供頁面調用

// vuex/index.js

class Store{
    constructor(options){
    }
}
// 創建 store,多例模式
export function createStore(options){
    return new Store(options)
}

// 使用 store
export function useStore(){}

createStore 創建出的store,在main.js 中 調用 use 方法

createApp(App).use(store)

use 會調用 store 的 install 方法,將 store 安裝到 Vue 上,所以 Store 類中還需要添加 install 方法

const storeKey = 'store' // 預設一個 store 名

class Store{
    constructor(options){
    }
    install(app,name){ 
        // app 是vue3暴露的對象
        // 在根組件上綁定了 store,子組件要用到 store
        // 根組件就需要 provide 出去,子組件 inject 接收
        app.provide(name || storeKey,this)
    }
}

// 創建 store
export function createStore(options){
    return new Store(options)
}
// 使用 store
export function useStore(name){
    // inject 去找父組件的 provide 的東西
    return inject(name!==undefined?name:storeKey)
}

此時在 App.vue 中列印的就是一個空對象

// App.vue
const store = useStore()
console.log(store);

image.png

在 Store 類中綁定傳進來的 state

constructor(options){
    this.state = options.state
}

列印就是

image.png

App.vue 中添加如下模板

<template>
  數量:{{count}}  // 正常列印 1
  數量:{{$store.state.count}} // 報錯了
  <br>
  <button @click="$store.state.count++">錯誤增加</button>
</template>

<script>
import { computed } from "vue";
import { useStore } from "@/vuex";
export default {
  name: 'App',
  setup(){
    const store = useStore()
    console.log(store);
    return {
      count:computed(() => store.state.count)
    }
  }
}
</script>

上面模板中的 {{$store.state.count}} 會報錯,是因為 $store 沒有綁定到 this 上。vue3 中綁定到 this 可以用 app.config.globalProperties[屬性名]

    // createApp(App).use(store,name)會調用store的install方法
    install(app,name){
        // app 是vue3暴露的對象
        app.provide(name || storeKey,this)
        app.config.globalProperties.$store = this
    }
}

$store 綁定到 this 後就不會報錯了

但此時點擊 錯誤增加 的按鈕沒有任何效果,

vuex2.gif

因為此時 store.state 並不是響應式的,需要增加響應式效果,vue3 為複雜數據提供了 reactive

class Store{
    constructor(options){
        // 這裡給options.state加了一層,用 data 包裹是為了重新賦值的時候可以直接 this._store.data = 。。。 ,而不用再使用 reactive
        this._store = reactive({data:options.state})
        this.state = this._store.data
    }
    // createApp(App).use(store,name)會調用store的install方法
    install(app,name){
        // app 是vue3暴露的對象
        app.provide(name || storeKey,this)
        app.config.globalProperties.$store = this
    }
}

這時候 錯誤增加 的按鈕就有效果了

vuex3.gif

實現getters

模板中是使用 getters 是以屬性的方式:

// App.vue
數量:{{count}} 
數量:{{$store.state.count}}
<br>
double:{{double}}
double:{{$store.getters.double}}
<br>
<button @click="$store.state.count++">錯誤增加</button>

在 store.js 中定義的getters 是由一個大對象裡面包含多個函數組成

getters: {
    double(state){
      return state.count * 2
    }
},

在 store 中 double 是函數,返回的 state.count * 2的結果。 在模板中使用的是 $store.getters.double ,這個 double 是 getters 上的一個屬性。所以這裡需要進行轉換

const forEachValue = function(obj,fn){
    return Object.keys(obj).forEach((key) =>{
        fn(obj[key],key)
    })
}

class Store{
    constructor(options){
        this._store = reactive({data:options.state})
        this.state = this._store.data
        this.getters = Object.create(null)
        forEachValue(options.getters,(fn,key) => {
            // 當模板解析 $store.getters.double 時,
            // 就去執行 options.getters裡面對應屬性的函數,並將函數結果賦予該屬性
            Object.defineProperty(this.getters,key,{
                // vue3.2之前的vuex中不能使用計算屬性 computed,導致每次訪問的時候都會執行函數引發潛在性能問題
                // vue3.2修複了這個bug
                get:() => {
                    return fn(this.state)
                }
            })
        })
    }
    // createApp(App).use(store,name)會調用store的install方法
    install(app,name){
        // app 是vue3暴露的對象
        app.provide(name || storeKey,this)
        app.config.globalProperties.$store = this
    }
}

forEachValue 函數接收一個對象參數 obj 和一個處理函數參數 fn;裡面會遍歷對象,迴圈調用 fn;

這裡遍歷 options.getters ,響應式註冊到 this.getters 上,這樣當模板解析 $store.getters.double 時,就會執行對應的 fn

點擊錯誤增加按鈕,改變 $store.state.count 的值進而導致 getters 值的變化

vuex4.gif

實現 commit 和 dispatch

commit 和 dispatch 在組件中是這樣使用的:

<template>
  <button @click="mutationsAdd">正確增加 mutation</button>
  <br>
  <button @click="actionAdd">正確增加 action</button>
</template>

<script>
import { useStore } from "@/vuex";
export default {
  name: 'App',
  setup(){
    const store = useStore()
    const mutationsAdd = () =>{
      store.commit('mutationsAdd',1)
    }
    const actionAdd = () =>{
      store.dispatch('actionAdd',1)
    }
    return {
      mutationsAdd,
      actionAdd,
    }
  }
}
</script>

store.js 中定義的是這樣的:

mutations: {
    mutationsAdd(state,preload){
      state.count += preload
    }
},
actions:{
    // 非同步調用
    actionAdd({commit},preload){
      setTimeout(() => {
        commit('mutationsAdd',preload)
      }, 1000);
    }
}

調用 mutation : store.commit(mutation類型,參數)

調用 action : store.dispatch(action類型,參數)

在 Store 類中實現 commit:

class Store{
    constructor(options){
        // 將 store.js 中定義的 mutations 傳進來
        this._mutations = options.mutations
        this.commit = function(name,preload){
            if(this._mutations[name]!==undefined){
                // 根據傳進來的類型,調用對應的方法
                this._mutations[name](this.state,preload)
            }
        }
    }
}

效果如下,數量每次增加 1

vuex5.gif

在 Store 類中實現 dispatch:

class Store{
    constructor(options){
        // 將 store.js 中定義的 actions 傳進來
        this._actions = options.actions
        this.dispatch = function(name,preload){
            if(this._actions[name]!==undefined){
                // 根據傳進來的類型,調用對應的方法
                let fn = this
                // dispatch 進來調用的是 actionAdd({commit},preload)
                this._actions[name].apply(fn,[fn].concat(preload))
            }
        }
    }
}

dispatch 調用的參數是({commit},preload),所以這裡傳進去需要是 (this,preload)

看看效果:

vuex6.gif

這裡報了錯,由 dispatch 觸發 actions 正常,但 actions 觸發 對應的 mutations 出錯了,顯示 this 是 undefined。那麼這裡就要修改下之前的 commit 實現了,先用一個變數將 Store 類實例的 this 保存起來

class Store{
    constructor(options){
        // 這裡創建一個 store 變數保存 this 是方便之後嵌套函數裡面訪問當前 this
        let store = this

        this._mutations = options.mutations
        this.commit = function(name,preload){
            if(store._mutations[name]!==undefined){
                store._mutations[name](store.state,preload)
            }
        }

        this._actions = options.actions
        this.dispatch = function(name,preload){
            if(store._actions[name]!==undefined){
                store._actions[name].apply(store,[store].concat(preload))
            }
        }
    }
}

這樣就可以了

vuex7.gif

註冊模塊

平常使用中定義 modules 如下

// store/index.js
import { createStore } from 'vuex'

export default createStore({
  // strict: true,
  state: {
    count: 1
  },
  // ...
  modules: {
    aCount: {
      state: {
        count: 1
      },
      modules: {
        cCount: {
          state: {
            count: 1
          },
        },
      }
    },
    bCount: {
      state: {
        count: 1
      },
    }
  }
})

組件中使用

// App.vue
<template>
  數量(根模塊):{{$store.state.count}}  <button @click="$store.state.count++">增加</button>
  <br>
  數量(aCount模塊):{{$store.state.aCount.count}} <button @click="$store.state.aCount.count++">增加</button>
  <br>
  數量(cCount模塊):{{$store.state.aCount.cCount.count}} <button @click="$store.state.aCount.cCount.count++">增加</button>
  <br>
</template>

vuex8.gif

先實現將用戶定義的多個 modules 進行格式化,創造父子關係

用戶傳進來的數據

image.png

格式化後的數據

image.png

上面的每個 modules 下的數據格式如下

this._raw = modules // 保存不做處理的源數據
this.state = modules.state // 保存狀態
this.children = {} // 創建子對象
  • _raw 保存不做處理的源數據
  • state 保存狀態
  • children 保存子模塊

比如 bCount

// 格式化前
bCount:{
   state: {
      count: 1
   },
}

// 格式化後
bCount: {
    _raw: bModule, // 這裡放的是格式化前的數據
    state: bModule.state,
    children: {}
},

所以這裡定義一個類 moduleCollection,專門用來收集模塊,將用戶寫的嵌套 modules 格式化,創造父子關係

class Store {
    constructor(options) {
        const store = this
        
        // 收集模塊,將用戶寫的嵌套modules格式化,創造父子關係
        store._modules = new moduleCollection(options)
        console.log(store._modules)
    }
}

在 moduleCollection類中定義 register 方法處理數據,定義 this.root 保存處理過後的數據

class moduleCollection{
    constructor(rootModule){
        // root 存儲格式化的數據,方便後續安裝到 store.state 上
        this.root = null
        this.register(rootModule,[])
    }
    register(rootModule,path){
    }
}

register 方法接受兩個參數

一個表示當前處理的模塊數據 rootModule ,一個表示當前處理的是誰的模塊數據 path

path之所以用數組表示,是因為後面建造父子關係時,使用path可以進行關聯

比如 path 是空數組,則表示處理的是根模塊的數據,是 [a] 則表示處理的是 a 模塊,是[a,c] 則表示處理的是 c 模塊的數據,並且,c模塊的數據要加到 a 模塊的 children 中。

對於 register 方法:

首先將用戶定義的 store 格式化賦值給 this.root,這裡可以抽象出一個類,因為每個模塊的格式都是 _raw,state,children

class Module{
    constructor(modules){
        this._raw = modules
        this.state = modules.state
        this.children = {}
    }
    getChild(key){
        return this.children[key]
    }
    addChild(key,module){
        this.children[key] = module
    }
}

class moduleCollection{
    register(rootModule,path){
        const newModule = new Module(rootModule)
        if(path.length===0){
            this.root = newModule
        }
    }
}

然後判斷 最外層的 store 中也就是根模塊還有沒有子模塊,如果有,繼續遞歸格式化子模塊數據

// 如果根模塊下還有子模塊,則繼續遞歸註冊
if(rootModule.modules){
    Object.keys(rootModule.modules).forEach((key) =>{
        this.register(rootModule.modules[key],path.concat(key))
    })
}

用戶定義的 store 中,根模塊下定義了子模塊,子模塊裡面分別是 aCount 和 bCount,所以執行到上面代碼時, key 就是 aCount,bCount

rootModule.modules[key] 是他們對應的模塊數據

當執行到 aCount 模塊時,此時的 path 是[a],代表處理 aCount 的數據,這時我們要在根模塊上添加 aCount,如果 path 是 [aCount,cCount],則需要在 aCount 模塊上添加 cCount 模塊,所以這裡需要定義一個尋找父模塊的方法。

使用 path.slice(0,-1) 得到父模塊的 key,預設是根模塊

const parent = path.slice(0,-1).reduce((modules,current) =>{
    return modules.getChild(current)
},this.root)

參數 modules 代表上一次執行結果,current代表當前元素,初始傳入根模塊

這裡如果 path 是 [a], 傳入給 reduce 時是 [] ,那麼返回的就是 this.root

path 是[a,c],傳入給reduce 時是 [a], 當執行module.getChild(current) 實際上就是 this.root.getChild(a)

找到父模塊後,給父模塊的 children 添加 modules

parent.addChild(path[path.length-1],newModule)

moduleCollection 類完整代碼:

class moduleCollection{
    constructor(rootModule){
        // root 存儲格式化的數據,方便後續安裝到 store.state 上
        this.root = null
        this.register(rootModule,[])
    }
    register(rootModule,path){
        // 註冊模塊,每個模塊的格式都是
        // _raw: rootModule,
        // state: rootModule.state,
        // children: {}
        // 所以給傳進來的模塊都格式化一下
        const newModule = new Module(rootModule)
        // 註冊根模塊
        if(path.length===0){
            this.root = newModule
        }else{
            // 註冊子模塊,將子模塊添加到對應的父模塊,通過 path路徑可以知道對應的父模塊
            const parent = path.slice(0,-1).reduce((modules,current) =>{
                return modules.getChild(current)
            },this.root)
            
            parent.addChild(path[path.length-1],newModule)
        }
        // 如果根模塊下還有子模塊,則繼續遞歸註冊
        if(rootModule.modules){
            Object.keys(rootModule.modules).forEach((key) =>{
                this.register(rootModule.modules[key],path.concat(key))
            })
        }
    }
}

得到格式化數據後,需要將各個模塊的 state 安裝在 store.state 上,以便之後調用:$store.state.aCount.cCount.count$store.state安裝後的樣子應該是:

state:{
    count:1,
    aCount:{
        count:1,
        cCount:{
            count:1
        }
    },
    bCount:{
        count:1
    }
}

創建一個 installModules 函數

function installModules(store,modules,path){}

class Store {
    constructor(options) {
        const store = this
        
        store._modules = new moduleCollection(options)
        console.log(store._modules)
        
        installModules(store,store._modules.root,[])
        console.log(store.state)
    }
}

store 是當前 Store 類的實例對象,模塊安裝的地方

modules 是要安裝的模塊

path 對應父子關係

installModules 方法和 register 方法類似

function installModules(store,modules,path){
    if(path.length===0){
        store.state = modules.state
    }else{
        const parent = path.slice(0,-1).reduce((result,current) =>{
            return result[current]
        },store.state)
        
        parent[path[path.length-1]] = modules.state
    }
    if(modules.children){
        Object.keys(modules.children).forEach((key) =>{
            installModules(store,modules.children[key],path.concat(key))
        })
    }
}

這樣也就得到了一個完整的 state

image.png

在組件中引用也能正確顯示了

image.png

image.png

註冊模塊上的 getters,mutations,actions 到 store 上

class Store {
    constructor(options) {
        const store = this
        
        // 收集模塊,將用戶寫的嵌套modules格式化,創造父子關係
        store._modules = new moduleCollection(options)
        
        // 定義私有變數存放對應的 getters,actions,mutations
        store._getters = Object.create(null)
        store._mutations = Object.create(null)
        store._actions = Object.create(null)

        installModules(store,store._modules.root,[])
        
    }
}

同樣也是在 installModules 方法裡面進行存放操作

在格式化模塊時,已經將每個模塊定義成這樣的數據格式:

this._raw = modules
this.state = modules.state
this.children = {}

_raw 存放的就是源數據,沒有被格式化的數據。

所以,取得模塊上的 getters 就是 modules._raw.getters;

取得模塊上的 mutations 就是 modules._raw.mutations;

取得模塊上的 actions 就是 modules._raw.actions;

遍歷 modules._raw.getters ,安裝到 store._getters 上。這裡需要註意的是

getters的參數是 state,這個 state 本來是 modules._raw.state,但 _raw.state沒有響應式,而後面store.state 是響應式的,需要根據 path 取得store.state裡面對應的 state

function getCurrentState(state,path){
    return path.reduce((result,current) =>{
        return result[current]
    },state)
}

function installModules(store,modules,path){
    ···
    ···
    if(modules._raw.getters){
        forEachValue(modules._raw.getters,(getters,key) =>{
            store._getters[key] = () =>{
                // 這裡的參數不能是 modules._raw.state,沒有響應式
                // 而後面 store.state 會是響應式的,需要根據 path 取得store.state裡面對應的 state
                return getters(getCurrentState(store.state,path))
            }
        })
    }
    ···
    ···
}

註冊 mutations

if(modules._raw.mutations){
    forEachValue(modules._raw.mutations,(mutations,key) =>{
        if(!store._mutations[key]){
            store._mutations[key] = []
        }
        store._mutations[key].push((preload) =>{ // store.commit(key,preload)
            mutations.call(store,getCurrentState(store.state,path),preload)
        })
    })
}

在模塊裡面,可能有多個同名的 mutations,所以這裡可能有多個同名 key,需要用數組包裝起來

註冊 actions

if(modules._raw.actions){
    forEachValue(modules._raw.actions,(actions,key) =>{
        if(!store._actions[key]){
            store._actions[key] = []
        }
        store._actions[key].push((preload) =>{
            // store.dispatch({commit},preload)
            // actions執行後返回的是promise
            let res = actions.call(store,store,preload)
            if(!isPromise(res)){
                return Promise.resolve(res)
            }
            return res
        })
    })
}

和 mutations 一樣,也可能會有多個重名的 actions。區別是 actions 執行完後返回的是一個 promise

命名空間

命名空間的用法,添加 namespaced:true

// store.js
aCount: {
  namespaced:true,
  state: {
    count: 1
  },
  mutations: {
    mutationsAdd(state, preload) {
      state.count += preload
    }
  },
  modules: {
    cCount: {
      namespaced:true,
      state: {
        count: 1
      },
      mutations: {
        mutationsAdd(state, preload) {
          state.count += preload
        }
      },
    },
  }
}

頁面中就可以使用 $store.commit('aCount/mutationsAdd',1) 調用 aCount 下的 mutationsAdd

$store.commit('aCount/cCount/mutationsAdd',1) 調用 cCount 下的 mutationsAdd

在安裝模塊的時候,通過檢測模塊是否定義 namespaced 為 true,來給安裝的模塊的 actions,mutations 添加命名空間首碼

function getNameSpace(modules,path){
    let root = modules.root
    // 傳入的是 根模塊
    // 當 path 是[],返回空字元串
    // 2、當 path 是 [aCount] ,根據path,取得根模塊下的對應的 aCount , modules.getChild(aCount)
    // 然後判斷 aCount 模塊下是否定義namespaced,有則返回 aCount/
    // 當 path 是 [aCount,cCount] ,重覆2,然後根據步驟2 取得的子模塊,再往下找子模塊cCount,
    // 然後判斷 cCount 模塊下是否定義namespaced,有則返回 aCount/cCount/
    // [] => ''  [aCount] => 'aCount/'  [aCount,cCount] => 'aCount/cCount'
    return path.reduce((module,current) =>{
        root = root.children[current]
        return root.namespaced?(module+current+'/'):''  
    },'')
}

function installModules(store,modules,path,root){
    ···
    // 所以這裡先根據path取得設置了 namespaced 的模塊名字,拼接後註冊到 mutations,actions的名字上
    const namespace = getNameSpace(root,path)
    console.log(namespace)

    if(modules._raw.mutations){
        forEachValue(modules._raw.mutations,(mutations,key) =>{
            if(!store._mutations[namespace + key]){
                store._mutations[namespace + key] = []
            }
            store._mutations[namespace + key].push((preload) =>{ // store.commit(key,preload)
                mutations.call(store,getCurrentState(store.state,path),preload)
            })
        })
    }

}

在 getNameSpace 方法中,傳入的參數 path 表示的是有父子關係的模塊名組成的數組,通過 path 找到對應的模塊,判斷是否定義 namespaced 為 true。最終返回命名空間字元串

嚴格模式

要設置嚴格模式,在根節點上指定 strict:true 即可。

const store = createStore({
  // ...
  strict: true
})

設置了嚴格模式,沒有通過 mutations 改變狀態都會彈出一個報錯信息(即通過 $store.state.count++ 直接改變狀態)

並且官方建議不要在發佈環境下啟用嚴格模式

那麼這裡可以創建一個變數 isCommiting ,用來判斷是否通過 mutations 改變狀態,只要是執行了 mutations 方法的都去改變 isCommiting。

store.commit = (type,preload) =>{
    this.withCommit(() =>{
        if(store._mutations[type]){
            store._mutations[type].forEach((fn) =>{
                fn(preload)
            })
        }
    })
}
withCommit(fn){
    this.isCommiting = true
    fn()
    this.isCommiting = false
}

上面執行 mutations 之前,isCommiting 為 true,此時只需要知道,當狀態變化的時候 isCommiting 不為 true,則提示報錯。

這裡每個數據狀態變化都需要知道 isCommiting 的值,所以需要深度監聽整個狀態。深度監聽會帶來一定性能損耗,所以嚴格模式不建議在生產環境使用。

if(store.strict){
    watch(() =>store._store.data,() =>{
        console.assert(store.isCommiting,'do not mutate vuex store state outside mutation handlers.')
    },{deep:true,flush:'sync'})
}

效果如下:

  數量(根模塊):{{$store.state.count}}  
  <button @click="$store.commit('mutationsAdd',1)">增加</button>
  <button @click="$store.state.count++">錯誤增加</button>

vuex9.gif

插件模式

Vuex 的插件實際上是一個函數,store 作為這個函數的唯一參數。定義插件即在 createStore 中定義 plugins 選項,選項是數組格式,可包含多個插件函數。這些插件函數會在創建 store 時依次執行

const plugins1 = (store) => {
  // 當 store 初始化後調用
  store.subscribe((mutation, state) => {
    // 每次 mutation 之後調用
    // mutation 的格式為 { type, payload }
  })
}

const store = createStore({
  // ...
  strict: true,
  plugins:[plugins1],
})

在插件中可以調用 subscribe 方法,參數是當前調用的 mutation 和調用 mutation 後的 state ,此時的 state 是最新的。並且 subscribe 中的函數都是在每次 mutation 之後調用。

根據這些,來實現 subscribe 方法

class Store {
    constructor(options) {
        const store = this
        // ...
        store._subscribe = []
        store.subscribe = (fn) =>{
            store._subscribe.push(fn)
        }
        const plugins = options.plugins
        plugins.forEach(fn => {
            fn(store)
        });
    }

定義 _subscribe 私有變數數組,用來存儲插件中 subscribe 的函數。如果定義了 plugins 選項,那麼依次執行選項中的插件函數。

store.commit = (type,preload) =>{
    this.withCommit(() =>{
        if(store._mutations[type]){
            store._mutations[type].forEach((fn) =>{
                fn(preload)
            })
            store._subscribe.forEach(fn => {
                fn({type:type,preload:preload},store.state)
            });
        }
    })
}

在調用完 mutation 後,迴圈調用 _subscribe 的函數。這樣每個函數中的 state 參數都是最新的

現在來實現一個持久化存儲的插件,將狀態存儲在 sessionStorage 中,頁面刷新後,從 sessionStorage 中取出並替換為最新狀態。

const customPlugin = (store) =>{
  const local = sessionStorage.getItem('vuexState')
  if(local){
    store.replaceState(JSON.parse(local))
  }
  store.subscribe((mutation,state) =>{
    sessionStorage.setItem('vuexState',JSON.stringify(state))
  })
}

在 store.subscribe 參數函數中,每次調用 mutation 後,將狀態存儲在 sessionStorage 中。store.replaceState 則是一個替換狀態的方法。

replaceState(newState){
    this.withCommit(() =>{
        this._store.data = newState
    })
}

效果如下:
image

總結

Vuex 在項目中用了很久,只知其然不知其所以然,故研究學習並實現出來。

從理解思路到手寫出來,然後將實現過程記錄下來就有了這篇文章,這個過程斷斷續續持續了大概一個月,項目和文章基本都是利用下班時間寫的,確實挺累的,不過實現出來後往回看,還是學到很多東西,還挺欣慰的;文章有不足的地方還請各位大佬指正;


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • > Sequelize是一個基於Node.js的ORM框架 ### 特點: * 1、支持多種資料庫:Sequelize支持多種關係型資料庫,包括MySQL、PostgreSQL、SQLite和MSSQL等,適用於需要在不同資料庫間切換或者相容多種資料庫的項目。 * 2、強大的查詢功能:Sequeli ...
  • 事務是應用程式將多個讀寫操作組合成一個邏輯單元的一種形式,這樣其中所有的讀寫操作都被視為單個操作來執行,要麼成功提交,要麼失敗回滾,不存在任何部分成功和部分失敗的情況。現在,幾乎所有的關係型資料庫和一些非關係型資料庫都支持事務。 ...
  • 本文分享自華為雲社區《直播回顧 | 實時入庫不用愁,HStore幫分憂》,作者:汀丶。 海量數據時代,如何實現數據實時入庫與實時查詢?GaussDB(DWS) HStore表為數據高效存儲與查詢提供了哪些助力?本期《數倉實時入庫利器—HStore表原理與應用實踐詳解》的主題直播中,我們邀請到華為雲E ...
  • # 索引結構 ![image-20230808101522006](https://picimg-blog.oss-cn-nanjing.aliyuncs.com/blog-img/image-20230808101522006.png) ## InnoDB B 樹 ![image-20230808 ...
  • NineData 是一款功能強大的資料庫對比工具,能夠幫助企業追蹤資料庫的變化、發現問題並快速修複。相比其他工具,NineData 具有以下優勢:即開即用、全面的數據源支持、完善的對比功能、快速高效、可視化界面、一鍵差異修複、免費使用、安全可靠。使用 NineData,您可以快速配置對比任務、查看對... ...
  • MySQL 和 Elasticsearch 是兩種不同的數據管理系統,它們各有優劣,適用於不同的場景。本文將從以下幾個方面對它們進行比較和分析: - 數據模型 - 查詢語言 - 索引和搜索 - 分散式和高可用 - 性能和擴展性 - 使用場景 ## 數據模型 MySQL 是一個關係型資料庫管理系統(R ...
  • 本篇文章主要是對方案性能優化2.0中,所做的緩存設計的過程、方案、結果做一個總結。 一、前言 對於方案中心,核心業務場景之一是物流場景下的物流費用計算。而部分業務場景下,對於物流費用計算的性能有較高要求,如ICBU網站運費模板鏈路,通方案中心計算快遞、海拼物流費用。在接入新的流量場景的背景下(ICB ...
  • 作為開發公司,我們開發完APP,如何發給客戶下載測試呢?安卓APP可以通過QQ直接發送給客戶,客戶可以在QQ內直接點擊安裝。 但是現在很多客戶都不用QQ,用微信的居多。而通過微信直接發送安卓APP安裝包(apk)的話,是不可以像QQ那樣直接點擊安裝的。這就需要我們把APP生成二維碼提供給客戶下載安裝 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...