能看到此文章的人,應該大部分都已經使用過vuex了,想更深一步瞭解vuex的內部實現原理。因此簡介就少介紹一點。官網介紹說Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。數據流的狀態很是清晰,按照 組件dispatch Action -> action內部commit Mutation -> Mutation再 mutate state 的數據,在觸發render函數引發視圖的更新。附上一張官網的流程圖及vuex的官網地址:vuex.vuejs.org/zh/ vue
在使用vuex的時候,你們有沒有以下幾個疑問,帶着這幾個疑問,再去看源碼,從中找到解答,這樣對vuex的理解能夠加深一些。vuex
vuex的源碼結構很是簡潔清晰,代碼量也不是很大,你們不要感到恐慌。bash
vue使用插件的方法很簡單,只需Vue.use(Plugins),對於vuex,只須要Vue.use(Vuex)便可。在use 的內部是如何實現插件的註冊呢?讀過vue源碼的都知道,若是傳入的參數有 install 方法,則調用插件的 install 方法,若是傳入的參數自己是一個function,則直接執行。那麼咱們接下來就須要去 vuex 暴露出來的 install 方法去看看具體幹了什麼。app
store.jsdom
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 方法中幹了什麼呢?ide
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.js中,是vuex的核心代碼。ui
首先,在 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
,那麼接下來咱們到它們的內部去看看,究竟都幹了些什麼。
因爲 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。
首先咱們來看一看執行完 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 函數先進行參數的適配處理,而後判斷當前 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 和 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 函數中包括初始化 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 下來的源碼,一步一步分析執行過程。