带你彻底攻略并实现一个简版的Vuex

227 阅读3分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。

前言

在本文里,我提及的Vuex版本还是Vuex3,不是与Vue3相对应的Vuex4,因为Vue3的项目更多可能是用pinia替代Vuex4作为状态管理库,最近我也在学习pinia,等学成后,我再来单独写一篇关于pinia的。虽然Vue3现在成为了主流版本,但和Vue2相关的技术还是得会,毕竟公司的老项目还依旧是基于Vue2的。

首先vuex是什么?

我在官网里找到了关于Vuex的定义,如下图

微信图片_20220211000328.png 有几个关键概念我加粗下,理解了这几个关键概念,Vuex这个Vue插件你就应该算是完全理解了。首先是个状态管理模式+库,然后采用的是集中式的管理方式,并用相应的规则保证状态变更的可预测性

微信图片_20220209110856.png

  • 状态管理模式 其实从上面的Vuex的状态管理模式图就可以发现,首先数据流是单向的。从组件派发一个Action,Action里可组合复杂的业务逻辑后,再提交一个Mutation改变state状态值,状态值改变后触发组件的更新函数,重新渲染组件。
  • 集中式 之所以称为集中式,因为所有组件的状态都集中交由Vuex的store类统一管理。
  • 可预测 那为什么是可预测呢?因为在严格模式开启下,所有的state变更都只能通过Vuex提供的Commit操作,这就确保了状态的稳定性,既然确保每次更改前都有Commit操作,那我们就可以在状态改变前执行些日志打印操作或接入Devtools用于调试。所以Vuex是可预测的。

接下来是Vuex如何使用?

Vuex的简单使用只需要4步走:

  1. 引入vuex,并执行Vue.use(Vuex)。
  2. 导出一个new Vuex.Store(options) 实例,options传递的是一个选项对象,包含state、mutations、actions、getters等。
  3. 在new Vue(options)的时候,把Store实例作为选项之一传进去,为了以后把Store实例挂载到了当前vue实例的this上。
  4. 在组件中可直接通过this.store访问state、getters里的状态值;可通过this.store.commit(type)提交一个mutation同步改变state状态值;可通过this.store.dispatch(type)派发一个action处理复杂逻辑,可包含异步操作,最终通过commit改变state状态值。 详细的使用教程可到Vuex官网查阅

最后一起来手写实现

  • 首先从Vuex使用的方式来看,导出去的Vuex是个对象,有install静态方法,有Store类用于管理状态。
  • install方法里用Vue.mixin混入一个beforeCreate生命周期函数,在Vue原型里注册$store属性。
  • state里的状态值变化,其依赖该状态值的组件也会重新渲染,所以state的状态值需要是响应式的;在Vuex里使用的是new Vue()的方式,把用户定义的state存入data选项的$$state属性,这样借助Vue就完成了state的响应式。
  • 细节1,为什么要用$$state属性名,因为我们不想用户直接操作这个state,而且在Vue源码里data里的响应式数据会代理到实例上,美元符号开头的属性名则不会被代理,这样就可以把它藏得深一些。然后通过Store类的存取器来暴露state获取状态值,并且可阻止用户直接覆盖state。这是Vuex能可预测的第一层封印。
  • 细节2,一开始我百思不得其解,为什么直接给state里的状态值赋值可以发出警告,而commit的方式却可以。后来在Vuex的源码里发现了答案,原来还是通过Vue定义一个用户watch,在每个state里的状态值改变的时候调用一个assert方法,assert内部会通过由commit调用引起的状态值变更,反之则报错。这是Vuex能可预测的第二层封印。
  • Vuex的getters则是通过Vue的computed选项完成,更多的细节都写在了代码的注释里了,在这就不一一列举。 详细的实现代码如下
// pvuex.js
let Vue;
function install(Vue_){
    Vue = Vue_;
    // 混入一个beforeCreate生命周期函数,在Vue原型里注册$store属性
    Vue.mixin({
        beforeCreate() {
            if(this.$options.store){
                Vue.prototype.$store = this.$options.store;
            }
        },
    })
}
class Store {
    constructor(options){
        // 作为在commit执行中的标识符
        this._committing = false
        // 保存用户的mutations、actions、getters的配置
        this._mutations = options.mutations;
        this._actions = options.actions;
        this._getters = options.getters;
        // 代表是严格模式下,不能直接给state里的状态值赋值,只可通过commit的方式提交修改
        this.strict = options.strict;
        // 定义配置在Vue里的computed,用Vue的computed特性实现getters机制
        let computed = {};
        // 暴露给外面使用的getters
        this.getters = {};
        // 把this保存一份
        const store = this;
        // 遍历每个_getters里的方法,封装一个高阶函数,目的是往遍历的方法里传递一个state
        Object.keys(this._getters).forEach(key => {
            const fn = this._getters[key];
            computed[key] = function(){
                return fn(store.state);
            }
            // 给store.getters的每个属性做一层访问代理,并封印getters里属性的setter
            Object.defineProperty(store.getters, key, {
                get(){
                    return store._vm[key];
                },
                set(val){
                    console.error('getters not set');
                    return;
                }
            });
        });
        // 借鸡生蛋,借助new Vue完成数据options.state的响应式
        this._vm = new Vue({
            data: {
                $$state: options.state
            },
            computed
        });

        // 如果是严格模式,则定义个用户watch,在每个state里的值改变的时候调用assert方法
        if(this.strict){
            this._vm.$watch(function () { return this._data.$$state }, (v) => {
                assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
              }, { deep: true, sync: true });
        }

        // 绑定commit和dispatch函数中的this指向
        this.commit = this.commit.bind(this);
        this.dispatch = this.dispatch.bind(this);
    }
    // 通过存取器获取响应后的state
    get state(){
        return this._vm._data.$$state;
    }
    // 直接覆盖state,将抛出错误
    set state(val){
        throw new Error();
    }
    
    commit(type, payload){
        const entry = this._mutations[type];
        if(!entry){
            console.error('unkown mutation type');
        }
        this._wrappedCommit(() => entry(this.state,payload))
    }

    dispatch(type, payload){
        const entry = this._actions[type];
        if(!entry){
            console.error('unkown action type');
        }
        entry(this,payload);
    }
    // 封装commit执行
    _wrappedCommit (fn) {
        const committing = this._committing;
        this._committing = true;
        fn();
        this._committing = committing;
    }
}

// 不合法抛出错误
function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`);
}

export default { Store , install}

测试

测试方案:新建vuecli项目作为测试项目,新建自己的pstore文件夹,把src/pstore/index.js里之前引入的vuex替换成我们自己实现的pvuex.js,在src/main.js里使用src/pstore/index.js导出的store,其他都不变,运行项目看看是否能正常运行。具体代码更改如下:

微信图片_20220211001904.png

src/pstore/index.js

微信图片_20220211001926.png

src/main.js

微信图片_20220211002300.png

src/HelloWorld.vue

微信图片_20220211003045.png

页面初始化展示。

微信图片_20220211013840.png

点击提交commit、dispatch后页面更新后,页面也显示正常。

微信图片_20220211013949.png

至此,测试项目确实保持正常运行,证明我们实现的简版Vuex没什么大问题。希望看完此文,大家能对Vuex3有更深的理解,写的若有失偏颇,尽可在评论里大家一起讨论。今天拜拜。