學習 vuex 源碼總體架構,打造屬於本身的狀態管理庫

前言

你好,我是若川。這是學習源碼總體架構第五篇。總體架構這詞語好像有點大,姑且就算是源碼總體結構吧,主要就是學習是代碼總體結構,不深究其餘不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。html

學習源碼總體架構系列文章以下:前端

1.學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫
2.學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫
3.學習 lodash 源碼總體架構,打造屬於本身的函數式編程類庫
4.學習 sentry 源碼總體架構,打造屬於本身的前端異常監控SDK
5.學習 vuex 源碼總體架構,打造屬於本身的狀態管理庫
6.學習 axios 源碼總體架構,打造屬於本身的請求庫
7.學習 koa 源碼的總體架構,淺析koa洋蔥模型原理和co原理
8.學習 redux 源碼總體架構,深刻理解 redux 及其中間件原理vue

感興趣的讀者能夠點擊閱讀。下一篇多是學習 axios 源碼。react

導讀
文章比較詳細的介紹了vuexvue源碼調試方法和 Vuex 原理。而且詳細介紹了 Vuex.use 安裝和 new Vuex.Store 初始化、Vuex.Store 的所有API(如dispatchcommit等)的實現和輔助函數 mapStatemapGettersmapActionsmapMutations createNamespacedHelperswebpack

chrome 瀏覽器調試 vuex 源碼方法

Vue文檔:在 VS Code 中調試 Vue 項目
從上文中同理可得調試 vuex 方法,這裏詳細說下,便於幫助到可能不知道如何調試源碼的讀者。
能夠把筆者的這個 vuex-analysis 源碼分析倉庫fork一份或者直接克隆下來, git clone https://github.com/lxchuan12/vuex-analysis.gitios

其中文件夾vuex,是克隆官方的vuex倉庫 dev分支。
截至目前(2019年11月),版本是v3.1.2,最後一次commitba2ff3a32019-11-11 11:51 Ben Hutton
包含筆者的註釋,便於理解。
git

克隆完成後, 在vuex/examples/webpack.config.js 中添加devtool配置。github

// 新增devtool配置,便於調試
devtool: 'source-map', output: {} 複製代碼
git clone https://github.com/lxchuan12/vuex-analysis.git
cd vuex npm i npm run dev 複製代碼

打開 http://localhost:8080/
點擊你想打開的例子,例如:Shopping Cart => http://localhost:8080/shopping-cart/
打開控制面板 source 在左側找到 webapck// . src 目錄 store 文件 根據本身需求斷點調試便可。
web

本文主要就是經過Shopping Cart,(路徑vuex/examples/shopping-cart)例子調試代碼的。面試

順便提一下調試 vue 源碼(v2.6.10)的方法

git clone https://github.com/vuejs/vue.git
複製代碼

克隆下來後將package.json 文件中的script dev命令後面添加這個 --sourcemap

{
 "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap" } 複製代碼
git clone https://github.com/vuejs/vue.git
cd vue npm i # 在 dist/vue.js 最後一行追加一行 //# sourceMappingURL=vue.js.map npm run dev # 新終端窗口 # 根目錄下 全局安裝http-server(一行命令啓動服務的工具) npm i -g http-server hs -p 8100  # 在examples 文件夾中把引用的vuejs的index.html 文件 vue.min.js 改成 vue.js # 或者把dist文件夾的 vue.min.js ,替換成npm run dev編譯後的dist/vue.js   # 瀏覽器打開 open http://localhost:8100/examples/  # 打開控制面板 source 在左側找到 src 目錄 即vue.js源碼文件 根據本身需求斷點調試便可。 複製代碼

本小節大篇幅介紹調試方法。是由於真的很重要。會調試代碼,看源碼就比較簡單了。關注主線調試代碼,很容易看懂。
強烈建議克隆筆者的這個倉庫,本身調試代碼,對着註釋看,不調試代碼,只看文章不容易吸取消化
筆者也看了文章末尾筆者推薦閱讀的文章,但仍是須要本身看源代碼,才知道這些文章哪裏寫到了,哪裏沒有細寫。

正文開始~

vuex 原理

簡單說明下 vuex 原理

<template>
<div>  count {{$store.state.count}} </div> </template> 複製代碼

每一個組件(也就是Vue實例)在beforeCreate的生命週期中都混入(Vue.mixin)同一個Store實例 做爲屬性 $store, 也就是爲啥能夠經過 this.$store.dispatch 等調用方法的緣由。

最後顯示在模板裏的 $store.state.count 源碼是這樣的。

