一步一步手写vuex4.0基本功能

2,973 阅读5分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,    点击了解详情一起参与。

前言

vuex解决的是不同组件之间的通信问题,不用关心它是父子关系还是兄弟关系,都可以拿到共同的数据,是vue全家桶中比较重要的一个角色。

组件间的通信大概是这样,如下图

未命名文件 (1).png

我们有很多组件,根组件、子组件、孙子组件、兄弟组件。可能有一些数据,在根组件,子组件,兄弟组件都需要用到,那么就需要共享这些数据。

实现的方案:定义一个store对象,把这个对象注入到每个组件上,这样每个组件都可以拿到里面的数据了,vuex4借用了vue3里面的provide和inject这两个api, provide代表提供数据,我们可以在最外层提供数据,然后在需要用到的组件上用inject注入数据

废话不多说,我们先创建一个vue项目,用一下vuex4。以todolist为例,里面包含新增,删除,修改状态,状态过滤等功能,项目基本结构如下:

image.png

先来看下main.js

//这是main.js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App)
//.use是插件语法,会默认调用store中的install方法,这和vue2基本上是一样的
.use(store)  
.mount('#app')

在store中,我们定义了一些数据和方法,然后通过createStore创建了一个容器,并导出它

//这是store/index
import { createStore } from "@/vuex";//这里的@/vuex是自己的
const store = {
  state: {
    todos: [],
    filter: "all",
    id: 0,
  },
  //计算属性
  getters: {
    filteredTodos({ filter, todos }) {
      const map = {
        finished: todos.filter(({ isFinished }) => isFinished),
        unfinished: todos.filter(({ isFinished }) => !isFinished),
      };
      return map[filter] || todos;
    },
  },
  //可以调用其他action或者mutation
  actions: {
    addTodo({ commit }, text) {
      commit("addTodo", text);
    },
    toggleTodo({ commit }, id) {
      commit("toggleTodo", id);
    },
    removeTodo({ commit }, id) {
      commit("removeTodo", id);
    },
  },
  //可以更改状态,但是必须是同步更改的
  mutations: {
    addTodo(state, text) {
      state.todos.push({ id: state.id++, text, isFinished: false });
    },
    toggleTodo(state, id) {
      state.todos = state.todos.map((row) => {
        if (row.id === id) row.isFinished = !row.isFinished;
        return row;
      });
    },
    removeTodo(state, id) {
      state.todos = state.todos.filter((row) => row.id !== id);
    },
    setFilter(state, filter) {
      state.filter = filter;
    },
  },
};
export default createStore(store);
//这是App.vue
<template>
  <div>
    <a
      :key="tab"
      v-for="tab in tabs"
      @click="setFilter(tab)"
      :class="{ active: $store.state.filter === tab }"
    >
      {{ tab }}
    </a>
  </div>
  <div>
    <input type="text" placeholder="请输入内容" v-model="inputRef" />
    <button @click="addTodo">addTodo</button>
  </div>
  <div v-for="item of store.getters.filteredTodos" :key="item.id">
    <input
      type="checkbox"
      :checked="item.isFinished"
      @click="toggleTodo(item.id)"
    />
    <span :class="{ isFinished: item.isFinished }">{{ item.text }}</span>
    <button @click="removeTodo(item.id)">Delete</button>
  </div>
</template>

<script setup>
import { useStore } from "@/vuex";//这里的@/vuex是自己的
import { ref } from "vue";
const store = useStore();
const tabs = ["all", "unfinished", "finished"];
const inputRef = ref("");
const toggleTodo = (id) => {
  store.dispatch("toggleTodo", id);
};
const removeTodo = (id) => {
  store.dispatch("removeTodo", id);
};
const setFilter = (filter) => {
  store.commit("setFilter", filter);
};
const addTodo = () => {
  store.dispatch("addTodo", inputRef.value);
};
</script>
<style lang="scss" scoped>
.isFinished {
  text-decoration: line-through;
}
a {
  margin-right: 15px;
  text-decoration: none;
  cursor: pointer;
  &.active {
    color: #006aff;
  }
}
</style>

通过以上代码,就生成了一个简易的todolist界面

tutieshi_524x452_15s.gif

实现我们自己的 mini-vuex

mini-vuex的功能仅包含:

  1. store多例的实现
  2. store的注册和派发
  3. state的响应式的实现
  4. 实现gettersmutationsactionscommitdispatch

首先实现 store 的多例,派发和注册以及state的响应式

