介紹
Vuex 是一個專為 Vue.js 應用程序開發(fā)的狀態(tài)管理模式。
這種集中管理應用狀態(tài)的模式相比父子組件通信來說,使數(shù)據(jù)的通信更方便,狀態(tài)的更改也更加直觀侮攀。
Bus
肯定有不少同學在寫 Vue 時使用過 new Vue()
創(chuàng)建 bus 進行數(shù)據(jù)通信。
import Vue from 'vue';
const bus = new Vue();
export default {
install(Vue) {
Object.defineProperty(Vue.prototype, '$bus', {
get () { return bus }
});
}
};
組件中使用 this.$bus.$on
this.$bus.$emit
監(jiān)聽和觸發(fā) bus 事件進行通信厢拭。
bus 的通信是不依賴組件的父子關系的魏身,因此實際上可以理解為最簡單的一種狀態(tài)管理模式。
通過 new Vue()
可以注冊響應式的數(shù)據(jù)蚪腐,
下面基于此對 bus 進行改造,實現(xiàn)一個最基本的狀態(tài)管理:
// /src/vuex/bus.js
let Vue
// 導出一個 Store 類税朴,一個 install 方法
class Store {
constructor (options) {
// 將 options.state 注冊為響應式數(shù)據(jù)
this._bus = new Vue({
data: {
state: options.state
}
})
}
// 定義 state 屬性
get state() {
return this._bus._data.state;
}
}
function install (_Vue) {
Vue = _Vue
// 全局混入 beforeCreate 鉤子
Vue.mixin({
beforeCreate () {
// 存在 $options.store 則為根組件
if (this.$options.store) {
// $options.store 就是創(chuàng)建根組件時傳入的 store 實例回季,直接掛在 vue 原型對象上
Vue.prototype.$store = this.$options.store
}
}
})
}
export default {
Store,
install
}
創(chuàng)建并導出 store 實例:
// /src/store.js
import Vue from 'vue'
import Vuex from './vuex/bus'
Vue.use(Vuex) // 調用 Vuex.install 方法
export default new Vuex.Store({
state: {
count: 0
}
})
創(chuàng)建根組件并傳入 store 實例:
// /src/main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
store,
render: h => h(App)
}).$mount('#app')
組件中使用示例:
<!-- /src/App.vue -->
<template>
<div id="app">
{{ count }}
<button @click="changeCount">+1</button>
</div>
</template>
<script>
export default {
name: 'app',
computed: {
count() {
return this.$store.state.count;
}
},
methods: {
changeCount() {
this.$store.state.count++
}
}
}
</script>
從零實現(xiàn)一個 Vuex
前一節(jié)通過 new Vue()
定義一個響應式屬性并通過 minxin 為所有組件混入 beforeCreate 生命周期鉤子函數(shù)的方法為每個組件內添加 $store
屬性指向根組件的 store 實例的方式,實現(xiàn)了最基本的狀態(tài)管理正林。
繼續(xù)這個思路泡一,下面從零一步步實現(xiàn)一個最基本的 Vuex。
以下代碼的 git 地址:simple-vuex
整體結構
let Vue;
class Store {}
function install() {}
export default {
Store,
install
}
install 函數(shù)
// 執(zhí)行 Vue.use(Vuex) 時調用 并傳入 Vue 類
// 作用是為所有 vue 組件內部添加 `$store` 屬性
function install(_Vue) {
// 避免重復安裝
if (Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error('[vuex] already installed. Vue.use(Vuex) should be called only once.');
}
return
}
Vue = _Vue; // 暫存 Vue 用于其他地方有用到 Vue 上的方法
Vue.mixin({
// 全局所有組件混入 beforeCreate 鉤子觅廓,給每個組件中添加 $store 屬性指向 store 實例
beforeCreate: function vuexInit() {
const options = this.$options;
if (options.store) {
// 接收參數(shù)有=中有 store 屬性則為根組件
this.$store = options.store;
} else if (options.parent && options.parent.$store) {
// 非根組件通過 parent 父組件獲取
this.$store = options.parent.$store;
}
}
})
}
Store 類
// 執(zhí)行 new Vuex.Store({}) 時調用
class Store {
constructor(options = {}) {
// 初始化 getters mutations actions
this.getters = {};
this._mutations = {};
this._actions = {};
// 給每個 module 注冊 _children 屬性指向子 module
// 用于后面 installModule 中根據(jù) _children 屬性查找子 module 進行遞歸處理
this._modules = new ModuleCollection(options)
const { dispatch, commit } = this;
// 固定 commit dispatch 的 this 指向 Store 實例
this.commit = (type, payload) => {
return commit.call(this, type, payload);
}
this.dispatch = (type, payload) => {
return dispatch.call(this, type, payload);
}
// 通過 new Vue 定義響應式 state
const state = options.state;
this._vm = new Vue({
data: {
state: state
}
});
// 注冊 getters mutations actions
// 并根據(jù) _children 屬性對子 module 遞歸執(zhí)行 installModule
installModule(this, state, [], this._modules.root);
}
// 定義 state commit dispatch
get state() {
return this._vm._data.state;
}
set state(v){
throw new Error('[Vuex] vuex root state is read only.')
}
commit(type, payload) {
return this._mutations[type].forEach(handler => handler(payload));
}
dispatch(type, payload) {
return this._actions[type].forEach(handler => handler(payload));
}
}
ModuleCollection 類
Store 類的構造函數(shù)中初始化 _modules 時是通過調用 ModuleCollection 這個類鼻忠,內部從根模塊開始遞歸遍歷 modules 屬性,初始化模塊的 _children 屬性指向子模塊杈绸。
class ModuleCollection {
constructor(rawRootModule) {
this.register([], rawRootModule)
}
// 遞歸注冊帖蔓,path 是記錄 module 的數(shù)組 初始為 []
register(path, rawModule) {
const newModule = {
_children: {},
_rawModule: rawModule,
state: rawModule.state
}
if (path.length === 0) {
this.root = newModule;
} else {
// 非最外層路由通過 reduce 從 this.root 開始遍歷找到父級路由
const parent = path.slice(0, -1).reduce((module, key) => {
return module._children[key];
}, this.root);
// 給父級路由添加 _children 屬性指向該路由
parent._children[path[path.length - 1]] = newModule;
// 父級路由 state 中也添加該路由的 state
Vue.set(parent.state, path[path.length - 1], newModule.state);
}
// 如果當前 module 還有 module 屬性則遍歷該屬性并拼接 path 進行遞歸
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule);
})
}
}
}
installModule
Store 類的構造函數(shù)中調用 installModule 矮瘟,通過 _modules 的 _children 屬性遍歷到每個模塊并注冊 getters mutations actions
function installModule(store, rootState, path, module) {
if (path.length > 0) {
const parentState = rootState;
const moduleName = path[path.length - 1];
// 所有子模塊都將 state 添加到根模塊的 state 上
Vue.set(parentState, moduleName, module.state)
}
const context = {
dispatch: store.dispatch,
commit: store.commit,
}
// 注冊 getters mutations actions
const local = Object.defineProperties(context, {
getters: {
get: () => store.getters
},
state: {
get: () => {
let state = store.state;
return path.length ? path.reduce((state, key) => state[key], state) : state
}
}
})
if (module._rawModule.actions) {
forEachValue(module._rawModule.actions, (actionFn, actionName) => {
registerAction(store, actionName, actionFn, local);
});
}
if (module._rawModule.getters) {
forEachValue(module._rawModule.getters, (getterFn, getterName) => {
registerGetter(store, getterName, getterFn, local);
});
}
if (module._rawModule.mutations) {
forEachValue(module._rawModule.mutations, (mutationFn, mutationName) => {
registerMutation(store, mutationName, mutationFn, local)
});
}
// 根據(jù) _children 拼接 path 并遞歸遍歷
forEachValue(module._children, (child, key) => {
installModule(store, rootState, path.concat(key), child)
})
}
installModule 中用來注冊 getters mutations actions 的函數(shù):
// 給 store 實例的 _mutations 屬性填充
function registerMutation(store, mutationName, mutationFn, local) {
const entry = store._mutations[mutationName] || (store._mutations[mutationName] = []);
entry.push((payload) => {
mutationFn.call(store, local.state, payload);
});
}
// 給 store 實例的 _actions 屬性填充
function registerAction(store, actionName, actionFn, local) {
const entry = store._actions[actionName] || (store._actions[actionName] = [])
entry.push((payload) => {
return actionFn.call(store, {
commit: local.commit,
state: local.state,
}, payload)
});
}
// 給 store 實例的 getters 屬性填充
function registerGetter(store, getterName, getterFn, local) {
Object.defineProperty(store.getters, getterName, {
get: () => {
return getterFn(
local.state,
local.getters,
store.state
)
}
})
}
// 將對象中的每一個值放入到傳入的函數(shù)中作為參數(shù)執(zhí)行
function forEachValue(obj, fn) {
Object.keys(obj).forEach(key => fn(obj[key], key));
}
使用
還有 modules、plugins 等功能還沒有實現(xiàn)塑娇,而且 getters 的并沒有使用 Vue 的 computed 而只是簡單的以函數(shù)的形式實現(xiàn)澈侠,但是已經(jīng)基本完成了 Vuex 的主要功能,下面是一個使用示例:
// /src/store.js
import Vue from 'vue'
import Vuex from './vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
changeCount(state, payload) {
console.log('changeCount', payload)
state.count += payload;
}
},
actions: {
asyncChangeCount(ctx, payload) {
console.log('asyncChangeCount', payload)
setTimeout(() => {
ctx.commit('changeCount', payload);
}, 500);
}
}
})
<!-- /src/App.vue -->
<template>
<div id="app">
{{ count }}
<button @click="changeCount">+1</button>
<button @click="asyncChangeCount">async +1</button>
</div>
</template>
<script>
export default {
name: 'app',
computed: {
count() {
return this.$store.state.count;
}
},
methods: {
changeCount() {
this.$store.commit('changeCount', 1);
},
asyncChangeCount() {
this.$store.dispatch('asyncChangeCount', 1);
}
},
mounted() {
console.log(this.$store)
}
}
</script>
閱讀源碼的過程中寫了一些方便理解的注釋埋酬,希望給大家閱讀源碼帶來幫助哨啃,github: vuex 源碼
參考
- vuex 源碼:如何實現(xiàn)一個簡單的 vuex -- cobish
- 用150行代碼實現(xiàn)Vuex 80%的功能 -- 殷榮檜
- Vuex 源碼解析 -- 染陌同學
- Vuex 源碼深度解析 -- yck
- Vuex 源碼分析 -- 網(wǎng)易考拉前端團隊
- Vue 源碼(三) —— Vuex -- 楊夕