學習 vuex 源碼整體架構,打造屬于自己的狀態管理庫

前言

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

其余四篇分別是:

  1. 學習 jQuery 源碼整體架構,打造屬于自己的 js 類庫

  2. 學習underscore源碼整體架構,打造屬于自己的函數式編程類庫

  3. 學習 lodash 源碼整體架構,打造屬于自己的函數式編程類庫

  4. 學習 sentry 源碼整體架構,打造屬于自己的前端異常監控SDK

感興趣的讀者可以點擊閱讀。下一篇可能是學習?axios?源碼。

導讀
文章比較詳細的介紹了vuexvue源碼調試方法和?Vuex?原理。并且詳細介紹了?Vuex.use?安裝和?new Vuex.Store?初始化、Vuex.Store?的全部API(如dispatchcommit等)的實現和輔助函數?mapStatemapGetters、?mapActionsmapMutations?createNamespacedHelpers

chrome 瀏覽器調試 vuex 源碼方法

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

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

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

// 新增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 文件 根據自己需求斷點調試即可。

本文主要就是通過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原理。文章比較長,如果暫時不想關注源碼細節,可以克隆一下本倉庫代碼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也就是Vueif (installedPlugins.indexOf(plugin) > -1) {return this}// additional parametersvar 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 就是 nullplugin.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 = _VueapplyMixin(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?實例對象關系圖:先大致有個印象。?

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 = [],// 嚴格模式默認是falsestrict = 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)
}

給自己 綁定?commit?和?dispatch

為何要這樣綁定 ?
說明調用?commit?和?dispach?的?this?不一定是?store?實例
這是確保這兩個函數里的?this?是?store?實例

