手写 Vuex4 源码

这篇具有很好参考价值的文章主要介绍了手写 Vuex4 源码。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

  1. 本文首发于掘金,未经许可禁止转载

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 文件,所以可以用上面命令。

手写 Vuex4 源码

选择 Vuex,使用空格选择或取消选择

手写 Vuex4 源码

手写 Vuex4 源码

启动项目如果如下图报错

手写 Vuex4 源码

可以试试输入命令

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

手写 Vuex4 源码

基本使用

使用 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>

手写 Vuex4 源码

编写源码

实现获取state并响应式修改state

修改 App.vue 的引用,@/vuex 是需要编写的源码的文件夹

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

修改 store 的引用

import { createStore } from '@/vuex'

在 src 目录下创建 vuex文件夹,里面添加 index.js

手写 Vuex4 源码

在 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);

手写 Vuex4 源码

在 Store 类中绑定传进来的 state

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

打印就是

手写 Vuex4 源码

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 后就不会报错了

但此时点击 错误增加 的按钮没有任何效果,

手写 Vuex4 源码

因为此时 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
    }
}

这时候 错误增加 的按钮就有效果了

手写 Vuex4 源码

实现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 源码

实现 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

手写 Vuex4 源码

在 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)

看看效果:

手写 Vuex4 源码

这里报了错,由 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))
            }
        }
    }
}

这样就可以了

手写 Vuex4 源码

注册模块

平常使用中定义 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>

手写 Vuex4 源码

先实现将用户定义的多个 modules 进行格式化,创造父子关系

用户传进来的数据

手写 Vuex4 源码

格式化后的数据

手写 Vuex4 源码

上面的每个 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

手写 Vuex4 源码

在组件中引用也能正确显示了

手写 Vuex4 源码

手写 Vuex4 源码

注册模块上的 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>

手写 Vuex4 源码

插件模式

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
    })
}

效果如下:
手写 Vuex4 源码

总结

Vuex 在项目中用了很久,只知其然不知其所以然,故研究学习并实现出来。

从理解思路到手写出来,然后将实现过程记录下来就有了这篇文章,这个过程断断续续持续了大概一个月,项目和文章基本都是利用下班时间写的,确实挺累的,不过实现出来后往回看,还是学到很多东西,还挺欣慰的;文章有不足的地方还请各位大佬指正;文章来源地址https://www.toymoban.com/news/detail-633566.html

到了这里,关于手写 Vuex4 源码的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 手写Vue3源码

    B站视频地址:https://www.bilibili.com/video/BV1nW4y147Pd?p=2vd_source=36bacfbaa95ea7a433650dab3f7fa0ae Monorepo 是管理项目代码的一种方式,只在一个仓库中管理多个模块/包 一个仓库可以维护多个模块,不用到处找仓库 方便版本管理和依赖管理,模块之间的引用和调用都非常方便 缺点:仓库的

    2024年01月20日
    浏览(44)
  • JS基础源码之手写模拟new

    new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一。 我们先看看new实现了哪些功能: 我们可以看到,实例person可以: 访问到Otaku构造函数里的属性; 访问到Otaku.prototype中的属性; 接下来,我们可以尝试着模拟一下: 因为new是,所以无

    2024年02月04日
    浏览(35)
  • Spring手写模拟源码篇(你值得拥有)

    下面是本文章关于Spring底层原理的章节 类-》推断构造方法-》根据构造方法创建普通对象-》依赖注入(@Autowired等进行属性注入)-》初始化前(@PostConstruct)-初始化(InitializingBean)-》初始化后(AOP)-》代理对象(没有开启AOP就会把普通对象放入单例池)-》放入单例池-》Bean对象

    2024年02月01日
    浏览(36)
  • Spring源码系列:初探底层,手写Spring

    在学习Spring框架源码时,记住一句话:源码并不难,只需要给你各种业务场景或者项目经理,你也能实现自己的Spring。虽然你的实现可能无法与开源团队相媲美,但是你肯定可以实现一个0.0.1版本。因此,初次阅读源码时,不要陷入太深的细节中。先了解大体逻辑,再仔细研

    2023年04月12日
    浏览(43)
  • 【神经网络手写数字识别-最全源码(pytorch)】

    Torch安装的方法 学习方法 1.边用边学,torch只是一个工具,真正用,查的过程才是学习的过程 2.直接就上案例就行,先来跑,遇到什么来解决什么 Mnist分类任务: 网络基本构建与训练方法,常用函数解析 torch.nn.functional模块 nn.Module模块 读取Mnist数据集 会自动进行下载 784是m

    2024年02月14日
    浏览(37)
  • 手写axios源码系列二:创建axios函数对象

    当前篇章正式进入手写 axios 源码系列,我们要真枪实弹的开始写代码了。 因为 axios 源码的代码量比较庞大,所以我们这里只抓主线,让大家对 axios 的主要核心功能有个大概了解,至于具体的细节深入,需要大家去通读 axios 源码了解。手写 axios 源码系列只将其中的4个主要功

    2023年04月23日
    浏览(41)
  • 【框架源码】手写Spring框架IOC容器核心流程

    要是想要了解Spring IOC底层,首先我们就得先了解什么是IOC。 IOC就是控制反转,把对象创建和对象之间的调用过程交给Spring进行管理。 使用IOC目的就是之前我们创建对象的方式都是用new的方式创建,这个方式有个缺点,被创建对象的类的位置一旦被改动,new就会报错,类与类

    2024年02月06日
    浏览(54)
  • 手写axios源码系列一:axios核心知识点

    最近从头搭建了一个vue小项目,想使用 axios 作为请求接口的第三方库。结果使用了 axios 这么长时间,想封装一下 axios ,也就是大部分项目中的 src/utils/request.js 文件,我居然无法默写出来,create 和 interceptors 这样的 api 都不知道怎么使用;所以决定深入一下 axios 源码,一探究

    2023年04月22日
    浏览(50)
  • 手写Java设计模式之抽象工厂模式,附源码解读

    接上篇,抽象工厂模式将汽车的一些属性可以抽象出来,可以理解为给不同汽车品牌生成时加上不同的特性,如颜色等,具体代码如下: 引入颜色接口: 将颜色与汽车生成品牌抽象出来,增加抽象类: 继承抽象类,分别对不同属性的特征进行操作,如涂上颜色等,首先实现

    2024年04月25日
    浏览(52)
  • 手写java设计模式之单例模式,附源码解读

    在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处: 1、减少类的频繁创建,减少使用频繁使用new创建实例,减少GC压力。 2、某些应用场景下,使用单例模式,保证整个系统中只会创建一个类。 单例模式分两种:饿汉模式和懒汉模

    2024年04月29日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包