美团面试官:那你讲一讲Vuex吧

654次阅读  |  发布于1年以前

前言

周一接到了美团的一面,面试官人很好,基本都是围绕着简历来问,下面就是我重新整理了一下怎么实现Vuex的min简单版本,可以看到Vuex的大致原理。

vuex的基本使用

//index.js
import { createStore } from './gvuex.js'
const store = createStore({
 // 定义store的状态
 state() {
   return {
     count: 1 
   }
 },
 // 定义获取state状态数据的计算属性getters,以下的值都被认为是state的派生值
 getters: {
   double(state) { 
     return state.count * 2
   }
 },
 // 定义修改state状态数据的方法mutations
 mutations: {
   add(state) { 
     state.count++
   }
 },
 // 定义异步操作的方法actions
 actions: {
   asyncAdd({ commit }) { 
     setTimeout(() => {
       commit('add')
     }, 1000)
   }
 }
})
// App.vue
<script setup>
import { useStore } from '../store/gvuex'
import { computed } from 'vue'
let store = useStore();
let count = computed(()=>{ store.state.count })
let double = computed(()=>{ store.getters.double })
function add() {
 store.commit('add')
}
function asyncAdd() {
 store.dispatch('asyncAdd')
}
</script>
<template>
<div class="">
 {{ count }} * 2 = {{ double }}
 <button @click="add">add</button>
 <button @click="asyncAdd">async add</button>
</div>
</template>
<style scoped>
</style>

知道了vuex的用法,你会不会发出以下疑问:

  1. 为什么要store.commit('add')才能触发事件执行呢? 可不可以进行直接调用mutation函数进行操作呢?
  2. 为什么不可以直接对state存储的状态进行修改,只能通过调用mutation函数的方式修改呢?
  3. 为什么存在异步调用的函数需要store.dispatch('asyncAdd')函数才能完成呢?可以直接调用store.commit('asyncAdd')嘛?如果不可以,为什么呢?
  4. createStore()useStore()到底发生了什么?

那么下面就来一一解密吧。

vue里注册全局组件

import { createApp } from 'vue'
import store from './store'
import App from './App.vue'
const app = createApp(App)
app
 .use(store)
 .mount('#app')

app.use() 用来安装插件,接受一个参数,通常是插件对象,该对象必须暴露一个install方法,调用app.use()时,会自动执行install()方法。

解析Store类里的流程

import { reactive, inject } from 'vue'
// 定义了一个全局的 key,用于在 Vue 组件中通过 inject API 访问 store 对象
const STORE_KEY = '__store__'
// 用于获取当前组件的 store 对象
function useStore() {
 return inject(STORE_KEY)
}
// Store 类,用于管理应用程序状态
class Store {
 // 构造函数,接收一个包含 state、mutations、actions 和 getters 函数的对象 options,然后将它们保存到实例属性中
 constructor(options) {
   this.$options = options;
   // 使用 Vue.js 的 reactive API 将 state 数据转换为响应式对象,并保存到实例属性 _state 中 
   this._state = reactive({
     data: options.state()
   })
   // 将 mutations 和 actions 函数保存到实例属性中
   this._mutations = options.mutations
   this._actions = options.actions;
   // 初始化 getters 属性为空对象
   this.getters = {};
   // 遍历所有的 getters 函数,将其封装成 computed 属性并保存到实例属性 getters 中
   Object.keys(options.getters).forEach(name => {
     const fn = options.getters(name);
     this.getters[name] = computed(() => fn(this.state));
   })  
 }
 // 用于获取当前状态数据
 get state() {
   return this._state.data
 }
 // 获取mutation内定义的函数并执行
 commit = (type, payload) => {
   const entry = this._mutations[type]
   entry && entry(this.state, payload)
 }
 // 获取actions内定义的函数并返回函数执行结果
 // 简略版dispatch
 dispatch = (type, payload) => { 
   const entry = this._actions[type];
   return entry && entry(this, payload)
 }
 // 将当前 store 实例注册到 Vue.js 应用程序中
 install(app) {
   app.provide(STORE_KEY, this)
 }
}
// 创建一个新的 Store 实例并返回
function createStore(options) {
 return new Store(options);
}
// 导出 createStore 和 useStore 函数,用于在 Vue.js 应用程序中管理状态
export {
 createStore,
 useStore
}

是不是很惊讶于vuex的底层实现就短短几十行代码就实现了,嘿嘿那是因为从vue里引入了reactiveinjectcomputed,并且对很大一部分的源码进行了省略,dispatchcommit远比这复杂多了,有兴趣去了解reactive的实现可以去看我另一篇文章学VUE源码之手写min版响应式原型 \- 掘金 \(juejin.cn\)[1],下面解答上面抛出的问题吧。

解答

问题一:为什么要store.commit('add')才能触发事件执行呢? 可不可以进行直接调用mutation函数进行操作呢?