分析以上代码得知,我们的vuex需要导出createStore和useStore两个方法

createStore表示创建一个容器,并且它返回的对象上需要有个install方法,因为在main.js中使用use注册了它 由于vuex4.0是多例模式,可以创建多个store,我们可以在注册它的时候带上一个标识

//main.js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App)
//在注册的时候可以带上一个标识来表示命名空间,也可以不带,如果不带,会给一个默认的
.use(store,'my') 
.mount('#app')
//app.vue 这是其中一段代码。。。
//如果注册的是时候带上了标识,那么使用的时候也要把这个标识带上
const store=useStore('my')
image.png

createStore().install

通过debugger可以看出,vue的use(),会调用传入实例的install方法,并把当前Vue实例app和其他参数(标识符/命名空间)传入install方法中,这时就可以在install方法里,把当前实例注入到app中去 install(app,injectKey=StoreKey){ app.provide(injectKey,this) }

useStore()

当组件需要用到store时,就会调用useStore(),然后通过vue提供的inject api,把store对象注入到每个组件中,通过调试可以看到,inject(key)会查找vue实例中所有provide的对象,然后通过传入的key找到对应的store对象返回

image.png

全局属性$store

vue3为了兼容vue2,仍然允许在模版中使用<template>{{$store}}</template>这个对象, 我们可以使用vue3提供的api: app.config.globalProperties,给当前的应用增加一个全局属性$store

app.config.globalProperties.$store=this

响应式state

通过vue提供的 reactive api,定义一个内部变量 store._state=reactive({data:option.state}), 这里多包一层的原因是,考虑到vuex中有一个比较重要的方法replaceState,它会给vuex中的state重新赋值,如果这里去掉data使用store._state=reactive(option.state) 的话,就会失去响应式,因为reactive数据被重新赋值后,原来数据的代理函数和最新的代理函数不是同一个会导致视图不更新,所以需要多包一层data,利用对象引用的特性来给state赋值,届时就可以通过 store._state.data='xx'来更新视图

然后通过类的属性访问器,往外暴露state get state(){return this._state.data }

//vuex/index.js
import { inject, reactive } from "vue";
/**
 * 当我们use(store)的时候会调用vuex的install方法,展开来看就是
 * use(createStore(options)),而createStore返回了一个Store类,所以需要在
 * Store类上加一个install方法
 */
const StoreKey='store'
class Store {
 construtor(options){
     const store = this //这里保存一下this
     store._state=reactive({data:option.state})
 }
 get state(){//属性访问器
     return this._state.data
 }
 /**
  * @param  app vue实例
  * @param  injectKey 命名空间,不传就给默认的
  */  
 // 
 install(app,injectKey=StoreKey){
 //把当前实例注入到app中去,在全局暴露一个store实例
   app.provide(injectKey,this)
   //增加全局$store属性 等同于 Vue.proptotype.$store = this
   app.config.globalProperties.$store=this 
   
 }
}
/**
 * @param options 创建store时传入的选项
 */
function createStore(options){
    return new Store(options)
}

function useStore(injectKey=StoreKey){
    return inject(injectKey)
}

到这一步,mini-vuex的注册,派发,响应式就实现了

当然这样的插件目前还无法使用,接下来就要开始继续完善了

实现getters、mutations、actions、commit、dispatch

在官方提供的vuex中,我们可以看到 _mutations,_actions是没有原型链的,说明他不需要继承任何东西

image.png

所以,接下来在构造函数中定义 两个不带原型链的内部变量 _mutations和_actions image.png

回顾一下我们之前定义的mutations,里面每个方法的入参都有state,那这个state怎么传进去的?

mutations

我们可以遍历用户的mutations,把里面的每个方法,都重新定义到_mutations上,这样就可以把state,加上用户的入参一起传到每个方法里

 class Store{
     constructor(options){
      //省略一部分代码...
      const store = this
      store._mutations = Object.create(null); //创建没有原型链的对象
      store._actions = Object.create(null);//创建没有原型链的对象
      Object.keys(options.mutations).forEach((key) => {
            //把用户定义的方法,重新挂到内部变量_mutations上
            store._mutations[key] = (payload) =>//payload是用户的传参
            /**
             * 这里重新定义一下this的指向,把它指向store,虽然不重新定义this指向
             * 也没有关系,但是用户可能在方法内部需要使用store实例
             */ 
            options.mutations[key].call(store, store.state, payload);
        });
     }
     commit(type,payload){
        this._mutations[type](payload);
     }
     //省略一部分代码...
 }
 //省略一部分代码...
   

