Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態只能經過可預測的方式改變。查看官網html
多組件共享同一個狀態時,會依賴同一狀態 單一數據流沒法知足需求:vue
最終致使代碼維護困難。es6
multiple components that share a common state: 多組件共享狀態 以下圖: vuex
Vue.js 官網文檔寫得很詳細,建議初學者必定要把文檔至少過一遍。數據庫
這裏附上文檔地址 Vuexapi
解析源碼以前,至少要對 Vuex 很是熟悉,再進一步實現。數組
在初始化 store.js 文件時,須要手動註冊 Vuex,這一步是爲了將 store 屬性注入到每一個組件的實例上,可經過 this.$store.state 獲取共享狀態。數據結構
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
複製代碼
Tip: 這裏 Vue 是在進行訂閱,Vuex.install 函數,根組件在實例化時,會自動派發執行 install 方法,並將 Vue 做爲參數傳入。異步
導出 Store 實例,傳入屬性對象,可包含如下屬性:async
export default new Vuex.Store({
state: {
todos: [
{ id: 0, done: true, text: 'Vue.js' },
{ id: 1, done: false, text: 'Vuex' },
{ id: 2, done: false, text: 'Vue-router' },
{ id: 3, done: false, text: 'Node.js' },
],
},
getters: {
doneTodosCount(state) {
return state.todos.filter(todo => todo.done).length;
},
},
mutations: {
syncTodoDone(state, id) {
state.todos.forEach((todo, index) => index===id && (todo.done = true))
}
},
actions: {
asyncChange({commit}, payload) {
setTimeout(() => commit('syncChange', payload), 1000);
}
},
modules: {
a: {
state: {
age: '18'
},
mutations: {
syncAgeIncrement(state) {
state.age += 1; // 這裏的 state 爲當前子模塊狀態
}
}
}
}
})
store.state.a.age // -> "18"
複製代碼
引入相關映射函數
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex';
複製代碼
組件中使用時,這四種映射函數用法差很少,都是返回一個對象。
方式一:
{
computed: {
todos() {
return this.$store.state.todos;
},
age() {
return this.$store.state.a.age
},
// ...
}
}
複製代碼
Tip: 上述方式在獲取多個狀態時,代碼重複過多且麻煩,this.$store.state,可進一步優化爲第二種方式
方式二:
{
computed: mapState({
todos: state => state.age,
// or
todos: 'todos', // 屬性名爲別名
// or
todos(state) {
return state.todos; // 內部可獲取 this 獲取當前實例數據
}
})
}
複製代碼
能夠看到上述的狀態映射,調用時可傳入 options 對象,屬性值有三種形式:
Tip: 若是當前實例組件,有本身的私有的計算屬性時,可以使用 es6 語法的 Object Spread Operator 對象展開運算符
方式三:
{
computed: {
...mapState([
'todos',
]),
otherValue: 'other value'
}
}
複製代碼
在子模塊狀態屬性中添加 namespaced: true 字段時,mapMutations, mapActions 須要添加對應的命名空間 方式一:
{
methods: {
syncTodoDone(payload) {
this.$store.commit('syncTodoDone', payload)
},
syncAgeIncrement() {
this.$store.commit('syncAgeIncrement')
}
}
}
複製代碼
方式二:
{
methods: {
...mapMutations([
"syncTodoDone",
]),
...mapMutations('a', [
"asyncIncrement"
]),
}
}
複製代碼
方式三: 藉助 vuex 內部幫助函數進行包裝
import { createNamespacedHelpers } from 'vuex'
const { mapMutations } = createNamespacedHelpers('a')
{
methods: mapMutations([
'syncAgeIncrement'
])
}
複製代碼
除了以上基礎用法以外,還有 plugins, registerModule 屬性與 api, 後續的源碼分析上會嘗試實現。
接下來開始構建一個簡易的 vuex
└── vuex
└── src
├── index.js
├── store.js # core code including install, Store
└── helpers # helper functions including mapState, mapGetters, mapMutations, mapActions, createNamespacedHelpers
└── util.js
├── plugins
│ └── logger.js
└── module
├── module-collection.js
└── module.js
複製代碼
導出包含核心代碼的對象
// index.js
import { Store, install } from './store';
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers';
export default {
Store,
install,
mapState,
mapGetters,
mapMutations,
mapActions,
createNamespacedHelpers
}
export {
Store,
install,
mapState,
mapGetters,
mapMutations,
mapActions,
createNamespacedHelpers
}
複製代碼
function install(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
if (this.$options.store) {
this.$store = this.$options.store;
} else {
this.$store = this.$parent && this.$parent.$store;
}
},
})
}
複製代碼
Tip: 內部經過調用 Vue.mixin(),爲全部組件注入 $store 屬性
interface StoreOptions<S> {
state?: S | (() => S); getters?: GetterTree<S, S>; actions?: ActionTree<S, S>; mutations?: MutationTree<S>; modules?: ModuleTree<S>; plugins?: Plugin<S>[]; strict?: boolean; } export declare class Store<S> { constructor(options: StoreOptions<S>); readonly state: S; readonly getters: any; replaceState(state: S): void; dispatch: Dispatch; commit: Commit; subscribe<P extends MutationPayload>(fn: (mutation: P, state: S) => any): () => void;
registerModule<T>(path: string, module: Module<T, S>, options?: ModuleOptions): void;
registerModule<T>(path: string[], module: Module<T, S>, options?: ModuleOptions): void;
}
複製代碼
Tip: 以上對源碼上有必定出入,簡化以後有些屬性和方法有所刪減和改動
依次執行步驟:
constructor(options = {}) {
if (!Vue && typeof Window !== undefined && Window.Vue) {
install(Vue);
}
}
複製代碼
this.strict = options.strict || false;
this._committing = false;
this.vm = new Vue({
data: {
state: options.state,
},
});
this.getters = Object.create(null);
this.mutations = Object.create(null);
this.actions = Object.create(null);
this.subs = [];
複製代碼
Tip: this.vm 是核心,實現狀態響應式,一旦發生改變,依賴的組件視圖就會當即更新。
類型爲 GetterTree 調用 Object.create(null) 建立一個乾淨的對象,即原型鏈指向 null,沒有原型對象的方法和屬性,提升性能
export interface GetterTree<S, R> {
[key: string]: Getter<S, R>;
}
複製代碼
export interface MutationTree<S> {
[key: string]: Mutation<S>;
}
複製代碼
類型爲 ActionTree
export interface ActionTree<S, R> {
[key: string]: Action<S, R>;
}
複製代碼
考慮到 state 對象下可能會有多個 modules,建立 ModuleCollection 格式化成想要的數據結構
export interface Module<S, R> {
namespaced?: boolean;
state?: S | (() => S); getters?: GetterTree<S, R>; actions?: ActionTree<S, R>; mutations?: MutationTree<S>; modules?: ModuleTree<R>; } export interface ModuleTree<R> { [key: string]: Module<any, R>; } 複製代碼
core 這裏進行了依賴收集,將用戶傳入的 state 變爲響應式數據,數據變化觸發依賴的頁面更新
get state() {
return this.vm.state;
}
複製代碼
訂閱的事件在每一次 mutation 時發佈
subscribe(fn) {
this.subs.push(fn);
}
複製代碼
replaceState(newState) {
this._withCommit(() => {
this.vm.state = newState;
})
}
複製代碼
訂閱事件的存儲隊列
const plugins= options.plugins;
plugins.forEach(plugin => plugin(this));
複製代碼
結構爲數組,暴露每一次 mutation 的鉤子函數。每個 Vuex 插件僅僅是一個函數,接收 惟一的參數 store,插件功能就是在mutation 調用時增長邏輯,如: - createLogger vuex/dist/logger 修改日誌(內置) - stateSnapShot 生成狀態快照 - persists 數據持久化
const persists = store => {
// mock data from server db
let local = localStorage.getItem('Vuex:state');
if (local) {
store.replaceState(JSON.parse(local));
}
// mock 每一次數據變了以後就往數據庫裏存數據
store.subscribe((mutation, state) => localStorage.setItem('Vuex:state', JSON.stringify(state)))
}
複製代碼
boolean 監聽異步邏輯是否在 dispatch 調用
函數接片,劫持mutation(commit) 觸發函數。
_withCommit(fn) {
const committing = this._committing;
this._committing = true;
fn();
this._committing = committing;
}
複製代碼
源碼中,在嚴格模式下,會深度監聽狀態異步邏輯的調用機制是否符合規範
if (this.strict) {
this.vm.$watch(
() => this.vm.state,
function() {
console.assert(this._committing, '不能異步調用')
},
{
deep: true,
sync: true,
}
);
}
複製代碼
Tip: 生產環境下須要禁用 strict 模式,深度監聽會消耗性能,核心是調用 Vue 的監聽函數
commit = (type, payload) => {
this._withCommit(() => {
this.mutations[type].forEach(fn => fn(payload));
})
}
複製代碼
dispatch = (type, payload) => {
this.actions[type].forEach(fn => fn(payload));
}
複製代碼
registerModule(moduleName, module) {
this._committing = true;
if (!Array.isArray(moduleName)) {
moduleName = [moduleName];
}
this.modules.register(moduleName, module);
installModule(this, this.state, moduleName, module.rawModule)
}
複製代碼
工具方法,註冊格式化後的數據,具體表現爲: (註冊)
/** * * @param {StoreOption} store 狀態實例 * @param {state} rootState 根狀態 * @param {Array<String>} path 父子模塊名構成的數組 * @param {Object} rawModule 當前模塊狀態對應格式化後的數據:{ state, _raw, _children, state } 其中 _raw 是 options: { namespaced?, state, getter, mutations, actions, modules?, plugins, strict} */
function installModule(store, rootState, path, rawModule) {
let { getters, mutations, actions } = rawModule._raw;
let root = store.modules.root;
複製代碼
const namespace = path.reduce((str, currentModuleName) => {
// root._raw 對應的就是 當前模塊的 option, 根模塊沒有 namespaced 屬性跳過
root = root._children[currentModuleName];
return str + (root._raw.namespaced ? currentModuleName + '/' : '')
}, '');
複製代碼
if (path.length > 0) {
let parentState = path.slice(0, -1).reduce((root, current) => root[current], rootState);
// CORE:動態給跟狀態添加新屬性,需調用 Vue.set API,添加依賴
Vue.set(parentState, path[path.length - 1], rawModule.state);
}
複製代碼
if (getters) {
foreach(getters, (type, fn) => Object.defineProperty(store.getters, namespace + type, {
get: () => fn(getState(store, path))
}))
}
複製代碼
if (mutations) {
foreach(mutations, (type, fn) => {
let arr = store.mutations[namespace + type] || (store.mutations[namespace + type] = []);
arr.push(payload => {
fn(getState(store, path), payload);
// 發佈 subscribe 訂閱的回調函數
store.subs.forEach(sub => sub({
type: namespace + type,
payload,
}, store.state));
})
})
}
複製代碼
if (actions) {
foreach(actions, (type, fn) => {
let arr = store.actions[namespace + type] || (store.actions[namespace + type] = []);
arr.push(payload => fn(store, payload));
})
}
複製代碼
foreach(rawModule._children, (moduleName, rawModule) => installModule(store, rootState, path.concat(moduleName), rawModule))
}
複製代碼
class ModuleCollection{
constructor(options) {
this.register([], options);
}
複製代碼
// rootModule: 爲當前模塊下的 StoreOption
register(path, rootModuleOption) {
let rawModule = {
_raw: rootModuleOption,
_children: Object.create(null),
state: rootModuleOption.state
}
rootModuleOption.rawModule = rawModule;
if (!this.root) {
this.root = rawModule;
} else {
// 若 modules: a.modules.b => [a, b] => root._children.a._children.b
let parentModule = path.slice(0, -1).reduce((root, current) => root._children[current], this.root);
parentModule._children[path[path.length - 1]] = rawModule
}
if (rootModuleOption.modules) {
foreach(rootModuleOption.modules, (moduleName, moduleOption) => this.register(path.concat(moduleName), moduleOption));
}
}
}
複製代碼
幫助文件中的四個函數都是經過接受對應要映射爲對象的參數名,直接供組件內部使用。
export const mapState = (options) => {
let obj = Object.create(null);
if (Array.isArray(options)) {
options.forEach((stateName) => {
obj[stateName] = function() {
return this.$store.state[stateName];
};
});
} else {
Object.entries(options).forEach(([stateName, value]) => {
obj[stateName] = function() {
if (typeof value === "string") {
return this.$store.state[stateName];
}
return value(this.$store.state);
}
});
}
return obj;
};
複製代碼
參數 options 類型能夠是: - Array[string], 如:[ 'count', 'list' ] - Object, key 值爲狀態名,value 能夠是 string, arrow function, normal function,其中常規函數,能夠在內部訪問到當前組件實例
export function mapGetters(namespace, options) {
let obj = Object.create(null);
if (Array.isArray(namespace)) {
options = namespace;
namespace = '';
} else {
namespace += '/';
}
options.forEach(getterName => {
console.log(getterName)
obj[getterName] = function() {
return this.$store.getters[namespace + getterName];
}
})
return obj;
}
複製代碼
export function mapMutations(namespace, options) {
let obj = Object.create(null);
if (Array.isArray(namespace)) {
options = namespace;
namespace = '';
} else {
namespace += '/';
}
options.forEach(mutationName => {
obj[mutationName] = function(payload) {
return this.$store.commit(namespace + mutationName, payload)
}
})
return obj;
}
複製代碼
export function mapActions(namespace, options) {
let obj = Object.create(null);
if (Array.isArray(namespace)) {
options = namespace;
namespace = '';
} else {
namespace += '/';
}
options.forEach(actionName => {
obj[actionName] = function(payload) {
return this.$store.dispatch(namespace + actionName, payload)
}
})
return obj;
}
複製代碼
以上後三個方法包含了子模塊命名空間,參數解析以下:
參數1可選值,爲子狀態模塊的命名空間 參數2爲選項屬性,類型同 mapState
處理對象鍵值對迭代函數處理
同步用戶調用 replaceState 後,狀態內部的新狀態
const foreach = (obj, callback) => Object.entries(obj).forEach(([key, value]) => callback(key, value));
const getState = (store, path) => path.reduce((newState, current) => newState[current], store.state);
複製代碼
至此, vuex 源碼的我的分析基本完結,由於是簡版與源碼會有必定的出入。 Feel free to tell me if there is any problem.