解答:store类里根本没有mutation方法,只能通过调用commit方法来执行mutation里的函数列表。

问题二:为什么不可以直接对state存储的状态进行修改,只能通过调用函数的方式修改呢?

解答:Vuex 通过强制限制对 store 的修改方式来确保状态的可追踪性。只有通过 mutation 函数才能修改 store 中的状态,这样可以轻松地跟踪状态的变化,也可以避免无意中从不同的组件中直接修改 store 导致的代码难以维护和调试的问题。

问题三:为什么存在异步调用的函数需要store.dispatch('asyncAdd')函数才能完成呢?可以直接调用store.commit('asyncAdd')嘛?如果不可以,为什么呢?

解答:实际上dispatch方法和commit方法远不止这么简单,下面先贴出部分vuex的关于这两个方法的源码部分

Store.prototype.dispatch = function dispatch (_type, _payload) {
 var this$1$1 = this;
 // check object-style dispatch
 var ref = unifyObjectStyle(_type, _payload);
 var type = ref.type;
 var payload = ref.payload;
 var action = { type: type, payload: payload };
 var entry = this._actions[type];
 if (!entry) {
   if ((process.env.NODE_ENV !== 'production')) {
     console.error(("[vuex] unknown action type: " + type));
   }
   return
 }
 try {
   this._actionSubscribers
     .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
     .filter(function (sub) { return sub.before; })
     .forEach(function (sub) { return sub.before(action, this$1$1.state); });
 } catch (e) {
   if ((process.env.NODE_ENV !== 'production')) {
     console.warn("[vuex] error in before action subscribers: ");
     console.error(e);
   }
 }
 var result = entry.length > 1
   ? Promise.all(entry.map(function (handler) { return handler(payload); }))
   : entry[0](payload);
 return new Promise(function (resolve, reject) {
   result.then(function (res) {
   try {
   this$1$1._actionSubscribers
     .filter(function (sub) { return sub.after; })
     .forEach(function (sub) { return sub.after(action, this$1$1.state); });
   } catch (e) {
     if ((process.env.NODE_ENV !== 'production')) {
       console.warn("[vuex] error in after action subscribers: ");
       console.error(e);
     }
   }
   resolve(res);
 }, function (error) {
   try {
     this$1$1._actionSubscribers
       .filter(function (sub) { return sub.error; })
       .forEach(function (sub) { return sub.error(action, this$1$1.state, error); });
   } catch (e) {
     if ((process.env.NODE_ENV !== 'production')) {
       console.warn("[vuex] error in error action subscribers: ");
       console.error(e);
     }
   }
   reject(error);
 });
 })
};
Store.prototype.commit = function commit (_type, _payload, _options) {
 var this$1$1 = this;
 // check object-style commit
 var ref = unifyObjectStyle(_type, _payload, _options);
 var type = ref.type;
 var payload = ref.payload;
 var options = ref.options;
 var mutation = { type: type, payload: payload };
 var entry = this._mutations[type];
 if (!entry) {
   if ((process.env.NODE_ENV !== 'production')) {
     console.error(("[vuex] unknown mutation type: " + type));
   }
   return
 }
 this._withCommit(function () {
   entry.forEach(function commitIterator (handler) {
     handler(payload);
   });
 });
 this._subscribers
   .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
   .forEach(function (sub) { return sub(mutation, this$1$1.state); });
 if (
 (process.env.NODE_ENV !== 'production') &&
 options && options.silent
 ) {
   console.warn(
   "[vuex] mutation type: " + type + ". Silent option has been removed. " +
   'Use the filter functionality in the vue-devtools'
   );
 }
};

源码里我们能看到,dispatch方法返回的是一个Promise对象,而commit方法没有返回值,完全进行的是同步代码的操作,虽然返回值可能适用的场景不多,但是这样设计的主要目的还是为了确保状态的可追踪性

问题四: createStore()useStore()到底发生了什么?

当我们去调用 createStore()函数,其构造函数就会接收一个包含 statemutationsactionsgetters 函数的对象 options, 然后将它们保存到实例属性中,此时state中的值都会转换为响应式对象,同时遍历所有的getters函数,将其封装成computed属性并保存到实例属性getters中,而在main.js里调用了app.use(), install方法自动执行,将将当前 store 实例注册到 Vue.js 应用程序中,只需要调用useStore()就可以拿到全局状态管理的store实例了,可以靠injectprovide实现全局共享。

总结

通过实现简单的vuex,完成基本的功能,对vuex有了不同的理解,但在vuex的源码方面,上面的demo只是简简单单做了大致的依赖关系整理,还有:不具备模块化功能、没有提供插件功能、没有提供严格模式、没有提供辅助函数和实现单例模式等等的实现。

参考资料

关于本文
作者:超人不会飛

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8