vuex簡介
能看到此文章的人,應該大部分都已經使用過vuex了,想更深一步瞭解vuex的內部實現原理。所以簡介就少介紹一點。官網介紹說Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。資料流的狀態非常清晰,按照 元件dispatch Action -> action內部commit Mutation -> Mutation再 mutate state 的資料,在觸發render函式引起檢視的更新。附上一張官網的流程圖及vuex的官網地址:vuex.vuejs.org/zh/
Questions
在使用vuex的時候,大家有沒有如下幾個疑問,帶著這幾個疑問,再去看原始碼,從中找到解答,這樣對vuex的理解可以加深一些。
- 官網在嚴格模式下有說明:在嚴格模式下,無論何時發生了狀態變更且不是由 mutation 函式引起的,將會丟擲錯誤。vuex是如何檢測狀態改變是由mutation函式引起的?
- 通過在根例項中註冊 store 選項,該 store 例項會注入到根元件下的所有子元件中。為什麼所有子元件都可以取到store?
- 為什麼用到的屬性在state中也必須要提前定義好,vue檢視才可以響應?
- 在呼叫dispatch和commit時,只需傳入(type, payload),為什麼action函式和mutation函式能夠在第一個引數中解構出來state、commit等? 帶著這些問題,我們來看看vuex的原始碼,從中尋找到答案。
原始碼目錄結構
vuex的原始碼結構非常簡潔清晰,程式碼量也不是很大,大家不要感到恐慌。
vuex掛載
vue使用外掛的方法很簡單,只需Vue.use(Plugins),對於vuex,只需要Vue.use(Vuex)即可。在use 的內部是如何實現外掛的註冊呢?讀過vue原始碼的都知道,如果傳入的引數有 install 方法,則呼叫外掛的 install 方法,如果傳入的引數本身是一個function,則直接執行。那麼我們接下來就需要去 vuex 暴露出來的 install 方法去看看具體幹了什麼。
store.js
export function install(_Vue) {
// vue.use原理:呼叫外掛的install方法進行外掛註冊,並向install方法傳遞Vue物件作為第一個引數
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== "production") {
console.error(
"[vuex] already installed. Vue.use(Vuex) should be called only once."
);
}
return;
}
Vue = _Vue; // 為了引用vue的watch方法
applyMixin(Vue);
}
複製程式碼
在 install 中,將 vue 物件賦給了全域性變數 Vue,並作為引數傳給了 applyMixin 方法。那麼在 applyMixin 方法中幹了什麼呢?
mixin.js
function vuexInit() {
const options = this.$options;
// store injection
if (options.store) {
this.$store =
typeof options.store === "function" ? options.store() : options.store;
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store;
}
}
複製程式碼
在這裡首先檢查了一下 vue 的版本,2以上的版本把 vuexInit 函式混入 vuex 的 beforeCreate 鉤子函式中。
在 vuexInit 中,將 new Vue()
時傳入的 store 設定到 this 物件的 $store
屬性上,子元件則從其父元件上引用其 $store
屬性進行層層巢狀設定,保證每一個元件中都可以通過 this.$store 取到 store 物件。
這也就解答了我們問題 2 中的問題。通過在根例項中註冊 store 選項,該 store 例項會注入到根元件下的所有子元件中,注入方法是子從父拿,root從options拿。
接下來讓我們看看new Vuex.Store()
都幹了什麼。
store建構函式
store物件構建的主要程式碼都在store.js中,是vuex的核心程式碼。
首先,在 constructor 中進行了 Vue 的判斷,如果沒有通過 Vue.use(Vuex) 進行 Vuex 的註冊,則呼叫 install 函式註冊。( 通過 script 標籤引入時不需要手動呼叫 Vue.use(Vuex) ) 並在非生產環境進行判斷: 必須呼叫 Vue.use(Vuex) 進行註冊,必須支援 Promise,必須用 new 建立 store。
if (!Vue && typeof window !== "undefined" && window.Vue) {
install(window.Vue);
}
if (process.env.NODE_ENV !== "production") {
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`);
assert(
typeof Promise !== "undefined",
`vuex requires a Promise polyfill in this browser.`
);
assert(
this instanceof Store,
`store must be called with the new operator.`
);
}
複製程式碼
然後進行一系列的屬性初始化。其中的重點是 new ModuleCollection(options)
,這個我們放在後面再講。先把 constructor 中的程式碼過完。
const { plugins = [], strict = false } = options;
// store internal state
this._committing = false; // 是否在進行提交mutation狀態標識
this._actions = Object.create(null); // 儲存action,_actions裡的函式已經是經過包裝後的
this._actionSubscribers = []; // action訂閱函式集合
this._mutations = Object.create(null); // 儲存mutations,_mutations裡的函式已經是經過包裝後的
this._wrappedGetters = Object.create(null); // 封裝後的getters集合物件
// Vuex支援store分模組傳入,在內部用Module建構函式將傳入的options構造成一個Module物件,
// 如果沒有命名模組,預設繫結在this._modules.root上
// ModuleCollection 內部呼叫 new Module建構函式
this._modules = new ModuleCollection(options);
this._modulesNamespaceMap = Object.create(null); // 模組名稱空間map
this._subscribers = []; // mutation訂閱函式集合
this._watcherVM = new Vue(); // Vue元件用於watch監視變化
複製程式碼
屬性初始化完畢後,首先從 this 中解構出原型上的 dispatch
和 commit
方法,並進行二次包裝,將 this 指向當前 store。
const store = this;
const { dispatch, commit } = this;
/**
把 Store 類的 dispatch 和 commit 的方法的 this 指標指向當前 store 的例項上.
這樣做的目的可以保證當我們在元件中通過 this.$store 直接呼叫 dispatch/commit 方法時,
能夠使 dispatch/commit 方法中的 this 指向當前的 store 物件而不是當前元件的 this.
*/
this.dispatch = function boundDispatch(type, payload) {
return dispatch.call(store, type, payload);
};
this.commit = function boundCommit(type, payload, options) {
return commit.call(store, type, payload, options);
};
複製程式碼
接著往下走,包括嚴格模式的設定、根state的賦值、模組的註冊、state的響應式、外掛的註冊等等,其中的重點在 installModule
函式中,在這裡實現了所有modules的註冊。
//options中傳入的是否啟用嚴格模式
this.strict = strict;
// new ModuleCollection 構造出來的_mudules
const state = this._modules.root.state;
// 初始化元件樹根元件、註冊所有子元件,並將其中所有的getters儲存到this._wrappedGetters屬性中
installModule(this, state, [], this._modules.root);
//通過使用vue例項,初始化 store._vm,使state變成可響應的,並且將getters變成計算屬性
resetStoreVM(this, state);
// 註冊外掛
plugins.forEach(plugin => plugin(this));
// 除錯工具註冊
const useDevtools =
options.devtools !== undefined ? options.devtools : Vue.config.devtools;
if (useDevtools) {
devtoolPlugin(this);
}
複製程式碼
到此為止,constructor 中所有的程式碼已經分析完畢。其中的重點在 new ModuleCollection(options)
和 installModule
,那麼接下來我們到它們的內部去看看,究竟都幹了些什麼。
ModuleCollection
由於 Vuex 使用單一狀態樹,應用的所有狀態會集中到一個比較大的物件。當應用變得非常複雜時,store 物件就有可能變得相當臃腫。Vuex 允許我們將 store 分割成模組(module),每個模組擁有自己的 state、mutation、action、getter、甚至是巢狀子模組。例如下面這樣:
const childModule = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
state,
getters,
actions,
mutations,
modules: {
childModule: childModule,
}
})
複製程式碼
有了模組的概念,可以更好的規劃我們的程式碼。對於各個模組公用的資料,我們可以定義一個common store,別的模組用到的話直接通過 modules 的方法引入即可,無需重複的在每一個模組都寫一遍相同的程式碼。這樣我們就可以通過 store.state.childModule 拿到childModule中的 state 狀態, 對於Module的內部是如何實現的呢?
export default class ModuleCollection {
constructor(rawRootModule) {
// 註冊根module,引數是new Vuex.Store時傳入的options
this.register([], rawRootModule, false);
}
register(path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== "production") {
assertRawModule(path, rawModule);
}
const newModule = new Module(rawModule, runtime);
if (path.length === 0) {
// 註冊根module
this.root = newModule;
} else {
// 註冊子module,將子module新增到父module的_children屬性上
const parent = this.get(path.slice(0, -1));
parent.addChild(path[path.length - 1], newModule);
}
// 如果當前模組有子modules,迴圈註冊
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime);
});
}
}
}
複製程式碼
在ModuleCollection中又呼叫了Module建構函式,構造一個Module。
Module建構函式
constructor (rawModule, runtime) {
// 初始化時為false
this.runtime = runtime
// 儲存子模組
this._children = Object.create(null)
// 將原來的module儲存,以備後續使用
this._rawModule = rawModule
const rawState = rawModule.state
// 儲存原來module的state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
複製程式碼
通過以上程式碼可以看出,ModuleCollection 主要將傳入的 options 物件整個構造為一個 Module 物件,並迴圈呼叫 this.register([key], rawModule, false) 為其中的 modules 屬性進行模組註冊,使其都成為 Module 物件,最後 options 物件被構造成一個完整的 Module 樹。
經過 ModuleCollection 構造後的樹結構如下:(以上面的例子生成的樹結構)
模組已經建立好之後,接下來要做的就是 installModule。
installModule
首先我們來看一看執行完 constructor 中的 installModule 函式後,這棵樹的結構如何?
從上圖中可以看出,在執行完installModule函式後,每一個 module 中的 state 屬性都增加了 其子 module 中的 state 屬性,但此時的 state 還不是響應式的,並且新增加了 context 這個物件。裡面包含 dispatch 、 commit 等函式以及 state 、 getters 等屬性。它就是 vuex 官方文件中所說的Action 函式接受一個與 store 例項具有相同方法和屬性的 context 物件
這個 context 物件。我們平時在 store 中呼叫的 dispatch 和 commit 就是從這裡解構出來的。接下來讓我們看看 installModule 裡面執行了什麼。
function installModule(store, rootState, path, module, hot) {
// 判斷是否是根節點,跟節點的path = []
const isRoot = !path.length;
// 取名稱空間,形式類似'childModule/'
const namespace = store._modules.getNamespace(path);
// 如果namespaced為true,存入_modulesNamespaceMap中
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module;
}
// 不是根節點,把子元件的每一個state設定到其父級的state屬性上
if (!isRoot && !hot) {
// 獲取當前元件的父元件state
const parentState = getNestedState(rootState, path.slice(0, -1));
// 獲取當前Module的名字
const moduleName = path[path.length - 1];
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state);
});
}
// 給context物件賦值
const local = (module.context = makeLocalContext(store, namespace, path));
// 迴圈註冊每一個module的Mutation
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key;
registerMutation(store, namespacedType, mutation, local);
});
// 迴圈註冊每一個module的Action
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key;
const handler = action.handler || action;
registerAction(store, type, handler, local);
});
// 迴圈註冊每一個module的Getter
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key;
registerGetter(store, namespacedType, getter, local);
});
// 迴圈_childern屬性
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot);
});
}
複製程式碼
在installModule函式裡,首先判斷是否是根節點、是否設定了名稱空間。在設定了名稱空間的前提下,把 module 存入 store._modulesNamespaceMap 中。在不是跟節點並且不是 hot 的情況下,通過 getNestedState 獲取到父級的 state,並獲取當前 module 的名字, 用 Vue.set() 方法將當前 module 的 state 掛載到父 state 上。然後呼叫 makeLocalContext 函式給 module.context 賦值,設定區域性的 dispatch、commit方法以及getters和state。那麼來看一看這個函式。
function makeLocalContext(store, namespace, path) {
// 是否有名稱空間
const noNamespace = namespace === "";
const local = {
// 如果沒有名稱空間,直接返回store.dispatch;否則給type加上名稱空間,類似'childModule/'這種
dispatch: noNamespace
? store.dispatch
: (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options);
const { payload, options } = args;
let { type } = args;
if (!options || !options.root) {
type = namespace + type;
if (
process.env.NODE_ENV !== "production" &&
!store._actions[type]
) {
console.error(
`[vuex] unknown local action type: ${
args.type
}, global type: ${type}`
);
return;
}
}
return store.dispatch(type, payload);
},
// 如果沒有名稱空間,直接返回store.commit;否則給type加上名稱空間
commit: noNamespace
? store.commit
: (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options);
const { payload, options } = args;
let { type } = args;
if (!options || !options.root) {
type = namespace + type;
if (
process.env.NODE_ENV !== "production" &&
!store._mutations[type]
) {
console.error(
`[vuex] unknown local mutation type: ${
args.type
}, global type: ${type}`
);
return;
}
}
store.commit(type, payload, options);
}
};
// getters and state object must be gotten lazily
// because they will be changed by vm update
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
});
return local;
}
複製程式碼
經過 makeLocalContext 處理的返回值會賦值給 local 變數,這個變數會傳遞給 registerMutation、forEachAction、registerGetter 函式去進行相應的註冊。
mutation可以重複註冊,registerMutation 函式將我們傳入的 mutation 進行了一次包裝,將 state 作為第一個引數傳入,因此我們在呼叫 mutation 的時候可以從第一個引數中取到當前的 state 值。
function registerMutation(store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = []);
entry.push(function wrappedMutationHandler(payload) {
// 將this指向store,將makeLocalContext返回值中的state作為第一個引數,呼叫值執行的payload作為第二個引數
// 因此我們呼叫commit去提交mutation的時候,可以從mutation的第一個引數中取到當前的state值。
handler.call(store, local.state, payload);
});
}
複製程式碼
action也可以重複註冊。註冊 action 的方法與 mutation 相似,registerAction 函式也將我們傳入的 action 進行了一次包裝。但是 action 中引數會變多,裡面包含 dispatch 、commit、local.getters、local.state、rootGetters、rootState,因此可以在一個 action 中 dispatch 另一個 action 或者去 commit 一個 mutation。這裡也就解答了問題4中提出的疑問。
function registerAction(store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = []);
entry.push(function wrappedActionHandler(payload, cb) {
//與mutation不同,action的第一個引數是一個物件,裡面包含dispatch、commit、getters、state、rootGetters、rootState
let res = handler.call(
store,
{
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
},
payload,
cb
);
if (!isPromise(res)) {
res = Promise.resolve(res);
}
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit("vuex:error", err);
throw err;
});
} else {
return res;
}
});
}
複製程式碼
註冊 getters,從getters的第一個引數中可以取到local state、local getters、root state、root getters。getters不允許重複註冊。
function registerGetter(store, type, rawGetter, local) {
// getters不允許重複
if (store._wrappedGetters[type]) {
if (process.env.NODE_ENV !== "production") {
console.error(`[vuex] duplicate getter key: ${type}`);
}
return;
}
store._wrappedGetters[type] = function wrappedGetter(store) {
// getters的第一個引數包含local state、local getters、root state、root getters
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
);
};
}
複製程式碼
現在 store 的 _mutation、_action 中已經有了我們自行定義的的 mutation 和 action函式,並且經過了一層內部報裝。當我們在元件中執行 this.$store.dispatch()
和 this.$store.commit()
的時候,是如何呼叫到相應的函式的呢?接下來讓我們來看一看 store 上的 dispatch 和 commit 函式。
commit
commit 函式先進行引數的適配處理,然後判斷當前 action type 是否存在,如果存在則呼叫 _withCommit 函式執行相應的 mutation 。
// 提交mutation函式
commit(_type, _payload, _options) {
// check object-style commit
//commit支援兩種呼叫方式,一種是直接commit('getName','vuex'),另一種是commit({type:'getName',name:'vuex'}),
//unifyObjectStyle適配兩種方式
const { type, payload, options } = unifyObjectStyle(
_type,
_payload,
_options
);
const mutation = { type, payload };
// 這裡的entry取值就是我們在registerMutation函式中push到_mutations中的函式,已經經過處理
const entry = this._mutations[type];
if (!entry) {
if (process.env.NODE_ENV !== "production") {
console.error(`[vuex] unknown mutation type: ${type}`);
}
return;
}
// 專用修改state方法,其他修改state方法均是非法修改,在嚴格模式下,無論何時發生了狀態變更且不是由 mutation 函式引起的,將會丟擲錯誤
// 不要在釋出環境下啟用嚴格模式!嚴格模式會深度監測狀態樹來檢測不合規的狀態變更——請確保在釋出環境下關閉嚴格模式,以避免效能損失。
this._withCommit(() => {
entry.forEach(function commitIterator(handler) {
handler(payload);
});
});
// 訂閱者函式遍歷執行,傳入當前的mutation物件和當前的state
this._subscribers.forEach(sub => sub(mutation, this.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"
);
}
}
複製程式碼
在 commit 函式中呼叫了 _withCommit 這個函式, 程式碼如下。 _withCommit 是一個代理方法,所有觸發 mutation 的進行 state 修改的操作都經過它,由此來統一管理監控 state 狀態的修改。在嚴格模式下,會深度監聽 state 的變化,如果沒有通過 mutation 去修改 state,則會報錯。官方建議 不要在釋出環境下啟用嚴格模式! 請確保在釋出環境下關閉嚴格模式,以避免效能損失。這裡就解答了問題1中的疑問。
_withCommit(fn) {
// 儲存之前的提交狀態false
const committing = this._committing;
// 進行本次提交,若不設定為true,直接修改state,strict模式下,Vuex將會產生非法修改state的警告
this._committing = true;
// 修改state
fn();
// 修改完成,還原本次修改之前的狀態false
this._committing = committing;
}
複製程式碼
dispatch
dispatch 和 commit 的原理相同。如果有多個同名 action,會等到所有的 action 函式完成後,返回的 Promise 才會執行。
// 觸發action函式
dispatch(_type, _payload) {
// check object-style dispatch
const { type, payload } = unifyObjectStyle(_type, _payload);
const action = { type, payload };
const entry = this._actions[type];
if (!entry) {
if (process.env.NODE_ENV !== "production") {
console.error(`[vuex] unknown action type: ${type}`);
}
return;
}
// 執行所有的訂閱者函式
this._actionSubscribers.forEach(sub => sub(action, this.state));
return entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload);
}
複製程式碼
至此,整個 installModule 裡涉及到的內容已經分析完畢。現在我們來看一看store樹結構。
我們在 options 中傳進來的 action 和 mutation 已經在 store 中。但是 state 和 getters 還沒有。這就是接下來的 resetStoreVM 方法做的事情。
resetStoreVM
resetStoreVM 函式中包括初始化 store._vm,觀測 state 和 getters 的變化以及執行是否開啟嚴格模式等。state 屬性賦值給 vue 例項的 data 屬性,因此資料是可響應的。這也就解答了問題 3,用到的屬性在 state 中也必須要提前定義好,vue 檢視才可以響應。
function resetStoreVM(store, state, hot) {
//儲存老的vm
const oldVm = store._vm;
// 初始化 store 的 getters
store.getters = {};
// _wrappedGetters 是之前在 registerGetter 函式中賦值的
const wrappedGetters = store._wrappedGetters;
const computed = {};
forEachValue(wrappedGetters, (fn, key) => {
// 將getters放入計算屬性中,需要將store傳入
computed[key] = () => fn(store);
// 為了可以通過this.$store.getters.xxx訪問getters
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
});
});
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
// 用一個vue例項來儲存store樹,將getters作為計算屬性傳入,訪問this.$store.getters.xxx實際上訪問的是store._vm[xxx]
const silent = Vue.config.silent;
Vue.config.silent = true;
store._vm = new Vue({
data: {
$$state: state
},
computed
});
Vue.config.silent = silent;
// enable strict mode for new vm
// 如果是嚴格模式,則啟用嚴格模式,深度 watch state 屬性
if (store.strict) {
enableStrictMode(store);
}
// 若存在oldVm,解除對state的引用,等dom更新後把舊的vue例項銷燬
if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null;
});
}
Vue.nextTick(() => oldVm.$destroy());
}
}
複製程式碼
開啟嚴格模式時,會深度監聽 $$state 的變化,如果不是通過this._withCommit()方法觸發的state修改,也就是store._committing如果是false,就會報錯。
function enableStrictMode(store) {
store._vm.$watch(
function() {
return this._data.$$state;
},
() => {
if (process.env.NODE_ENV !== "production") {
assert(
store._committing,
`do not mutate vuex store state outside mutation handlers.`
);
}
},
{ deep: true, sync: true }
);
}
複製程式碼
讓我們來看一看執行完 resetStoreVM 後的 store 結構。現在的 store 中已經有了 getters 屬性,並且 getters 和 state 都是響應式的。
至此 vuex 的核心程式碼初始化部分已經分析完畢。原始碼裡還包括一些外掛的註冊及暴露出來的 API 像 mapState mapGetters mapActions mapMutation等函式就不在這裡介紹了,感興趣的可以自行去原始碼裡看看,比較好理解。這裡就不做過多介紹。
總結
vuex的原始碼相比於vue的原始碼來說還是很好理解的。分析原始碼之前建議大家再細讀一遍官方文件,遇到不太理解的地方記下來,帶著問題去讀原始碼,有目的性的研究,可以加深記憶。閱讀的過程中,可以先寫一個小例子,引入 clone 下來的原始碼,一步一步分析執行過程。