一篇文章带你自己实现一个vuex

74 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3天,点击查看活动详情

本篇文章会对vuex的state、getters、mutations、actionsuseStore做一个实现。

添加store

首先,vuex对于vue来说是一个状态管理的插件,在vuex4中,插件抛出一个createStore方法,这个方法内部肯定包含一个install函数,createStore实际功能也就是帮我们返回store实例。
创建一个vuex.js文件存放自己实现的vuex

// vuex.js
function createStore(options){
  return new Store(options)
}
const Store = function(options){}
Store.prototype.install = function(app){
  console.log(app); // 测试插件是否注册成功
  app.config.globalProperties.$store = this; // 把store挂载到vue上
}

export {
  createStore
}

创建一个store.js写使用vuex的代码。

// store.js
import { createStore } from './vuex' // 引入自己写的vuex.js文件

export default createStore({})

在main中使用store

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

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

控制台将会打印install方法中的app,也就是当前vue实例。这样就实现了store的容器。

接下来对入参的实现。
下文中代码块中的 ... 代表省略了大部分代码,只会保留关键代码。

state

state是保存状态的地方,接收一个对象。上面的示例中options就是我们传入store的state、getters、mutations、actions等等内容。所以可以直接从options拿到传入的state

// vuex.js
const Store = function(options = {}){
  this.state = options.state || {}
}

接着随便在state中传入一个参数

