说完Vuex原理,面试官起身喝了杯茶😇

1,078 阅读3分钟

前言

不知不觉发现现在的文章很多围绕着面试展开,我也来蹭蹭这种氛围。

我相信对于技术栈是vue攻城狮来讲,光光只会应用vue是远远不够的,文本主要以状态管理器vuex来展开,让我们来手写一个属于我们自己的vuex吧!

一、vuex是什么?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension (opens new window),提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

1.🤪了解vuex

在我们的项目里面,如果在多个页面或者多个组件之间存在着某种依赖关系,这些页面都需要共享一个状态的时候,这时候就出现了以下问题:

  • 多个页面共享一个状态。
  • 一个页面修改了状态,另外的页面也需要跟着改动。

很多小伙伴可能会说利用可以利用父子组件传参来解决。但是当我们的项目过于庞大了之后,不利于维护这种状态,以至于到最后 ~~~~~ 你懂得

1639186592(1).png

2.🤯安装使用vuex

进入项目,在命令行中输入安装指令,回车

npm install vuex --save

然后再src/store/目录下创建一个index.js文件

import Vue from "vue";
import Vuex from "../vuex/index";

Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        name: 'zxl'
    },
    mutations: {
        setName(state) {
            state.name += '1';
        },
    },
    actions: {
        setName(store){
            store.commit("setName");
        }
    },
    getters: {
        
    },
})

在入口文件main.js引入

import Vue from 'vue';
import App from './App.vue';
import store from "../src/store/store";//在此引入

Vue.config.productionTip = false;

new Vue({
  store,++++++++++++++++++++
  render: h => h(App),
}).$mount('#app');

在页面中引用状态

<template>
  <div class="hello">
    {{$store.state.name}}
  </div>
</template>

具体的应用小编这里就不在做过多的介绍了。

下载.jpg

二、手动实现一个vuex

1.😇先观察我们配置的文件

为了能够更清晰手写vuex,我们先来看下我们上面配置的index.js文件。

1639187464(1).png

  1. 首先导入了vuexvue
  2. 直接在vue上注册了vuex,从这里我们可以知道vuex可以被vue。use方法使用,vuex里面必定提供了一个install方法。
  3. 直接执行了new Vuex.store,从这里我们又可以知道vuex提供了Store属性,该属性是一个class,可以被new关键字使用,构造函数里面传入我们的配置对象。

接下来我们就跟着这个步骤来实现属于我们自己得状态管理器吧!

2.👶install注册函数的实现

我们在我们自己项目src目录下创建一个vuex文件,这个文件就放我们自己写的代码,然后再目录下创建一个index.js文件。

1639188143(1).png

index.js我们需要导出个对象,对象里面有两个东西,一个是install方法、一个是Store类。

export default {
    Store,
    install
}

首先我们现在实现下install方法。

let Vue;
function install(_Vue) {
    Vue = _Vue;//缓存Vue构造函数,后面要用到
    Vue.mixin({
        beforeCreate() {
            if (this.$options.store) {//如果是根实例
                this.$store = this.$options.store;
            } else {//如果不是根实例,就拿父亲实例上的$store
                this.$store = this.$parent && this.$parent.$store;
            }
        }
    })
}

利用Vue.mixin来进行全局混入,在所有组件上 beforeCreate 生命周期注入了设置 this.$store 这样一个对象。

2.😨state实现

前面我们实现了install方法,那么接下来就实现这个核心Store类吧!

class Store{
    constructor(options){//options是我们传入的配置对象
        
    }
}

state和我们全局对象的区别就是当我们修改状态时,会触发响应式来更新试图。所以在这里尤大大果然还是个聪明人。我们知道在组件里,data上定义的属性是已经经过响应式化的,那么可以不可以把state也定义在vuedata上。

class Store{
    constructor(options){//options是我们传入的配置对象
        this.vm = new Vue({//在这里又new了一个vue
            data: {
                state: options.state,//在data上定义了state对象
            }
        })
    }
    get state() {//获取state
        return this.vm.state;
    }
}

3.🤩getters实现

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

class Store{
    constructor(options){//options是我们传入的配置对象
        let getters = options.getters;//拿到配置对象的getters对象
        this.getters = {};//在当前实例上定义getters
        Object.keys(getters).forEach(getterName => {
            Object.defineProperty(this.getters, {
                get() {
                    return getters[getterName](this.state);//执行getters中的回调函数,将自身state传入
                }
            })
        })
    }
}
  • 先拿到配置对象上的getters对象。
  • Store实例上定义了getters
  • 遍历对象key,通过Object.defineProperty在自身this.getters上定义了get监听,将自身state作为参数执行了配置对象中getters回调函数。

