vuex是專門爲vuejs應用設計的一套狀態管理模式,提供了一套集中管理數據的概念和用法,用來解決中大型項目中組件間大量數據共享的問題。它的核心概念包括state、mutations、actions,基於這些它的工做流程是: vue
vuex最終export了一個對象這個對象包括了一個install方法和一個類Store, 注意對應咱們的使用方法vuex
let store = new Vuex.Store({})
複製代碼
install方法供Vue.use()使用,內部使用Vue.mixin在每個組件的beforeCreate生命週期中給組件混入了一個$store屬性,這個屬性就是那個惟一的store。編程
從new Vuex.Store(options)開始,有如下主要流程:api
除了以上內容以外vuex還導出了一些輔助函數好比mapState、mapMutations、mapActions。數組
嚴格模式下,mutation只能是同步。非嚴格模式下,若是你硬要使用異步也能夠,固然不建議這麼作。瀏覽器
若是咱們使用異步的方法來更改狀態,設想一下,咱們但願使用log來記錄狀態更改來源,當兩個組件同時修改同一個狀態,對應有兩個log, 那麼這兩個log到底分別對應的是哪一個組件呢?bash
由於是異步的因此這兩個修改動做的前後順序沒法保證,那麼咱們根本沒法判斷log和組件的對應關係,可能你覺得是這個組件的log,實際上它是另外一個組件的,這就讓咱們沒辦法去精確的進行調試和跟蹤狀態更改信息。異步
很簡單就是直接使用 new Vue() 構造了一個Vue實例:async
this.vm = new Vue({
data: {
state: options.state// 響應化處理
}
});
複製代碼
這樣state就是響應式的了,看到這其實你就應該明白爲何Vuex只能用在Vue項目中了。函數
當咱們在訪問state時還須要一層代理:
get state() {// 獲取實例上的state屬性就會執行此方法
return this.vm.state
}
複製代碼
固然這還沒結束,以上只是針對最外層的state,那麼若是咱們寫了modules,modules內部的模塊的state是怎麼處理的呢?
從這開始咱們就接觸到了Vuex最核心的東西了,上面咱們說了new一個Store實例時會先將用戶傳入的數據進行格式化,這就是register方法主要作的事,下面咱們就看看他究竟是怎麼格式化?格式化最終的結果是什麼?
咱們舉一個例子:
let store = new Vuex.Store({
state: {// 單一數據源
age: 10
},
getters: {
myAge(state) {
return state.age + 20;
}
},
strict: true,
// 嚴格模式下只能使用同步
mutations: {
syncChange(state, payload) {
state.age += payload;
}
},
actions: {
asyncChange({commit}, payload) {
setTimeout(() => {
commit('syncChange', payload);
}, 1000);
}
},
modules: {
a: {
namespaced: true,
state: {
age: 'a100'
},
mutations: {
syncChange() {
console.log('a-syncChange');
}
}
},
b: {
namespaced: true,
state: {
age: 'b100'
},
mutations: {
syncChange() {
console.log('b-syncChange');
}
},
modules: {
c: {
namespaced: true,
state: {
age: 'c100'
},
mutations: {
syncChange() {
console.log('c-syncChange');
}
},
}
}
}
}
});
複製代碼
先看看最終的結果
{
_raw: rootModule,
state: rootModule.state,
_children: {
a: {
_raw: aModule,
state: aModule.state,
_children: {}
},
b: {
_raw: aModule,
state: aModule.state,
_children: {
c: {
_raw: cModule,
state: cModule.state,
_children: {}
}
}
}
}
}
複製代碼
能夠看到,這仍是一個樹形結構, _raw就是用戶本身寫的格式化以前的模塊,state單獨拿了出來是由於咱們在安裝模塊時會用到,_children放的就是modules下面的內容, 固然子模塊下還有可能有孫子模塊...
劃重點,register
register(path, rootModule) {
let rawModule = {
_raw: rootModule,// 用戶傳入的模塊定義
_children: {},
state: rootModule.state
}
rootModule.rawModule = rawModule;// 雙向記錄
if (!this.root) {
this.root = rawModule;
} else {
// 經典方法 找到當前模塊的父親
let parentModule = path.slice(0, -1).reduce((root, current) => { // 註冊c的時候 [b, c].slice(0, -1) 至關於 [b, c].slice(0, 1) 的結果就是[b]
return root._children[current]
}, this.root);
parentModule._children[path[path.length-1]] = rawModule;
}
if (rootModule.modules) {
forEach(rootModule.modules, (moduleName, module) => {
// 註冊a, [a] a模塊的定義
// 註冊b, [b] b模塊的定義
// 註冊c, [b, c] c模塊的定義
this.register(path.concat(moduleName), module);
});
}
}
複製代碼
register有兩個參數,第一個是路徑,數組類型,這裏提一下,Vuex中判斷模塊層級關係就是使用數組,這也是一個經典通用的作法,下面會細說。第二個是根模塊也就是進行格式化的起點,這裏起點就是用戶傳入的數據。
往下咱們看到有這一句
rootModule.rawModule = rawModule;// 雙向記錄
複製代碼
這個實際上是爲了動態註冊時用的,以後會說。
再往下就是經典的找爸爸了:
對於根模塊,先定義一個根root,做爲起點,接下來的子模塊會先走forEach,使用path.concat(moduleName)
肯定模塊層級關係,而後進行遞歸註冊,這裏的forEach方法是咱們本身封裝的:
let forEach = (obj, callback) => {
Object.keys(obj).forEach(key => {
callback(key, obj[key]);
})
}
複製代碼
注意slice的用法,slice(0, -1)就是取出除了當前模塊以外的模塊,不清楚slice用法的趕忙去補補吧~
找到當前模塊的父模塊以後就把當前模塊放在父模塊的_children中,這樣一次父子模塊的註冊就算完成了。
好了,咱們回到正題,上面說了最外層的state已經實現了響應式,那麼modules內部的state如何實現響應式處理?
這就又涉及到了Vuex的另外一個核心方法installModules方法了:
function installModule(store, rootState, path, rawModule) {// 安裝時用的rawModule是格式化後的數據
// 安裝子模塊的狀態
// 根據當前用戶傳入的配置 判斷是否添加前綴
let root = store.modules.root // 獲取到最終整個的格式化結果
let namespace = path.reduce((str, current) => {
root= root._children[current];// a
str = str + (root._raw.namespaced ? current + '/' : '');
return str;
}, '');
// console.log(path, namespace);
if(path.length > 0) {// 代表是子模塊
// 若是是c,就先找到b
// [b,c,e] => [b, c] => c
let parentState = path.slice(0, -1).reduce((rootState, current) => {
return rootState[current];
}, rootState);
// vue的響應式不能對不存在的屬性進行響應化
Vue.set(parentState, path[path.length-1], rawModule.state);
}
// 安裝getters
let getters = rawModule._raw.getters;
if (getters) {
forEach(getters, (getterName, value) => {
Object.defineProperty(store.getters, namespace + getterName, {
get: () => {
// return value(rawModule.state);// rawModule就是當前的模塊
return value(getState(store, path));
}
});
});
}
// 安裝mutation
let mutations = rawModule._raw.mutations;
if (mutations) {
forEach(mutations, (mutationName, value) => {
let arr = store.mutations[namespace + mutationName] || (store. mutations[namespace + mutationName] = []);
arr.push((payload) => {
// value(rawModule.state, payload);// 真正執行mutation的地方
value(getState(store, path), payload);
store.subs.forEach(fn => fn({type: namespace + mutationName, payload: payload}, store.state)); // 這就用到了切片
});
});
}
// 安裝action
let actions = rawModule._raw.actions;
if (actions) {
forEach(actions, (actionName, value) => {
let arr = store.actions[namespace + actionName] || (store.actions[namespace + actionName] = []);
arr.push((payload) => {
value(store, payload);
});
});
}
// 處理子模塊
forEach(rawModule._children, (moduleName, rawModule) => {
installModule(store, rootState, path.concat(moduleName), rawModule)
});
}
複製代碼
installModules方法有四個參數:
咱們如今只關注這一段代碼:
if(path.length > 0) {// 代表是子模塊
// 若是是c,就先找到b
// [b,c,e] => [b, c] => c
let parentState = path.slice(0, -1).reduce((rootState, current) => {
return rootState[current];
}, rootState);
// vue的響應式不能對不存在的屬性進行響應化
Vue.set(parentState, path[path.length-1], rawModule.state);
}
複製代碼
看到了吧,仍是找爸爸,以前是找父模塊,此次是找父模塊的狀態。而後使用Vue.set()方法,對modules下的模塊進行響應化處理。以後依舊是遞歸
// 處理子模塊
forEach(rawModule._children, (moduleName, rawModule) => {
installModule(store, rootState, path.concat(moduleName), rawModule)
});
複製代碼
基於以上, 咱們直接看installModules這一段代碼:
// 安裝getters
let getters = rawModule._raw.getters;
if (getters) {
forEach(getters, (getterName, value) => {
Object.defineProperty(store.getters, namespace + getterName, {
get: () => {
// return value(rawModule.state);// rawModule就是當前的模塊
return value(getState(store, path));
}
});
});
}
複製代碼
這裏面涉及了兩個新東西:
先說namespace,看下面這段代碼
// 根據當前用戶傳入的配置 判斷是否添加前綴
let root = store.modules.root // 獲取到最終整個的格式化結果
let namespace = path.reduce((str, current) => {
root= root._children[current];// a
str = str + (root._raw.namespaced ? current + '/' : '');
return str;
}, '');
複製代碼
仍是reduce,加上了命名空間以後原來的方法xxx就變成了a/b/xxx。
getState方法:
// 遞歸的獲取每個模塊的最新狀態
function getState(store, path) {
let local = path.reduce((newState, current) => {
return newState[current];
}, store.state);
return local;
}
複製代碼
仍是reduce,getState主要是結合Vuex實現數據持久化使用,下面咱們會介紹,這裏先跳過。
ok, 大體瞭解了這兩個東西以後,咱們再看getters, 能夠看出最終全部的getter都使用Object.defineProperty定義在了store.getters這個對象中,注意這裏至關於把原本的樹形結構給鋪平了
, 這也就是當咱們不適用namespace時,必定要保證不能重名的緣由。
// 安裝mutation
let mutations = rawModule._raw.mutations;
if (mutations) {
forEach(mutations, (mutationName, value) => {
let arr = store.mutations[namespace + mutationName] || (store. mutations[namespace + mutationName] = []);
arr.push((payload) => {
value(getState(store, path), payload);
});
});
}
複製代碼
知道了getters的原理mutations的原理就也知道了,這裏就是訂閱,對應的commit就是發佈。
// 安裝action
let actions = rawModule._raw.actions;
if (actions) {
forEach(actions, (actionName, value) => {
let arr = store.actions[namespace + actionName] || (store.actions[namespace + actionName] = []);
arr.push((payload) => {
value(store, payload);
});
});
}
複製代碼
同理,依舊是訂閱和發佈。
動態註冊是Vuex提供的一個類方法registerModule
// 動態註冊
registerModule(moduleName, module) {
if (!Array.isArray(moduleName)) {
moduleName = [moduleName];
}
this.modules.register(moduleName, module); // 這裏只作了格式化
installModule(this, this.state, moduleName, module.rawModule);// 從當前模塊開始安裝
}
複製代碼
它有兩個參數,第一個是要註冊的模塊名稱,這個參數對應的register方法中的path,應該是一個數組類型。第二個是對應的選項。內部仍是先格式化而後安裝的流程,安裝時就用到了上面提到的雙向綁定
rootModule.rawModule = rawModule;
複製代碼
對應的
rawModule._raw = rootModule;
複製代碼
rootModule是格式化以前的模塊,rawModule是格式化以後的模塊。rootModule
注意在安裝的時候是從當前的模塊開始的,並非從根模塊開始。
使用方式:
註冊一個單一模塊
store.registerModule('d', {
state: {
age: 'd100'
}
});
複製代碼
註冊一個有層級的模塊
store.registerModule(['b','c','e'], {
state: {
age: 'e100'
}
});
複製代碼
多模塊下,若是不使用命名空間爲何不可以重名?
這個上面說getters實現原理時就提到過,由於安裝時,不管是getters仍是mutations、actions, 要麼是在一個對象中鋪平,要麼是在一個數組中鋪平,若是重名且不使用命名空間勢必會衝突。
用戶選項中除了state、getters、mutations、actions等以外還有一個plugins選項,爲每個mutation暴露一個鉤子,結合Vuex提供的subscribe方法就可以監聽到每一次的mutation信息。
插件其實就是函數,當new Vuex.Store() 時就會去執行一次
this.subs = [];
let plugins = options.plugins;
plugins.forEach(plugin => plugin(this));
複製代碼
內部其實仍是發佈和訂閱的應用,這裏咱們實現兩個經常使用插件
在此以前咱們先看下Vuex提供的subscribe方法, 這是一個類方法
subscribe(fn) {
this.subs.push(fn);
}
複製代碼
能夠看到就是訂閱,既然是用來監聽mutation變化,那發佈的位置必然是和mutation相關的,接下來咱們更改下mutation的安裝
// 安裝mutation
let mutations = rawModule._raw.mutations;
if (mutations) {
forEach(mutations, (mutationName, value) => {
let arr = store.mutations[namespace + mutationName] || (store. mutations[namespace + mutationName] = []);
arr.push((payload) => {
value(getState(store, path), payload);
store.subs.forEach(fn => fn({type: namespace + mutationName, payload: payload}, store.state)); // 這就用到了切片
});
});
}
複製代碼
當執行了mutation以後,再去執行subs裏面的每個方法,這裏就是發佈了。在這裏咱們也看到了另一個編程的亮點:切片
,
arr.push((payload) => {
value(getState(store, path), payload);
store.subs.forEach(fn => fn({type: namespace + mutationName, payload: payload}, store.state)); // 這就用到了切片
});
複製代碼
這裏若是咱們不考慮subscribe,徹底就能夠寫成
arr.push(value);
複製代碼
由於value就是一個mutation,是一個方法,直接存起來就行了,可是這樣一來就無法作其餘事情了,而使用切片就很方便咱們擴展,這就是切片編程的魅力所在。
下面咱們就看看logger插件的實現
function logger(store) {
let prevState = JSON.stringify(store.state);// 默認狀態, 須要用JSON.stringify作一層深拷貝,不然preState是引用類型,那麼當store.state變化,preState立馬就跟着變化,這樣就沒法打印出上一次的狀態
// let prevState = store.state;// 默認狀態
// 訂閱
store.subscribe((mutation, newState) => {// 每次調用mutation 此方法就會執行
console.log(prevState);
console.log(mutation);
console.log(JSON.stringify(newState));
prevState = JSON.stringify(newState);// 保存最新狀態
});
}
複製代碼
數據持久化
function persists(store) {
let local = localStorage.getItem('VUEX:state');
if (local) {
store.replaceState(JSON.parse(local));// 會用local替換全部狀態
}
store.subscribe((mutation, state) => {
// 屢次更改只記錄一次, 須要作一個防抖
debounce(() => {
localStorage.setItem('VUEX:state', JSON.stringify(state));
}, 500)();
});
}
複製代碼
原理就是使用了瀏覽器自帶的一個api localStorage,這裏有一個新方法replaceState, 這也是一個類方法
replaceState(newState) {
this.vm.state = newState;
}
複製代碼
該方法用來更新狀態, 這裏須要注意,咱們更改的僅僅是狀態state, 而getters、 mutations、 actions執行時仍舊使用的是舊的狀態,這個是在安裝時決定的,所以咱們還須要讓他們在每次執行的時候可以拿到最新的狀態,因此還記得上面說的getState方法嗎?
這裏作了一個小的優化就是用節流作了一個優化, 以應對接二連三的更改狀態,至於節流就再也不這贅述了。
最後咱們說下插件的使用:
plugins: [
persists,
logger
],
複製代碼
注意logger方法, 第一次狀態改變時,prev state只包含非動態註冊的模塊, next state包含全部模塊,這是由於第一次執行logger方法的時候傳入的store尚未包含動態註冊的模塊。
Vuex提供了mapState、mapGetters、mapMutations、mapActions這幾個輔助方法,方便咱們書寫。咱們把這幾個分紅兩類:
computed: {
...mapState(['age']),// 解構處理
...mapGetters(['myAge'])
/* age() { return this.$store.state.age;// 和mapState效果同樣 } */
},
複製代碼
methods: {
// ...mapMutations(['syncChange']),
...mapMutations({aaa: 'syncChange'}),// 使用別名
...mapActions({bbb: 'asyncChange'})
}
複製代碼
mapState
export function mapState (stateArr) {// {age: fn}
let obj = {};
stateArr.forEach(stateName => {
obj[stateName] = function() {
return this.$store.state[stateName];
}
});
return obj;
}
複製代碼
mapGetters
export function mapGetters(gettersArr) {
let obj = {};
gettersArr.forEach(getterName => {
obj[getterName] = function() {
return this.$store.getters[getterName];
}
});
return obj;
}
複製代碼
mapMutations
export function mapMutations(obj) {
let res = {};
Object.entries(obj).forEach(([key, value]) => {
res[key] = function(...args) {
this.$store.commit(value, ...args);
}
});
return res;
}
複製代碼
mapActions
export function mapActions(obj) {
let res = {};
Object.entries(obj).forEach(([key, value]) => {
res[key] = function(...args) {
this.$store.dispatch(value, ...args);
}
});
return res;
}
複製代碼
咱們這裏分別實現了傳入數組和對象類型的參數,源碼中使用normalizeMap方法兼容了兩者。最終返回了一個對象,所以咱們使用時須要進行解構。
let Vue;
let forEach = (obj, callback) => {
Object.keys(obj).forEach(key => {
callback(key, obj[key]);
})
}
class ModuleCollection {
constructor(options) {
// 深度遍歷,將全部的子模塊都遍歷一遍
this.register([], options);
}
register(path, rootModule) {
let rawModule = {
_raw: rootModule,// 用戶傳入的模塊定義
_children: {},
state: rootModule.state
}
rootModule.rawModule = rawModule;// 雙向記錄
if (!this.root) {
this.root = rawModule;
} else {
// 經典方法 找到當前模塊的父親
let parentModule = path.slice(0, -1).reduce((root, current) => { // 註冊c的時候 [b, c].slice(0, -1) 至關於 [b, c].slice(0, 1) 的結果就是[b]
return root._children[current]
}, this.root);
parentModule._children[path[path.length-1]] = rawModule;
}
if (rootModule.modules) {
forEach(rootModule.modules, (moduleName, module) => {
// 註冊a, [a] a模塊的定義
// 註冊b, [b] b模塊的定義
// 註冊c, [b, c] c模塊的定義
this.register(path.concat(moduleName), module);
});
}
}
}
// 遞歸的獲取每個模塊的最新狀態
function getState(store, path) {
let local = path.reduce((newState, current) => {
return newState[current];
}, store.state);
return local;
}
function installModule(store, rootState, path, rawModule) {// 安裝時用的rawModule是格式化後的數據
// 安裝子模塊的狀態
// 根據當前用戶傳入的配置 判斷是否添加前綴
let root = store.modules.root // 獲取到最終整個的格式化結果
let namespace = path.reduce((str, current) => {
root= root._children[current];// a
str = str + (root._raw.namespaced ? current + '/' : '');
return str;
}, '');
// console.log(path, namespace);
if(path.length > 0) {// 代表是子模塊
// 若是是c,就先找到b
// [b,c,e] => [b, c] => c
let parentState = path.slice(0, -1).reduce((rootState, current) => {
return rootState[current];
}, rootState);
// vue的響應式不能對不存在的屬性進行響應化
Vue.set(parentState, path[path.length-1], rawModule.state);
}
// 安裝getters
let getters = rawModule._raw.getters;
if (getters) {
forEach(getters, (getterName, value) => {
Object.defineProperty(store.getters, namespace + getterName, {
get: () => {
// return value(rawModule.state);// rawModule就是當前的模塊
return value(getState(store, path));
}
});
});
}
// 安裝mutation
let mutations = rawModule._raw.mutations;
if (mutations) {
forEach(mutations, (mutationName, value) => {
let arr = store.mutations[namespace + mutationName] || (store. mutations[namespace + mutationName] = []);
arr.push((payload) => {
// value(rawModule.state, payload);// 真正執行mutation的地方
value(getState(store, path), payload);
store.subs.forEach(fn => fn({type: namespace + mutationName, payload: payload}, store.state)); // 這就用到了切片
});
});
}
// 安裝action
let actions = rawModule._raw.actions;
if (actions) {
forEach(actions, (actionName, value) => {
let arr = store.actions[namespace + actionName] || (store.actions[namespace + actionName] = []);
arr.push((payload) => {
value(store, payload);
});
});
}
// 處理子模塊
forEach(rawModule._children, (moduleName, rawModule) => {
installModule(store, rootState, path.concat(moduleName), rawModule)
});
}
class Store {
constructor(options) {
// console.log(options);
// 獲取用戶傳入的全部屬性
// this.state = options.state;
this.vm = new Vue({
data: {
state: options.state// 響應化處理
}
});
this.getters = {};// store內部使用的getters
this.mutations = {};
this.actions = {};
// 1. 須要將用戶傳入的對象格式化操做
this.modules = new ModuleCollection(options);
// 2. 遞歸安裝模塊 ,從根模塊開始
installModule(this, this.state, [], this.modules.root);
this.subs = [];
let plugins = options.plugins;
plugins.forEach(plugin => plugin(this));
}
subscribe(fn) {
this.subs.push(fn);// 能夠屢次訂閱
}
replaceState(newState) {
this.vm.state = newState;// 更新狀態, 這裏須要注意,咱們更改的僅僅是狀態state, 而getters mutations actions仍舊使用的時舊的狀態,這個是在安裝時決定的,所以咱們還須要讓他們在每次執行的時候可以拿到最新的狀態
}
get state() {// 獲取實例上的state屬性就會執行此方法
return this.vm.state
}
commit = (mutationName, payload) => {// es7寫法, 這個裏面的this永遠指向的就是當前store實例
// this.mutations[mutationName](payload);
this.mutations[mutationName].forEach(mutation => mutation(payload));
}
dispatch = (actionName, payload) => {
// this.actions[actionName](payload);
this.actions[actionName].forEach(action => action(payload));
}
// 動態註冊
registerModule(moduleName, module) {
if (!Array.isArray(moduleName)) {
moduleName = [moduleName];
}
this.modules.register(moduleName, module); // 這裏只作了格式化
installModule(this, this.state, moduleName, module.rawModule);// 從當前模塊開始安裝
}
}
const install = (_Vue) => {
Vue = _Vue;
// 放到原型上不對,由於默認會把全部Vue實例都添加$store屬性
// 咱們想要的是隻從當前的根實例開始,到他全部的子組件都有$store屬性
Vue.mixin({
beforeCreate() {
// console.log('這是mixin中的1', this.$options.name);
// 把根實例的store屬性放到每個組件中
if (this.$options.store) {
this.$store = this.$options.store;
} else {
this.$store = this.$parent && this.$parent.$store;
}
}
});// 抽離公共的邏輯
}
export function mapState (stateArr) {// {age: fn}
let obj = {};
stateArr.forEach(stateName => {
obj[stateName] = function() {
return this.$store.state[stateName];
}
});
return obj;
}
export function mapGetters(gettersArr) {
let obj = {};
gettersArr.forEach(getterName => {
obj[getterName] = function() {
return this.$store.getters[getterName];
}
});
return obj;
}
export function mapMutations(obj) {
let res = {};
Object.entries(obj).forEach(([key, value]) => {
res[key] = function(...args) {
this.$store.commit(value, ...args);
}
});
return res;
}
export function mapActions(obj) {
let res = {};
Object.entries(obj).forEach(([key, value]) => {
res[key] = function(...args) {
this.$store.dispatch(value, ...args);
}
});
return res;
}
export default {
install,
Store
}
複製代碼