class Store{
 get state () {  return this._vm._data.$state  } } 複製代碼

其實就是: vm.$store._vm._data.?state.count 其中vm.$store._vm._data.?state 是 響應式的。 怎麼實現響應式的?其實就是new Vue()

function resetStoreVM (store, state, hot) {
 // 省略若干代碼  store._vm = new Vue({  data: {  $state: state  },  computed  })  // 省略若干代碼 } 複製代碼

這裏的 state 就是 用戶定義的 state。 這裏的 computed 就是處理後的用戶定義的 getters。 而 class Store上的一些函數(API)主要都是圍繞修改vm.$store._vm._data.?statecomputed(getter)服務的。

Vue.use 安裝

筆者畫了一張圖表示下Vuex對象,是Vue的一個插件。

Vuex 對象關係圖
Vuex 對象關係圖

看到這裏,恭喜你已經瞭解了Vuex原理。文章比較長,若是暫時不想關注源碼細節,能夠克隆一下本倉庫代碼git clone https://github.com/lxchuan12/vuex-analysis.git,後續調試代碼,點贊收藏到時想看了再看。

文檔 Vue.use Vue.use(Vuex)

參數: Object Function plugin 用法:
安裝 Vue.js 插件。若是插件是一個對象,必須提供 install 方法。若是插件是一個函數,它會被做爲 install 方法。install 方法調用時,會將 Vue 做爲參數傳入。
該方法須要在調用 new Vue() 以前被調用。
install 方法被同一個插件屢次調用,插件將只會被安裝一次。

根據斷點調試,來看下Vue.use的源碼。

function initUse (Vue) {
 Vue.use = function (plugin) {  var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));  // 若是已經存在,則直接返回this也就是Vue  if (installedPlugins.indexOf(plugin) > -1) {  return this  }   // additional parameters  var args = toArray(arguments, 1);  // 把 this(也就是Vue)做爲數組的第一項  args.unshift(this);  // 若是插件的install屬性是函數,調用它  if (typeof plugin.install === 'function') {  plugin.install.apply(plugin, args);  } else if (typeof plugin === 'function') {  // 若是插件是函數,則調用它  // apply(null) 嚴格模式下 plugin 插件函數的 this 就是 null  plugin.apply(null, args);  }  // 添加到已安裝的插件  installedPlugins.push(plugin);  return this  }; } 複製代碼

install 函數

vuex/src/store.js

export function install (_Vue) {
 // Vue 已經存在而且相等,說明已經Vuex.use過  if (Vue && _Vue === Vue) {  // 省略代碼:非生產環境報錯,vuex已經安裝  return  }  Vue = _Vue  applyMixin(Vue) } 複製代碼

接下來看 applyMixin 函數

applyMixin 函數

vuex/src/mixin.js

export default function (Vue) {
 // Vue 版本號  const version = Number(Vue.version.split('.')[0])  if (version >= 2) {  // 合併選項後 beforeCreate 是數組裏函數的形式 [ƒ, ƒ]  // 最後調用循環遍歷這個數組,調用這些函數,這是一種函數與函數合併的解決方案。  // 假設是咱們本身來設計,會是什麼方案呢。  Vue.mixin({ beforeCreate: vuexInit })  } else {  // 省略1.x的版本代碼 ...  }   /**  * Vuex init hook, injected into each instances init hooks list.  */  function vuexInit () {  const options = this.$options  // store injection  // store 注入到每個Vue的實例中  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的實例對象,都有一個$store屬性。且是同一個Store實例。
用購物車的例子來舉例就是:

const vm = new Vue({
 el: '#app',  store,  render: h => h(App) }) console.log('vm.$store === vm.$children[0].$store', vm.$store === vm.$children[0].$store) // true console.log('vm.$store === vm.$children[0].$children[0].$store', vm.$store === vm.$children[0].$children[0].$store) // true console.log('vm.$store === vm.$children[0].$children[1].$store', vm.$store === vm.$children[0].$children[1].$store) // true 複製代碼

Vuex.Store 構造函數

先看最終 new Vuex.Store 以後的 Store 實例對象關係圖:先大體有個印象。 new Vuex.Store以後的 Store 實例對象關係圖

export class Store {
 constructor (options = {}) {  // 這個構造函數比較長,這裏省略,後文分開細述  } } 複製代碼
if (!Vue && typeof window !== 'undefined' && window.Vue) {
 install(window.Vue) } 複製代碼

若是是 cdn script 方式引入vuex插件,則自動安裝vuex插件,不須要用Vue.use(Vuex)來安裝。

// asset 函數實現
export function assert (condition, msg) {  if (!condition) throw new Error(`[vuex] ${msg}`) } 複製代碼
if (process.env.NODE_ENV !== 'production') {
 // 可能有讀者會問:爲啥不用 console.assert,console.assert 函數報錯不會阻止後續代碼執行  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.`) } 複製代碼

條件斷言:不知足直接拋出錯誤

1.必須使用 Vue.use(Vuex) 建立 store 實例。
2.當前環境不支持Promise,報錯:vuex 須要 Promise polyfill
3.Store 函數必須使用 new 操做符調用。

const {
 // 插件默認是空數組  plugins = [],  // 嚴格模式默認是false  strict = false } = options 複製代碼

從用戶定義的new Vuex.Store(options) 取出pluginsstrict參數。

// store internal state
// store 實例對象 內部的 state this._committing = false // 用來存放處理後的用戶自定義的actoins this._actions = Object.create(null) // 用來存放 actions 訂閱 this._actionSubscribers = [] // 用來存放處理後的用戶自定義的mutations this._mutations = Object.create(null) // 用來存放處理後的用戶自定義的 getters this._wrappedGetters = Object.create(null) // 模塊收集器,構造模塊樹形結構 this._modules = new ModuleCollection(options) // 用於存儲模塊命名空間的關係 this._modulesNamespaceMap = Object.create(null) // 訂閱 this._subscribers = [] // 用於使用 $watch 觀測 getters this._watcherVM = new Vue() // 用來存放生成的本地 getters 的緩存 this._makeLocalGettersCache = Object.create(null) 複製代碼

聲明Store實例對象一些內部變量。用於存放處理後用戶自定義的actionsmutationsgetters等變量。

提一下 Object.create(null){} 的區別。前者沒有原型鏈,後者有。 即 Object.create(null).__proto__undefined ({}).__proto__Object.prototype

// bind commit and dispatch to self
const store = this const { dispatch, commit } = 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) } 複製代碼

給本身 綁定 commitdispatch

爲什麼要這樣綁定 ?
說明調用 commitdispachthis 不必定是 store 實例
這是確保這兩個函數裏的 thisstore 實例

// 嚴格模式,默認是false
this.strict = strict // 根模塊的state const state = this._modules.root.state // init root module. // this also recursively registers all sub-modules // and collects all module getters inside this._wrappedGetters installModule(this, state, [], this._modules.root) // initialize the store vm, which is responsible for the reactivity // (also registers _wrappedGetters as computed properties) resetStoreVM(this, state) 複製代碼

上述這段代碼 installModule(this, state, [], this._modules.root)

初始化 根模塊。
而且也遞歸的註冊全部子模塊。
而且收集全部模塊的 getters 放在 this._wrappedGetters 裏面。

resetStoreVM(this, state)

初始化 store._vm 響應式的
而且註冊 _wrappedGetters 做爲 computed 的屬性

plugins.forEach(plugin => plugin(this))
複製代碼

插件:把實例對象 store 傳給插件函數,執行全部插件。

const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {  devtoolPlugin(this) } 複製代碼

初始化 vue-devtool 開發工具。
參數 devtools 傳遞了取 devtools 不然取Vue.config.devtools 配置。

初讀這個構造函數的所有源代碼。會發現有三個地方須要重點看。分別是:

this._modules = new ModuleCollection(options)
installModule(this, state, [], this._modules.root) resetStoreVM(this, state) 複製代碼

閱讀時能夠斷點調試,賦值語句this._modules = new ModuleCollection(options),若是暫時不想看,能夠直接看返回結果。installModuleresetStoreVM函數則能夠斷點調試。

class ModuleCollection

收集模塊,構造模塊樹結構。

註冊根模塊 參數 rawRootModule 也就是 Vuex.Storeoptions 參數
未加工過的模塊(用戶自定義的),根模塊

export default class ModuleCollection {
 constructor (rawRootModule) {  // register root module (Vuex.Store options)  this.register([], rawRootModule, false)  } } 複製代碼
/**  * 註冊模塊  * @param {Array} path 路徑  * @param {Object} rawModule 原始未加工的模塊  * @param {Boolean} runtime runtime 默認是 true  */ register (path, rawModule, runtime = true) {  // 非生產環境 斷言判斷用戶自定義的模塊是否符合要求  if (process.env.NODE_ENV !== 'production') {  assertRawModule(path, rawModule)  }   const newModule = new Module(rawModule, runtime)  if (path.length === 0) {  this.root = newModule  } else {  const parent = this.get(path.slice(0, -1))  parent.addChild(path[path.length - 1], newModule)  }   // register nested modules  // 遞歸註冊子模塊  if (rawModule.modules) {  forEachValue(rawModule.modules, (rawChildModule, key) => {  this.register(path.concat(key), rawChildModule, runtime)  })  } } 複製代碼

class Module

// Base data struct for store's module, package with some attribute and method
// store 的模塊 基礎數據結構,包括一些屬性和方法 export default class Module {  constructor (rawModule, runtime) {  // 接收參數 runtime  this.runtime = runtime  // Store some children item  // 存儲子模塊  this._children = Object.create(null)  // Store the origin module object which passed by programmer  // 存儲原始未加工的模塊  this._rawModule = rawModule  // 模塊 state  const rawState = rawModule.state   // Store the origin module's state  // 原始Store 多是函數,也多是是對象,是假值,則賦值空對象。  this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}  } } 複製代碼

通過一系列的註冊後,最後 this._modules = new ModuleCollection(options) this._modules 的值是這樣的。 筆者畫了一張圖表示: ModuleCollection

installModule 函數

function installModule (store, rootState, path, module, hot) {
 // 是根模塊  const isRoot = !path.length  // 命名空間 字符串  const namespace = store._modules.getNamespace(path)  if (module.namespaced) {  // 省略代碼: 模塊命名空間map對象中已經有了,開發環境報錯提示重複  // module 賦值給 _modulesNamespaceMap[namespace]  store._modulesNamespaceMap[namespace] = module  }  // ... 後續代碼 移出來 待讀解釋 } 複製代碼

註冊 state

// set state
// 不是根模塊且不是熱重載 if (!isRoot && !hot) {  // 獲取父級的state  const parentState = getNestedState(rootState, path.slice(0, -1))  // 模塊名稱  // 好比 cart  const moduleName = path[path.length - 1]  // state 註冊  store._withCommit(() => {  // 省略代碼:非生產環境 報錯 模塊 state 重複設置  Vue.set(parentState, moduleName, module.state)  }) } 複製代碼

最後獲得的是相似這樣的結構且是響應式的數據 實例 Store.state 好比:

{
 // 省略若干屬性和方法  // 這裏的 state 是隻讀屬性 可搜索 get state 查看,上文寫過  state: {  cart: {  checkoutStatus: null,  items: []  }  } } 複製代碼
const local = module.context = makeLocalContext(store, namespace, path)
複製代碼

module.context 這個賦值主要是給 helpersmapStatemapGettersmapMutationsmapActions四個輔助函數使用的。
生成本地的dispatch、commit、getters和state。
主要做用就是抹平差別化,不須要用戶再傳模塊參數。

遍歷註冊 mutation

module.forEachMutation((mutation, key) => {
 const namespacedType = namespace + key  registerMutation(store, namespacedType, mutation, local) }) 複製代碼
/**  * 註冊 mutation  * @param {Object} store 對象  * @param {String} type 類型  * @param {Function} handler 用戶自定義的函數  * @param {Object} local local 對象  */ function registerMutation (store, type, handler, local) {  // 收集的全部的mutations找對應的mutation函數,沒有就賦值空數組  const entry = store._mutations[type] || (store._mutations[type] = [])  // 最後 mutation  entry.push(function wrappedMutationHandler (payload) {  /**  * mutations: {  * pushProductToCart (state, { id }) {  * console.log(state);  * }  * }  * 也就是爲何用戶定義的 mutation 第一個參數是state的緣由,第二個參數是payload參數  */  handler.call(store, local.state, payload)  }) } 複製代碼

遍歷註冊 action

module.forEachAction((action, key) => {
 const type = action.root ? key : namespace + key  const handler = action.handler || action  registerAction(store, type, handler, local) }) 複製代碼
/** * 註冊 mutation * @param {Object} store 對象 * @param {String} type 類型 * @param {Function} handler 用戶自定義的函數 * @param {Object} local local 對象 */ function registerAction (store, type, handler, local) {  const entry = store._actions[type] || (store._actions[type] = [])  // payload 是actions函數的第二個參數  entry.push(function wrappedActionHandler (payload) {  /**  * 也就是爲何用戶定義的actions中的函數第一個參數有  * { dispatch, commit, getters, state, rootGetters, rootState } 的緣由  * actions: {  * checkout ({ commit, state }, products) {  * console.log(commit, state);  * }  * }  */  let res = handler.call(store, {  dispatch: local.dispatch,  commit: local.commit,  getters: local.getters,  state: local.state,  rootGetters: store.getters,  rootState: store.state  }, payload)  /**  * export function isPromise (val) {  return val && typeof val.then === 'function'  }  * 判斷若是不是Promise Promise 化,也就是爲啥 actions 中處理異步函數  也就是爲何構造函數中斷言不支持promise報錯的緣由  vuex須要Promise polyfill  assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)  */  if (!isPromise(res)) {  res = Promise.resolve(res)  }  // devtool 工具觸發 vuex:error  if (store._devtoolHook) {  // catch 捕獲錯誤  return res.catch(err => {  store._devtoolHook.emit('vuex:error', err)  // 拋出錯誤  throw err  })  } else {  // 而後函數執行結果  return res  }  }) } 複製代碼

遍歷註冊 getter

module.forEachGetter((getter, key) => {
 const namespacedType = namespace + key  registerGetter(store, namespacedType, getter, local) }) 複製代碼
/**  * 註冊 getter  * @param {Object} store Store實例  * @param {String} type 類型  * @param {Object} rawGetter 原始未加工的 getter 也就是用戶定義的 getter 函數  * @examples 好比 cartProducts: (state, getters, rootState, rootGetters) => {}  * @param {Object} local 本地 local 對象  */ function registerGetter (store, type, rawGetter, local) {  // 類型若是已經存在,報錯:已經存在  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 中能獲取到 (state, getters, rootState, rootGetters) 這些值的緣由  * getters = {  * cartProducts: (state, getters, rootState, rootGetters) => {  * console.log(state, getters, rootState, rootGetters);  * }  * }  */  return rawGetter(  local.state, // local state  local.getters, // local getters  store.state, // root state  store.getters // root getters  )  } } 複製代碼

遍歷註冊 子模塊

module.forEachChild((child, key) => {
 installModule(store, rootState, path.concat(key), child, hot) }) 複製代碼

resetStoreVM 函數

resetStoreVM(this, state, hot)

初始化 store._vm 響應式的
而且註冊 _wrappedGetters 做爲 computed 的屬性

function resetStoreVM (store, state, hot) {
  // 存儲一份老的Vue實例對象 _vm  const oldVm = store._vm   // bind store public getters  // 綁定 store.getter  store.getters = {}  // reset local getters cache  // 重置 本地getters的緩存  store._makeLocalGettersCache = Object.create(null)  // 註冊時收集的處理後的用戶自定義的 wrappedGetters  const wrappedGetters = store._wrappedGetters  // 聲明 計算屬性 computed 對象  const computed = {}  // 遍歷 wrappedGetters 賦值到 computed 上  forEachValue(wrappedGetters, (fn, key) => {  // use computed to leverage its lazy-caching mechanism  // direct inline function use will lead to closure preserving oldVm.  // using partial to return function with only arguments preserved in closure environment.  /**  * partial 函數  * 執行函數 返回一個新函數  export function partial (fn, arg) {  return function () {  return fn(arg)  }  }  */  computed[key] = partial(fn, store)  // getter 賦值 keys  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 實例對象存儲 state 樹  // 阻止警告 用戶添加的一些全局mixins   // 聲明變量 silent 存儲用戶設置的靜默模式配置  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,只能使用 mutation 修改 也就是 _withCommit 函數  if (store.strict) {  enableStrictMode(store)  }   // 若是存在老的 _vm 實例  if (oldVm) {  // 熱加載爲 true  if (hot) {  // dispatch changes in all subscribed watchers  // to force getter re-evaluation for hot reloading.  // 設置 oldVm._data.$state = null  store._withCommit(() => {  oldVm._data.$state = null  })  }  // 實例銷燬  Vue.nextTick(() => oldVm.$destroy())  } } 複製代碼

到此,構造函數源代碼看完了,接下來看 Vuex.Store 的 一些 API 實現。

Vuex.Store 實例方法

Vuex API 文檔

commit

提交 mutation

commit (_type, _payload, _options) {
 // check object-style commit  // 統一成對象風格  const {  type,  payload,  options  } = unifyObjectStyle(_type, _payload, _options)   const mutation = { type, payload }  // 取出處理後的用戶定義 mutation  const entry = this._mutations[type]  // 省略 非生產環境的警告代碼 ...  this._withCommit(() => {  // 遍歷執行  entry.forEach(function commitIterator (handler) {  handler(payload)  })  })  // 訂閱 mutation 執行  this._subscribers.forEach(sub => sub(mutation, this.state))   // 省略 非生產環境的警告代碼 ... } 複製代碼

commit 支持多種方式。好比:

store.commit('increment', {
 count: 10 }) // 對象提交方式 store.commit({  type: 'increment',  count: 10 }) 複製代碼

unifyObjectStyle函數將參數統一,返回 { type, payload, options }

dispatch

分發 action

dispatch (_type, _payload) {
 // check object-style dispatch  // 獲取到type和payload參數  const {  type,  payload  } = unifyObjectStyle(_type, _payload)   // 聲明 action 變量 等於 type和payload參數  const action = { type, payload }  // 入口,也就是 _actions 集合  const entry = this._actions[type]  // 省略 非生產環境的警告代碼 ...  try {  this._actionSubscribers  .filter(sub => sub.before)  .forEach(sub => sub.before(action, this.state))  } catch (e) {  if (process.env.NODE_ENV !== 'production') {  console.warn(`[vuex] error in before action subscribers: `)  console.error(e)  }  }   const result = entry.length > 1  ? Promise.all(entry.map(handler => handler(payload)))  : entry[0](payload)   return result.then(res => {  try {  this._actionSubscribers  .filter(sub => sub.after)  .forEach(sub => sub.after(action, this.state))  } catch (e) {  if (process.env.NODE_ENV !== 'production') {  console.warn(`[vuex] error in after action subscribers: `)  console.error(e)  }  }  return res  }) } 複製代碼

replaceState

替換 store 的根狀態,僅用狀態合併或時光旅行調試。

replaceState (state) {
 this._withCommit(() => {  this._vm._data.$state = state  }) } 複製代碼

watch

響應式地偵聽 fn 的返回值,當值改變時調用回調函數。

/**  * 觀測某個值  * @param {Function} getter 函數  * @param {Function} cb 回調  * @param {Object} options 參數對象  */ watch (getter, cb, options) {  if (process.env.NODE_ENV !== 'production') {  assert(typeof getter === 'function', `store.watch only accepts a function.`)  }  return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options) } 複製代碼

subscribe

訂閱 storemutation

subscribe (fn) {
 return genericSubscribe(fn, this._subscribers) } 複製代碼
// 收集訂閱者
function genericSubscribe (fn, subs) {  if (subs.indexOf(fn) < 0) {  subs.push(fn)  }  return () => {  const i = subs.indexOf(fn)  if (i > -1) {  subs.splice(i, 1)  }  } } 複製代碼

subscribeAction

訂閱 storeaction

subscribeAction (fn) {
 const subs = typeof fn === 'function' ? { before: fn } : fn  return genericSubscribe(subs, this._actionSubscribers) } 複製代碼

registerModule

註冊一個動態模塊。

/**  * 動態註冊模塊  * @param {Array|String} path 路徑  * @param {Object} rawModule 原始未加工的模塊  * @param {Object} options 參數選項  */ registerModule (path, rawModule, options = {}) {  // 若是 path 是字符串,轉成數組  if (typeof path === 'string') path = [path]   // 省略 非生產環境 報錯代碼   // 手動調用 模塊註冊的方法  this._modules.register(path, rawModule)  // 安裝模塊  installModule(this, this.state, path, this._modules.get(path), options.preserveState)  // reset store to update getters...  // 設置 resetStoreVM  resetStoreVM(this, this.state) } 複製代碼

unregisterModule

卸載一個動態模塊。

/**  * 註銷模塊  * @param {Array|String} path 路徑  */ unregisterModule (path) {  // 若是 path 是字符串,轉成數組  if (typeof path === 'string') path = [path]   // 省略 非生產環境 報錯代碼 ...   // 手動調用模塊註銷  this._modules.unregister(path)  this._withCommit(() => {  // 註銷這個模塊  const parentState = getNestedState(this.state, path.slice(0, -1))  Vue.delete(parentState, path[path.length - 1])  })  // 重置 Store  resetStore(this) } 複製代碼

hotUpdate

熱替換新的 actionmutation

// 熱加載
hotUpdate (newOptions) {  // 調用的是 ModuleCollection 的 update 方法,最終調用對應的是每一個 Module 的 update  this._modules.update(newOptions)  // 重置 Store  resetStore(this, true) } 複製代碼

組件綁定的輔助函數

文件路徑:vuex/src/helpers.js

mapState

爲組件建立計算屬性以返回 Vuex store 中的狀態。

export const mapState = normalizeNamespace((namespace, states) => {
 const res = {}  // 非生產環境 判斷參數 states 必須是數組或者是對象  if (process.env.NODE_ENV !== 'production' && !isValidMap(states)) {  console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')  }  normalizeMap(states).forEach(({ key, val }) => {  res[key] = function mappedState () {  let state = this.$store.state  let getters = this.$store.getters  // 傳了參數 namespace  if (namespace) {  // 用 namespace 從 store 中找一個模塊。  const module = getModuleByNamespace(this.$store, 'mapState', namespace)  if (!module) {  return  }  state = module.context.state  getters = module.context.getters  }  return typeof val === 'function'  ? val.call(this, state, getters)  : state[val]  }  // 標記爲 vuex 方便在 devtools 顯示  // mark vuex getter for devtools  res[key].vuex = true  })  return res }) 複製代碼

normalizeNamespace 標準化統一命名空間

function normalizeNamespace (fn) {
 return (namespace, map) => {  // 命名空間沒傳,交換參數,namespace 爲空字符串  if (typeof namespace !== 'string') {  map = namespace  namespace = ''  } else if (namespace.charAt(namespace.length - 1) !== '/') {  // 若是是字符串,最後一個字符不是 / 添加 /  // 由於 _modulesNamespaceMap 存儲的是這樣的結構。  /**  * _modulesNamespaceMap:  cart/: {}  products/: {}  }  * */  namespace += '/'  }  return fn(namespace, map)  } } 複製代碼
// 校驗是不是map 是數組或者是對象。
function isValidMap (map) {  return Array.isArray(map) || isObject(map) } 複製代碼
/**  * Normalize the map  * 標準化統一 map,最終返回的是數組  * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]  * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]  * @param {Array|Object} map  * @return {Object}  */ function normalizeMap (map) {  if (!isValidMap(map)) {  return []  }  return Array.isArray(map)  ? map.map(key => ({ key, val: key }))  : Object.keys(map).map(key => ({ key, val: map[key] })) } 複製代碼

module.context 這個賦值主要是給 helpersmapStatemapGettersmapMutationsmapActions四個輔助函數使用的。

// 在構造函數中 installModule 中
const local = module.context = makeLocalContext(store, namespace, path) 複製代碼

這裏就是抹平差別,不用用戶傳遞命名空間,獲取到對應的 commit、dispatch、state、和 getters

getModuleByNamespace

function getModuleByNamespace (store, helper, namespace) {
 // _modulesNamespaceMap 這個變量在 class Store installModule 函數中賦值的  const module = store._modulesNamespaceMap[namespace]  if (process.env.NODE_ENV !== 'production' && !module) {  console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)  }  return module } 複製代碼

看完這些,最後舉個例子: vuex/examples/shopping-cart/components/ShoppingCart.vue

computed: {
 ...mapState({  checkoutStatus: state => state.cart.checkoutStatus  }), } 複製代碼

沒有命名空間的狀況下,最終會轉換成這樣

computed: {
 checkoutStatus: this.$store.state.checkoutStatus } 複製代碼

假設有命名空間'ruochuan',

computed: {
 ...mapState('ruochuan', {  checkoutStatus: state => state.cart.checkoutStatus  }), } 複製代碼

則會轉換成:

computed: {
 checkoutStatus: this.$store._modulesNamespaceMap.['ruochuan/'].context.checkoutStatus } 複製代碼

mapGetters

爲組件建立計算屬性以返回 getter 的返回值。

export const mapGetters = normalizeNamespace((namespace, getters) => {
 const res = {}  // 省略代碼:非生產環境 判斷參數 getters 必須是數組或者是對象  normalizeMap(getters).forEach(({ key, val }) => {  // The namespace has been mutated by normalizeNamespace  val = namespace + val  res[key] = function mappedGetter () {  if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {  return  }  // 省略代碼:匹配不到 getter  return this.$store.getters[val]  }  // mark vuex getter for devtools  res[key].vuex = true  })  return res }) 複製代碼

舉例:

computed: {
 ...mapGetters('cart', {  products: 'cartProducts',  total: 'cartTotalPrice'  }) }, 複製代碼

最終轉換成:

computed: {
 products: this.$store.getters['cart/cartProducts'],  total: this.$store.getters['cart/cartTotalPrice'], } 複製代碼

mapActions

建立組件方法分發 action

export const mapActions = normalizeNamespace((namespace, actions) => {
 const res = {}  // 省略代碼: 非生產環境 判斷參數 actions 必須是數組或者是對象  normalizeMap(actions).forEach(({ key, val }) => {  res[key] = function mappedAction (...args) {  // get dispatch function from store  let dispatch = this.$store.dispatch  if (namespace) {  const module = getModuleByNamespace(this.$store, 'mapActions', namespace)  if (!module) {  return  }  dispatch = module.context.dispatch  }  return typeof val === 'function'  ? val.apply(this, [dispatch].concat(args))  : dispatch.apply(this.$store, [val].concat(args))  }  })  return res }) 複製代碼

mapMutations

建立組件方法提交 mutation。 mapMutations 和 mapActions 相似,只是 dispatch 換成了 commit。

let commit = this.$store.commit
commit = module.context.commit return typeof val === 'function'  ? val.apply(this, [commit].concat(args))  : commit.apply(this.$store, [val].concat(args)) 複製代碼

vuex/src/helpers

mapMutationsmapActions 舉例:

{
 methods: {  ...mapMutations(['inc']),  ...mapMutations('ruochuan', ['dec']),  ...mapActions(['actionA'])  ...mapActions('ruochuan', ['actionB'])  } } 複製代碼

最終轉換成

{
 methods: {  inc(...args){  return this.$store.dispatch.apply(this.$store, ['inc'].concat(args))  },  dec(...args){  return this.$store._modulesNamespaceMap.['ruochuan/'].context.dispatch.apply(this.$store, ['dec'].concat(args))  },  actionA(...args){  return this.$store.commit.apply(this.$store, ['actionA'].concat(args))  }  actionB(...args){  return this.$store._modulesNamespaceMap.['ruochuan/'].context.commit.apply(this.$store, ['actionB'].concat(args))  }  } } 複製代碼

因而可知:這些輔助函數極大地方便了開發者。

createNamespacedHelpers

建立基於命名空間的組件綁定輔助函數。

export const createNamespacedHelpers = (namespace) => ({
 // bind(null) 嚴格模式下,napState等的函數 this 指向就是 null  mapState: mapState.bind(null, namespace),  mapGetters: mapGetters.bind(null, namespace),  mapMutations: mapMutations.bind(null, namespace),  mapActions: mapActions.bind(null, namespace) }) 複製代碼

就是把這些輔助函數放在一個對象中。

插件

插件部分文件路徑是:
vuex/src/plugins/devtool
vuex/src/plugins/logger

文章比較長了,這部分就再也不敘述。具體能夠看筆者的倉庫 vuex-analysis vuex/src/plugins/ 的源碼註釋。

總結

文章比較詳細的介紹了vuexvue源碼調試方法和 Vuex 原理。而且詳細介紹了 Vuex.use 安裝和 new Vuex.Store 初始化、Vuex.Store 的所有API(如dispatchcommit等)的實現和輔助函數 mapStatemapGettersmapActionsmapMutations createNamespacedHelpers

文章註釋,在vuex-analysis源碼倉庫裏基本都有註釋分析,求個star。再次強烈建議要克隆代碼下來。

git clone https://github.com/lxchuan12/vuex-analysis.git
複製代碼

先把 Store 實例打印出來,看具體結構,再結合實例斷點調試,事半功倍。

Vuex 源碼相對很少,打包後一千多行,很是值得學習,也比較容易看完。

若是讀者發現有不妥或可改善之處,再或者哪裏沒寫明白的地方,歡迎評論指出。另外以爲寫得不錯,對您有些許幫助,能夠點贊、評論、轉發分享,也是對筆者的一種支持,萬分感謝。

推薦閱讀

vuex 官方文檔
vuex github 倉庫
美團明裔:Vuex框架原理與源碼分析這篇文章強烈推薦,流程圖畫的很好
知乎黃軼:Vuex 2.0 源碼分析這篇文章也強烈推薦,講述的比較全面
小蟲巨蟹:Vuex 源碼解析(如何閱讀源代碼實踐篇)這篇文章也強烈推薦,主要講如何閱讀源代碼
染陌:Vuex 源碼解析
網易考拉前端團隊:Vuex 源碼分析
yck:Vuex 源碼深度解析
小生方勤:【前端詞典】從源碼解讀 Vuex 注入 Vue 生命週期的過程

筆者另外一個系列

面試官問:JS的繼承
面試官問:JS的this指向
面試官問:可否模擬實現JS的call和apply方法
面試官問:可否模擬實現JS的bind方法
面試官問:可否模擬實現JS的new操做符

關於

做者:常以若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
我的博客-若川,使用vuepress重構了,閱讀體驗可能更好些
掘金專欄,歡迎關注~
segmentfault前端視野專欄,歡迎關注~
知乎前端視野專欄,歡迎關注~
github blog,相關源碼和資源都放在這裏,求個star^_^~

歡迎加微信交流 微信公衆號

可能比較有趣的微信公衆號,長按掃碼關注。也能夠加微信 ruochuan12,註明來源,拉您進【前端視野交流羣】。

若川視野
若川視野

本文使用 mdnice 排版

相關文章
相關標籤/搜索