4.🤔mutations实现。

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type)  和 一个 回调函数 (handler) 。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数

class Store{
    constructor(options){//options是我们传入的配置对象
        let mutations = options.mutations;
        this.mutations = {};
        Object.keys(mutations).forEach(mutationName => {
            this.mutations[mutationName] = (payload) => {
                mutations[mutationName](this.state, payload);
            }
        })
    }
    commit(mutationName, payload) {
        this.mutations[mutationName](payload);
    }
}

代码逻辑大致是相同的,只不过多了一个commit方法。

5.🤖actions实现。

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。

class Store{
    constructor(options){//options是我们传入的配置对象
        let actions = options.actions;
        this.actions = {};
        Object.keys(actions).forEach((actionName) => {
            this.actions[actionName] = (payload) => {
                actions[actionName](this, payload);
            }
        })
    }
    dispatch(actionName, payload) {
        this.actions[actionName](payload);
    }
}

自此大家可以看看自己得一个基本的状态管理器能够正常使用了

1639191211(1).png

vuex的引入改成自己文件的路径。

src=http___qqpublic.qpic.cn_qq_public_0_0-2485884314-3301BD658FDDF980B77F435D8311A77A_0_fmt=jpg&size=32&h=652&w=640&ppv=1.jpg&refer=http___qqpublic.qpic.jpg

6.🤖modules

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module) 。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。

这里我需要维护这样一个modules的父子关系。

class Store{
    constructor(options){//options是我们传入的配置对象
        this.modules = new ModuleCollection(options);
        installModule(this, this.state, [], this.modules.root);
    }
}
class ModuleCollection {
    constructor(options) {
        this.register([], options);
    }

    register(path, rootModule) {
        let rawModule = {
            _raw: rootModule,
            _children: {},//儿子modules
            state: rootModule.state,//当前的state
        }
        if (!this.root) {//如果是根module
            this.root = rawModule;
        } else {
            let parentModule = path.slice(0, -1).reduce((root, current) => {
                return root._children[current];
            }, this.root);
            parentModule._children[path[path.length - 1]] = rawModule;
        }

        if (rootModule.modules) {//如果有孩子,options上定义了modules
            Object.keys(rootModule.modules).forEach(moduleName => {
                this.register(path.concat(moduleName), rootModule.modules[moduleName]);
            })
        }
    }
}

不知道为什么掘金写文章今天上传图片一直失败,本来还想把生成的modules给大家看下的。大家可以打印下自己项目里的this.$store,里面会有个_modules,这段代码的作用就是一个module里可以新添加一个module,维护了子父module这种关系。

修改之前的代码

function installModule(store, rootState, path, rawModule) {
    if (path.length > 0) {//如果是子模块
        let parentState = path.slice(0, -1).reduce((root, current) => {
            return rootState[current];
        }, rootState)
        Vue.set(parentState, path[path.length - 1], rawModule.state);//将子module的状态state定义到根模块上。
    }

    let getters = rawModule._raw.getters;
    if (getters) {//定义getters
        if (!store.getters) {
            store.getters = {};
        }
        Object.keys(getters).forEach(getterName => {
            Object.defineProperty(store.getters, getterName, {
                get() {
                    return getters[getterName](rawModule.state);
                }
            })
        })
    }
    let mutations = rawModule._raw.mutations;
    if (mutations) {
        Object.keys(mutations).forEach(mutationName => {
            if (!store.mutations) {
                store.mutations = {};
            }
            let arr = store.mutations[mutationName] || (store.mutations[mutationName] = []);
            arr.push((payload) => {
                mutations[mutationName](rawModule.state, payload);
            })
        })
    }
    let actions = rawModule._raw.actions;
    if (actions) {
        Object.keys(actions).forEach(actionName => {
            if (!store.actions) {
                store.actions = {};
            }
            let arr = store.actions[actionName] || (store.actions[actionName] = []);
            arr.push((payload) => {
                actions[actionName](store, payload);
            })
        })
    }

    let keys = Object.keys(rawModule._children);
    keys.forEach(moduleName => {
        installModule(store, rootState, path.concat(moduleName), rawModule);
    })
}

这段代码和我们上面的操作是基本一样的,不同的区别就是它通过vue.set将子module的状态state定义到根模块上。

7.🤗registerModule

registerModule(moduleName, module) {//动态加载模块
        if (!Array.isArray(moduleName)) {
            moduleName = [moduleName];
        }
        this.modules.register(moduleName, module);
        installModule(this, this.state, moduleName, module);
}

参考链接

# 手把手教你使用Vuex,猴子都能看懂的教程

# 字节跳动面试官:请说一下vuex工作原理(重点就几行代码而已啦)