// store.js
...
export default createStore({
  state: {
    count: 0
  }
}

这样就可以在组件中使用了

<template>
    <p>访问count: {{ $store.state.count }}</p>
</template>

页面上正常显示

图片.png

看起来state的实现非常简单,但是这肯定不是state的实现,它现在存在一个致命的问题,等实现完getters再来修改这个问题。

getters

getters可以认为是 store 的计算属性。它接收一个state的参数,所以需要遍历传入的函数将state传入函数。

// vuex.js
const Store = function(options = {}){
...
  this.getters = {}
  const getters = options.getters || {}
  Object.keys(getters).forEach(key => {
    Object.defineProperty(this.getters, key, {
      get: () => {
        return getters[key](this.state)
      }
    })
  })
}

写一个getters方法

...
getters: {
  printCount(state){
    return `当前count值为${state.count}`;
  }
}
...

在组件中使用printCount

<template>
  <p>访问count:{{ $store.state.count }}</p>
  
  <p>调用getters打印count:{{ $store.getters.printCount }}</p>
</template>

图片.png

也可以正常展示内容
不过开头说到了getters可以认为是 store 的计算属性,那么测试改一下state,看看getters结果会不会重写计算。

<template>
  <p>访问count:{{ $store.state.count }}</p>
  
  <p>调用getters打印count:{{ $store.getters.printCount }}</p>
  <p>修改state查看getters是否会改变:<button @click="$store.state.count = 2">修改</button></p>
</template>

执行之后页面显示

20230220_161639.gif

当点击修改按钮,count和getters并没有改变过来,这就是上面说的问题,state并不是响应性数据。那么在vue3中,将一个对象置为响应性对象,可以使用reactive在vuex中同样可以使用reactive添加state的响应性。我们首先需要导入vue,从vue中拿到这个方法。

var vue = require('vue');
...
this.state = vue.reactive(options.state || {}) // 响应性
...

这时再去修改count,页面上就能更新了。

20230220_164515.gif

mutations

mutations是专门改变state的地方,实现方式与getters类似。不同的是在订阅的时候返回一个函数。触发mutations是使用commit,接收两个参数,mutations名和附加参数。

// vuex.js
const Store = function(options = {}){
...
  this.mutations = {}
  const mutations = options.mutations || {}
  Object.keys(mutations).forEach(key => {
    Object.defineProperty(this.mutations, key, {
      get: () => {
        return (payload) => { // payload就是调用时传递的第二个参数
          mutations[key](this.state, payload)
        }
      }
    })
  })
...
}

// 将commit方法写到Store的原型上
Store.prototype.commit = function(mutationsName, payload){
  this.mutations[mutationsName](payload)
}

添加一个mutations方法

// store.js
...
mutations: {
  addCount(state, payload){
    state.count += payload;
  }
},
...

组件中使用

<template>
  <div>
    <p>访问count:{{ $store.state.count }}</p>

    <p>调用getters打印count:{{ $store.getters.printCount }}</p>

    <p>修改state查看getters是否会改变:<button @click="$store.state.count = 2">修改</button></p>

    <p>调用执行mutations: <button @click="$store.commit('addCount', 1)">执行</button></p>
  </div>
</template>

20230220_170346.gif

正常工作没问题

actios

同理,基本实现方式与mutatios保持一致,但是actions函数第一个参数是当前的store上下文,包含store所有的属性及函数。这时在订阅中传入的就是this而不是this.state。

// vuex.js
...
this.actions = {}
const actions = options.actions || {}
Object.keys(actions).forEach(key => {
  Object.defineProperty(this.actions, key, {
    get: () => {
      return (payload) => {
        actions[key](this, payload)
      }
    }
  })
})
...

// 同样添加dispath方法
Store.prototype.dispath = function(actionsName, payload){
  this.actions[actionsName](payload)
}

添加一个异步更改count的actios

// store.js
...
mutations: {
  addCount(state, payload){
    state.count += payload;
  }
},
...

组件中使用

<template>
  <div>
    <p>访问count:{{ $store.state.count }}</p>

    <p>调用getters打印count:{{ $store.getters.printCount }}</p>

    <p>修改state查看getters是否会改变:<button @click="$store.state.count = 2">修改</button></p>
      
    <p>调用执行mutations: <button @click="$store.commit('addCount', 1)">执行</button></p>

    <p>调用执行actions: <button @click="$store.dispath('asyncAddCount', 1)">执行</button></p>
  </div>
</template>

20230220_171212.gif

现在,基本完成了vuex核心概念中的四个的实现。但是现在的vuex只能在模板中和除开setup外其它选项式api中使用,想在setup中使用还得实现一个组合式apiuseStore

useStore

useStore函数返回一个store,肯定不能直接返回Store构造函数,这时需要返回构造函数Store的this。可以在install方法中使用provide方法向vue实例提供一个名为store的属性,值为当前this。然后在useStore函数中返回。

// vuex.js
...
Store.prototype.install = function(app){
  app.provide('store', this); // 将store提供到实例中
  app.config.globalProperties.$store = this; // 把store挂载到vue上
}

// 定义useStore方法
const useStore = function(){
  return vue.inject('store'); // 返回提供的store
}

export {
  createStore,
  useStore
}

在script setup中使用,并且结合vue的computed和watch监听state的变化。

<script setup>
import { computed } from '@vue/reactivity';
import { watch } from 'vue';
import { useStore } from './vuex'

const store = useStore();

const count = computed(() => store.state.count);
watch(count, (newVal, oldVal) => {
  console.log(newVal, oldVal); // 每次更改都将会被监听到
})
</script>

vuex.js文件完整代码

var vue = require('vue');

function createStore(options){
  return new Store(options)
}

const Store = function(options = {}){
  this.state = vue.reactive(options.state || {}) // 响应性

  this.getters = {}
  const getters = options.getters || {}
  Object.keys(getters).forEach(key => {
    Object.defineProperty(this.getters, key, {
      get: () => {
        return getters[key](this.state)
      }
    })
  })

  this.mutations = {}
  const mutations = options.mutations || {}
  Object.keys(mutations).forEach(key => {
    Object.defineProperty(this.mutations, key, {
      get: () => {
        return (payload) => {
          mutations[key](this.state, payload)
        }
      }
    })
  })
  
  this.actions = {}
  const actions = options.actions || {}
  Object.keys(actions).forEach(key => {
    Object.defineProperty(this.actions, key, {
      get: () => {
        return (payload) => {
          actions[key](this, payload)
        }
      }
    })
  })
}
Store.prototype.install = function(app){
  app.provide('store', this); // 将store提供到实例中
  app.config.globalProperties.$store = this; // 把store挂载到vue上
}
Store.prototype.commit = function(mutationsName, payload){
  this.mutations[mutationsName](payload)
}
Store.prototype.dispath = function(actionsName, payload){
  this.actions[actionsName](payload)
}
const useStore = function(){
  return vue.inject('store'); // 返回提供的store
}

export {
  createStore,
  useStore
}

在这个代码中,Object.keys(actions).forEach...重复率非常高,所以可以优化一下。

var vue = require('vue');

// 提取的公共代码
const forEachValue = (params, target, callback) => {
  Object.keys(params).forEach(key => {
    Object.defineProperty(target, key, {
      get: () => {
        return callback(params[key])
      }
    })
  })
}

function createStore(options){
  return new Store(options)
}
const Store = function(options = {}){
  this.state = vue.reactive(options.state || {}) // 响应性

  this.getters = {}
  const getters = options.getters || {}
  forEachValue(getters, this.getters, (val) => val(this.state))

  this.mutations = {}
  const mutations = options.mutations || {}
  forEachValue(mutations, this.mutations, (val) => {return (payload) => val(this.state, payload)} )

  this.actions = {}
  const actions = options.actions || {}、
  forEachValue(actions, this.actions, (val) => {return (payload) => val(this, payload)} )
}
Store.prototype.install = function(app){
  app.provide('store', this); // 将store提供到实例中
  app.config.globalProperties.$store = this; // 把store挂载到vue上
}
Store.prototype.commit = function(mutationsName, payload){
  this.mutations[mutationsName](payload)
}
Store.prototype.dispath = function(actionsName, payload){
  this.actions[actionsName](payload)
}
const useStore = function(){
  return vue.inject('store'); // 返回提供的store
}

export {
  createStore,
  useStore
}