actions

同样的 actions也可以这样处理

getters

image.png image.png

回顾下我们定义的getters,filteredTodos是个方法,但是我们使用的时候用的是store.getters.filteredTodos并没有带括号,说明他肯定是用Object.defineProperty包装后的属性 我们可以定义store.getters={},然后遍历用户传入的options.getters,把它里面每个属性都再挂到store.getters={}上,然后用Object.defineProperty重新包装一下

commit、dispatch

image.png

一般在调用commit的时候,会传两个参数:type和payload,那么我们就可以在store._mutations根据type找到对应的方法,并且执行,同时传入payload. 同理 dispatch也是一样,只不过它是在store._actions上找

注意

这里commit的写法需要小心一点,如果按照下面的写法,会报错:this是undefend

原因在于 用户在使用commit的时候,是解构出来的,对于一个类而言,如果使用的是从类里面解构出来的方法,那么它的this指向,会发生变化
 class Store{
     constructor(options){
      //省略一部分代码...
     }
     commit(type,payload){
        this._mutations[type](payload);
     }
     //省略一部分代码...
 }
 //省略一部分代码...

解决方法:

使用es7的箭头函数

 class Store{
     constructor(options){
      //省略一部分代码...
     }
     commit=(type,payload)=>{
        this._mutations[type](payload);
     }
     //省略一部分代码...
 }
 //省略一部分代码...
import { inject, reactive } from "vue";
class Store{
    constructor(options){
        const store=this
        store._state = reactive({ data: options.state });
        store._mutations=Object.create(null)
        store._actions=Object.create(null)
        store.getters = {};
        //遍历用户传入的mutations
        Object.keys(options.mutations).forEach((key) => {
            //把用户定义的方法,重新挂到内部变量_mutations上
            store._mutations[key] = (payload) =>//payload是用户的传参
            /**
             * 这里重新定义一下this的指向,把它指向store,虽然不重新定义this指向
             * 也没有关系,但是用户可能在方法内部需要使用store实例
             */ 
            options.mutations[key].call(store, store.state, payload);
        });
        Object.keys(options.actions).forEach((key) => {
            store._actions[key] = (payload) => 
      //注意,这里传入的是store,因为用户的方法实际上会解构store拿出里面的{commit}
                options.actions[key].call(store, store, payload);
        });
        Object.keys(options.getters).forEach((key) => {
            Object.defineProperty(store.getters, key, {
              get: () => options.getters[key](store.state),
            });
        });
    }
    commit=(type,payload)=>{
        this._mutations[type](payload);
     }
     dispatch=(type,payload)=>{
        this._actions[type](payload);
     }
 //省略一部分代码...

}
 //省略一部分代码...

优化

上面的 mutations,actions,getters的遍历还可以再优化下,把循环部分拿出来,提出公共的方法

import { inject, reactive } from "vue";
const StoreKey = "store";

 //定义一个公共的方法
 function foreachValue(obj,callback){
     Object.keys(obj).forEach(key=>callback(obj[key],key))
 }
 class Store {
     constructor(options) {
        const { state, getters, mutations, actions } = options;
        const store = this;
        store._state = reactive({ data: state });
        store._mutations = Object.create(null);
        store._actions = Object.create(null);
        store.getters = {};
        foreachValue(mutations, (mutationFn, key) => {
          store._mutations[key] = (payload) =>
            mutationFn.call(store, store.state, payload);
        });
        foreachValue(actions, (actionFn, key) => {
          store._actions[key] = (payload) => actionFn.call(store, store, payload);
        });
        foreachValue(getters, (getterFn, key) => {
          Object.defineProperty(store.getters, key, {
            get: () => getterFn(store.state, store.getters),
          });
        });
      }
      get state() {
        return this._state.data;
      }
      commit = (type, payload) => {
        this._mutations[type](payload);
      };
      dispatch = (type, payload) => {
        this._actions[type](payload);
      };
      install(app, injectKey = StoreKey) {
        app.provide(injectKey, this);
        app.config.globalProperties.$store = this;
      }
}
/**
 * @param options 创建store时传入的选项
 */
function createStore(options){
    return new Store(options)
}

function useStore(injectKey=StoreKey){
    return inject(injectKey)
}

以上就完成了vuex4.0最基本的功能,有些的不好的地方欢迎大佬们批评指正