// 嚴格模式,默認是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.Store?的?options?參數
未加工過的模塊(用戶自定義的),根模塊

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) {// 接收參數 runtimethis.runtime = runtime// Store some children item// 存儲子模塊this._children = Object.create(null)// Store the origin module object which passed by programmer// 存儲原始未加工的模塊this._rawModule = rawModule// 模塊 stateconst rawState = rawModule.state// Store the origin module's state// 原始Store 可能是函數,也可能是是對象,是假值,則賦值空對象。this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}}
}

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

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) {// 獲取父級的stateconst parentState = getNestedState(rootState, path.slice(0, -1))// 模塊名稱// 比如 cartconst 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?這個賦值主要是給?helpers?中?mapStatemapGettersmapMutationsmapActions四個輔助函數使用的。
生成本地的dispatch、commit、getters和state。
主要作用就是抹平差異化,不需要用戶再傳模塊參數。

遍歷注冊 mutation

module.forEachMutation((mutation, key) => {const namespacedType = namespace + keyregisterMutation(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] = [])// 最后 mutationentry.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 + keyconst handler = action.handler || actionregisterAction(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 polyfillassert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)*/if (!isPromise(res)) {res = Promise.resolve(res)}// devtool 工具觸發 vuex:errorif (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 + keyregisterGetter(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 statelocal.getters, // local gettersstore.state, // root statestore.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實例對象 _vmconst oldVm = store._vm// bind store public getters// 綁定 store.getterstore.getters = {}// reset local getters cache// 重置 本地getters的緩存store._makeLocalGettersCache = Object.create(null)// 注冊時收集的處理后的用戶自定義的 wrappedGettersconst 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 賦值 keysObject.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 = truestore._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) {// 熱加載為 trueif (hot) {// dispatch changes in all subscribed watchers// to force getter re-evaluation for hot reloading.// 設置  oldVm._data.$$state = nullstore._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 }// 取出處理后的用戶定義 mutationconst 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

訂閱?store?的?mutation

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

訂閱?store?的?action

subscribeAction (fn) {const subs = typeof fn === 'function' ? { before: fn } : fnreturn 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...// 設置 resetStoreVMresetStoreVM(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])})// 重置 StoreresetStore(this)
}

hotUpdate

熱替換新的?action?和?mutation

// 熱加載
hotUpdate (newOptions) {// 調用的是 ModuleCollection 的 update 方法,最終調用對應的是每個 Module 的 updatethis._modules.update(newOptions)// 重置 StoreresetStore(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.statelet getters = this.$store.getters// 傳了參數 namespaceif (namespace) {// 用 namespace 從 store 中找一個模塊。const module = getModuleByNamespace(this.$store, 'mapState', namespace)if (!module) {return}state = module.context.stategetters = module.context.getters}return typeof val === 'function'? val.call(this, state, getters): state[val]}// 標記為 vuex 方便在 devtools 顯示// mark vuex getter for devtoolsres[key].vuex = true})return res
})

normalizeNamespace 標準化統一命名空間

function normalizeNamespace (fn) {return (namespace, map) => {// 命名空間沒傳,交換參數,namespace 為空字符串if (typeof namespace !== 'string') {map = namespacenamespace = ''} 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?這個賦值主要是給?helpers?中?mapStatemapGettersmapMutationsmapActions四個輔助函數使用的。

// 在構造函數中 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 normalizeNamespaceval = namespace + valres[key] = function mappedGetter () {if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {return}// 省略代碼:匹配不到 getterreturn this.$store.getters[val]}// mark vuex getter for devtoolsres[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 storelet dispatch = this.$store.dispatchif (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 指向就是 nullmapState: 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等)的實現和輔助函數?mapStatemapGetters、?mapActionsmapMutations?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 生命周期的過程

筆者精選文章

工作一年后,我有些感悟(寫于2017年)

高考七年后、工作三年后的感悟

面試官問:JS的繼承

前端使用puppeteer 爬蟲生成《React.js 小書》PDF并合并

學習 jQuery 源碼整體架構,打造屬于自己的 js 類庫

學習underscore源碼整體架構,打造屬于自己的函數式編程類庫

學習 lodash 源碼整體架構,打造屬于自己的函數式編程類庫

學習 sentry 源碼整體架構,打造屬于自己的前端異常監控SDK

關于

作者:常以若川為名混跡于江湖。前端路上 | PPT愛好者 | 所知甚少,唯善學。
個人博客 https://lxchuan12.cn/posts?使用?vuepress重構了,閱讀體驗可能更好些
https://github.com/lxchuan12/blog,相關源碼和資源都放在這里,求個 star^_^~

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

可能比較有趣的微信公眾號,長按掃碼關注。也可以加微信?lxchuan12,注明來源,拉您進【前端視野交流群】。

左邊是個人微信號? lxchuan12,右邊是公眾號【若川視野】

由于公眾號限制外鏈,點擊閱讀原文,或許閱讀體驗更佳,覺得文章不錯,可以點個在看呀^_^

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/274412.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/274412.shtml
英文地址,請注明出處:http://en.pswp.cn/news/274412.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

VMware workstation 8.0上安裝VMware ESXI5.0

首先&#xff0c;在VMware的官網上注冊&#xff0c;下載VMware ESXI的安裝包vmware&#xff0d;vmvisor&#xff0d;installer&#xff0d;5.0.0&#xff0d;469512.x86_64.iso&#xff0c;它是iso文件&#xff0c;刻盤進行安裝&#xff0c;安裝過程中&#xff0c;會將硬盤全部…

最新ui設計趨勢_10個最新且有希望的UI設計趨勢

最新ui設計趨勢重點 (Top highlight)Recently, I’ve spent some time observing the directions in which UI design is heading. I’ve stumbled across a few very creative, promising and inspiring trends that, in my opinion, will shape the UI design in the nearest…

Lists

動態數組&#xff0c;可以存儲不同數據類型 >>> a [spam, eggs, 100, 1234] >>> a [spam, eggs, 100, 1234] 和string一樣&#xff0c;支持索引&#xff0c;&#xff0c;* >>> a[0] spam >>> a[3] 1234 >>> a[-2] 100 >>&…

學習 axios 源碼整體架構,打造屬于自己的請求庫

前言這是學習源碼整體架構系列第六篇。整體架構這詞語好像有點大&#xff0c;姑且就算是源碼整體結構吧&#xff0c;主要就是學習是代碼整體結構&#xff0c;不深究其他不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。學習源碼整體架構系列文章如下&#xff1a;1.…

404 錯誤頁面_如何設計404錯誤頁面,以使用戶留在您的網站上

404 錯誤頁面重點 (Top highlight)網站設計 (Website Design) There is a thin line between engaging and enraging when it comes to a site’s 404 error page. They are the most neglected of any website page. The main reason being, visitors are not supposed to end…

宏定義學習

【1】宏定義怎么理解&#xff1f; 關于宏定義&#xff0c;把握住本質&#xff1a;僅僅是一種字符替換&#xff0c;而且是在預處理之前就進行。 【2】宏定義可以包括分號嗎&#xff1f; 可以&#xff0c;示例代碼如下&#xff1a; 1 #include<iostream>2 using namespace…

學習 koa 源碼的整體架構,淺析koa洋蔥模型原理和co原理

前言這是學習源碼整體架構系列第七篇。整體架構這詞語好像有點大&#xff0c;姑且就算是源碼整體結構吧&#xff0c;主要就是學習是代碼整體結構&#xff0c;不深究其他不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。學習源碼整體架構系列文章如下&#xff1a;1.…

公網對講機修改對講機程序_更少的對講機,對講機-更多專心,專心

公網對講機修改對講機程序重點 (Top highlight)I often like to put a stick into the bike wheel of the UX industry as it’s strolling along feeling proud of itself. I believe — strongly — that as designers we should primarily be doers not talkers.我經常喜歡在…

spring配置文件-------通配符

<!-- 這里一定要注意是使用spring的mappingLocations屬性進行通配的 --> <property name"mappingLocations"> <list> <value>classpath:/com/model/domain/*.hbm.xml</value> </list> </proper…

若川知乎問答:2年前端經驗,做的項目沒什么技術含量,怎么辦?

知乎問答&#xff1a;做了兩年前端開發&#xff0c;平時就是拿 Vue 寫寫頁面和組件&#xff0c;簡歷的項目經歷應該怎么寫得好看&#xff1f;以下是我的回答&#xff0c;閱讀量5000&#xff0c;所以發布到公眾號申明原創。題主說的2年經驗做的東西沒什么技術含量&#xff0c;應…

ui設計基礎_我不知道的UI設計的9個重要基礎

ui設計基礎重點 (Top highlight)After listening to Craig Federighi’s talk on how to be a better software engineer I was sold on the idea that it is super important for a software engineer to learn the basic principles of software design.聽了克雷格費德里希(C…

Ubuntu下修改file descriptor

要修改Ubuntu下的file descriptor的話&#xff0c;請參照一下步驟。&#xff08;1&#xff09;修改limits.conf  $sudo vi /etc/security/limits.conf  增加一行  *  -  nofile  10000&#xff08;2&#xff09;修改 common-session  $ sudo vi/etc/pam.d/common…

C# 多線程控制 通訊 和切換

一.多線程的概念   Windows是一個多任務的系統&#xff0c;如果你使用的是windows 2000及其以上版本&#xff0c;你可以通過任務管理器查看當前系統運行的程序和進程。什么是進程呢&#xff1f;當一個程序開始運行時&#xff0c;它就是一個進程&#xff0c;進程所指包括運行中…

vue路由匹配實現包容性_包容性設計:面向老年用戶的數字平等

vue路由匹配實現包容性In Covid world, a lot of older users are getting online for the first time or using technology more than they previously had. For some, help may be needed.在Covid世界中&#xff0c;許多年長用戶首次上網或使用的技術比以前更多。 對于某些人…

IPhone開發 用子類搞定不同的設備(iphone和ipad)

用子類搞定不同的設備 因為要判斷我們的程序正運行在哪個設備上&#xff0c;所以&#xff0c;我們的代碼有些混亂了&#xff0c;IF來ELSE去的&#xff0c;記住&#xff0c;將來你花在維護代碼上的時間要比花在寫代碼上的時間多&#xff0c;如果你的項目比較大&#xff0c;且IF語…

見證開戶_見證中的發現

見證開戶Each time we pick up a new video game, we’re faced with the same dilemma: “How do I play this game?” Most games now feature tutorials, which can range from the innocuous — gently introducing each mechanic at a time through natural gameplay — …

使用JXL組件操作Excel和導出文件

使用JXL組件操作Excel和導出文件 原文鏈接&#xff1a;http://tianweili.github.io/blog/2015/01/29/use-jxl-produce-excel/ 前言&#xff1a;這段時間參與的項目要求做幾張Excel報表&#xff0c;由于項目框架使用了jxl組件&#xff0c;所以把jxl組件的詳細用法歸納總結一下。…

facebook有哪些信息_關于Facebook表情表情符號的所有信息

facebook有哪些信息Ever since worldwide lockdown and restriction on travel have been imposed, platforms like #Facebook, #Instagram, #Zoom, #GoogleDuo, & #Whatsapp have become more important than ever to connect with your loved ones (apart from the sourc…

M2總結報告

團隊成員 李嘉良 http://home.cnblogs.com/u/daisuke/ 王熹 http://home.cnblogs.com/u/vvnx/ 王冬 http://home.cnblogs.com/u/darewin/ 王泓洋 http://home.cnblogs.com/u/fiverice/ 劉明 http://home.cnblogs.com/u/liumingbuaa/ 由之望 http://www.cnbl…

react動畫庫_React 2020動畫庫

react動畫庫Animations are important in instances like page transitions, scroll events, entering and exiting components, and events that the user should be alerted to.動畫在諸如頁面過渡&#xff0c;滾動事件&#xff0c;進入和退出組件以及應提醒用戶的事件之類的…