# 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=""> | |
* 2 = | |
<button @click="add">add</button> | |
<button @click="asyncAdd">async add</button> | |
</div> | |
</template> | |
<style scoped> | |
</style> |
知道了 vuex 的用法,你会不会发出以下疑问:
- 为什么要
store.commit('add')
才能触发事件执行呢?可不可以进行直接调用mutation
函数进行操作呢? - 为什么不可以直接对
state
存储的状态进行修改,只能通过调用mutation
函数的方式修改呢? - 为什么存在异步调用的函数需要
store.dispatch('asyncAdd')
函数才能完成呢?可以直接调用store.commit('asyncAdd')
嘛?如果不可以,为什么呢? 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 里引入了 reactive
、 inject
和 computed
,并且对很大一部分的源码进行了省略, dispatch
和 commit
远比这复杂多了,下面解答上面抛出的问题吧。
# 解答
# 问题一:为什么要 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()
函数,其构造函数就会接收一个包含 state
、 mutations
、 actions
和 getters
函数的对象 options
, 然后将它们保存到实例属性中,此时 state
中的值都会转换为响应式对象,同时遍历所有的 getters
函数,将其封装成 computed
属性并保存到实例属性 getters
中,而在 main.js 里调用了 app.use (), install 方法自动执行,将将当前 store 实例注册到 Vue.js 应用程序中,只需要调用 useStore()
就可以拿到全局状态管理的 store 实例了,可以靠 inject
和 provide
实现全局共享。
# 总结
通过实现简单的 vuex,完成基本的功能,对 vuex 有了不同的理解,但在 vuex 的源码方面,上面的 demo 只是简简单单做了大致的依赖关系整理,还有:不具备模块化功能、没有提供插件功能、没有提供严格模式、没有提供辅助函数和实现单例模式等等的实现。