vuex源碼解析

vuex簡介

能看到此文章的人,應該大部分都已經使用過vuex了,想更深一步瞭解vuex的內部實現原理。所以簡介就少介紹一點。官網介紹說Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。數據流的狀態非常清晰,按照 組件dispatch Action -> action內部commit Mutation -> Mutation再 mutate state 的數據,在觸發render函數引起視圖的更新。附上一張官網的流程圖及vuex的官網地址:https://vuex.vuejs.org/zh/
clipboard.png

Questions

在使用vuex的時候,大家有沒有如下幾個疑問,帶着這幾個疑問,再去看源碼,從中找到解答,這樣對vuex的理解可以加深一些。

  1. 官網在嚴格模式下有說明:在嚴格模式下,無論何時發生了狀態變更且不是由 mutation 函數引起的,將會拋出錯誤。vuex是如何檢測狀態改變是由mutation函數引起的?
  2. 通過在根實例中註冊 store 選項,該 store 實例會注入到根組件下的所有子組件中。爲什麼所有子組件都可以取到store?
  3. 爲什麼用到的屬性在state中也必須要提前定義好,vue視圖纔可以響應?
  4. 在調用dispatch和commit時,只需傳入(type, payload),爲什麼action函數和mutation函數能夠在第一個參數中解構出來state、commit等?

帶着這些問題,我們來看看vuex的源碼,從中尋找到答案。

源碼目錄結構

vuex的源碼結構非常簡潔清晰,代碼量也不是很大,大家不要感到恐慌。

clipboard.png

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 中解構出原型上的 dispatchcommit 方法,並進行二次包裝,將 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 構造後的樹結構如下:(以上面的例子生成的樹結構)

clipboard.png

模塊已經創建好之後,接下來要做的就是 installModule。

installModule

首先我們來看一看執行完 constructor 中的 installModule 函數後,這棵樹的結構如何?

clipboard.png

從上圖中可以看出,在執行完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 裏涉及到的內容已經分析完畢。我們在 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 都是響應式的。

clipboard.png

至此 vuex 的核心代碼初始化部分已經分析完畢。源碼裏還包括一些插件的註冊及暴露出來的 API 像 mapState mapGetters mapActions mapMutation等函數就不在這裏介紹了,感興趣的可以自行去源碼裏看看,比較好理解。這裏就不做過多介紹。

總結

vuex的源碼相比於vue的源碼來說還是很好理解的。分析源碼之前建議大家再細讀一遍官方文檔,遇到不太理解的地方記下來,帶着問題去讀源碼,有目的性的研究,可以加深記憶。閱讀的過程中,可以先寫一個小例子,引入 clone 下來的源碼,一步一步分析執行過程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章