<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          學習 vuex 源碼整體架構(gòu),打造屬于自己的狀態(tài)管理庫

          共 30773字,需瀏覽 62分鐘

           ·

          2021-06-14 19:00

          前言

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

          其余四篇分別是:

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

          2. 學習underscore源碼整體架構(gòu),打造屬于自己的函數(shù)式編程類庫

          3. 學習 lodash 源碼整體架構(gòu),打造屬于自己的函數(shù)式編程類庫

          4. 學習 sentry 源碼整體架構(gòu),打造屬于自己的前端異常監(jiān)控SDK

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

          導讀
          文章比較詳細的介紹了vuex、vue源碼調(diào)試方法和 Vuex 原理。并且詳細介紹了 Vuex.use 安裝和 new Vuex.Store 初始化、Vuex.Store 的全部API(如dispatchcommit等)的實現(xiàn)和輔助函數(shù) mapState、mapGetters、 mapActions、mapMutations createNamespacedHelpers。

          chrome 瀏覽器調(diào)試 vuex 源碼方法

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

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

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

          // 新增devtool配置,便于調(diào)試
          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 在左側(cè)找到 webapck// . src 目錄 store 文件 根據(jù)自己需求斷點調(diào)試即可。

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

          順便提一下調(diào)試 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 在左側(cè)找到 src 目錄 即vue.js源碼文件 根據(jù)自己需求斷點調(diào)試即可。

          本小節(jié)大篇幅介紹調(diào)試方法。是因為真的很重要。會調(diào)試代碼,看源碼就比較簡單了。關(guān)注主線調(diào)試代碼,很容易看懂。
          強烈建議克隆筆者的這個倉庫,自己調(diào)試代碼,對著注釋看,不調(diào)試代碼,只看文章不容易吸收消化。
          筆者也看了文章末尾筆者推薦閱讀的文章,但還是需要自己看源代碼,才知道這些文章哪里寫到了,哪里沒有細寫。 

          正文開始~

          vuex 原理

          簡單說明下 vuex 原理

          <template>
          <div>
          count {{$store.state.count}}
          </div>
          </template>

          每個組件(也就是Vue實例)在beforeCreate的生命周期中都混入(Vue.mixin)同一個Store實例 作為屬性 $store, 也就是為啥可以通過 this.$store.dispatch 等調(diào)用方法的原因。

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

          class Store{
          get state () {
          return this._vm._data.$$state
          }
          }

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

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

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

          Vue.use 安裝

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

          看到這里,恭喜你已經(jīng)了解了Vuex原理。文章比較長,如果暫時不想關(guān)注源碼細節(jié),可以克隆一下本倉庫代碼git clone https://github.com/lxchuan12/vuex-analysis.git,后續(xù)調(diào)試代碼,點贊收藏到時想看了再看。


          文檔 Vue.use Vue.use(Vuex)

          參數(shù):{Object | Function} plugin 用法:
          安裝 Vue.js 插件。如果插件是一個對象,必須提供 install 方法。如果插件是一個函數(shù),它會被作為 install 方法。install 方法調(diào)用時,會將 Vue 作為參數(shù)傳入。
          該方法需要在調(diào)用 new Vue() 之前被調(diào)用。
          當 install 方法被同一個插件多次調(diào)用,插件將只會被安裝一次。

          根據(jù)斷點調(diào)試,來看下Vue.use的源碼。

          function initUse (Vue) {
          Vue.use = function (plugin) {
          var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
          // 如果已經(jīng)存在,則直接返回this也就是Vue
          if (installedPlugins.indexOf(plugin) > -1) {
          return this
          }

          // additional parameters
          var args = toArray(arguments, 1);
          // 把 this(也就是Vue)作為數(shù)組的第一項
          args.unshift(this);
          // 如果插件的install屬性是函數(shù),調(diào)用它
          if (typeof plugin.install === 'function') {
          plugin.install.apply(plugin, args);
          } else if (typeof plugin === 'function') {
          // 如果插件是函數(shù),則調(diào)用它
          // apply(null) 嚴格模式下 plugin 插件函數(shù)的 this 就是 null
          plugin.apply(null, args);
          }
          // 添加到已安裝的插件
          installedPlugins.push(plugin);
          return this
          };
          }

          install 函數(shù)

          vuex/src/store.js

          export function install (_Vue) {
          // Vue 已經(jīng)存在并且相等,說明已經(jīng)Vuex.use過
          if (Vue && _Vue === Vue) {
          // 省略代碼:非生產(chǎn)環(huán)境報錯,vuex已經(jīng)安裝
          return
          }
          Vue = _Vue
          applyMixin(Vue)
          }

          接下來看 applyMixin 函數(shù)

          applyMixin 函數(shù)

          vuex/src/mixin.js

          export default function (Vue) {
          // Vue 版本號
          const version = Number(Vue.version.split('.')[0])
          if (version >= 2) {
          // 合并選項后 beforeCreate 是數(shù)組里函數(shù)的形式 [?, ?]
          // 最后調(diào)用循環(huán)遍歷這個數(shù)組,調(diào)用這些函數(shù),這是一種函數(shù)與函數(shù)合并的解決方案。
          // 假設(shè)是我們自己來設(shè)計,會是什么方案呢。
          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 構(gòu)造函數(shù)

          先看最終 new Vuex.Store 之后的 Store 實例對象關(guān)系圖:先大致有個印象。 

          export class Store {
          constructor (options = {}) {
          // 這個構(gòu)造函數(shù)比較長,這里省略,后文分開細述
          }
          }
          if (!Vue && typeof window !== 'undefined' && window.Vue) {
          install(window.Vue)
          }

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

          // asset 函數(shù)實現(xiàn)
          export function assert (condition, msg) {
          if (!condition) throw new Error(`[vuex] ${msg}`)
          }
          if (process.env.NODE_ENV !== 'production') {
          // 可能有讀者會問:為啥不用 console.assert,console.assert 函數(shù)報錯不會阻止后續(xù)代碼執(zhí)行
          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) 創(chuàng)建 store 實例。
          2.當前環(huán)境不支持Promise,報錯:vuex 需要 Promise polyfill。
          3.Store 函數(shù)必須使用 new 操作符調(diào)用。

          const {
          // 插件默認是空數(shù)組
          plugins = [],
          // 嚴格模式默認是false
          strict = false
          } = options

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

          // store internal state
          // store 實例對象 內(nèi)部的 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)
          // 模塊收集器,構(gòu)造模塊樹形結(jié)構(gòu)
          this._modules = new ModuleCollection(options)
          // 用于存儲模塊命名空間的關(guān)系
          this._modulesNamespaceMap = Object.create(null)
          // 訂閱
          this._subscribers = []
          // 用于使用 $watch 觀測 getters
          this._watcherVM = new Vue()
          // 用來存放生成的本地 getters 的緩存
          this._makeLocalGettersCache = Object.create(null)

          聲明Store實例對象一些內(nèi)部變量。用于存放處理后用戶自定義的actions、mutationsgetters等變量。

          提一下 Object.create(null) 和 {} 的區(qū)別。前者沒有原型鏈,后者有。即 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

          為何要這樣綁定 ?
          說明調(diào)用 commit 和 dispach 的 this 不一定是 store 實例
          這是確保這兩個函數(shù)里的 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 傳給插件函數(shù),執(zhí)行所有插件。

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

          初始化 vue-devtool 開發(fā)工具。
          參數(shù) devtools 傳遞了取 devtools 否則取Vue.config.devtools 配置。

          初讀這個構(gòu)造函數(shù)的全部源代碼。會發(fā)現(xiàn)有三個地方需要重點看。分別是:

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

          閱讀時可以斷點調(diào)試,賦值語句this._modules = new ModuleCollection(options),如果暫時不想看,可以直接看返回結(jié)果。installModule,resetStoreVM函數(shù)則可以斷點調(diào)試。

          class ModuleCollection

          收集模塊,構(gòu)造模塊樹結(jié)構(gòu)。

          注冊根模塊 參數(shù) rawRootModule 也就是 Vuex.Store 的 options 參數(shù)
          未加工過的模塊(用戶自定義的),根模塊

          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) {
          // 非生產(chǎn)環(huán)境 斷言判斷用戶自定義的模塊是否符合要求
          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 的模塊 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),包括一些屬性和方法
          export default class Module {
          constructor (rawModule, runtime) {
          // 接收參數(shù) 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 可能是函數(shù),也可能是是對象,是假值,則賦值空對象。
          this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
          }
          }

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

          installModule 函數(shù)

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

          注冊 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(() => {
          // 省略代碼:非生產(chǎn)環(huán)境 報錯 模塊 state 重復設(shè)置
          Vue.set(parentState, moduleName, module.state)
          })
          }

          最后得到的是類似這樣的結(jié)構(gòu)且是響應式的數(shù)據(jù) 實例 Store.state 比如:

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

          module.context 這個賦值主要是給 helpers 中 mapState、mapGetters、mapMutationsmapActions四個輔助函數(shù)使用的。
          生成本地的dispatch、commit、getters和state。
          主要作用就是抹平差異化,不需要用戶再傳模塊參數(shù)。

          遍歷注冊 mutation

          module.forEachMutation((mutation, key) => {
          const namespacedType = namespace + key
          registerMutation(store, namespacedType, mutation, local)
          })
          /**
          * 注冊 mutation
          * @param {Object} store 對象
          * @param {String} type 類型
          * @param {Function} handler 用戶自定義的函數(shù)
          * @param {Object} local local 對象
          */
          function registerMutation (store, type, handler, local) {
          // 收集的所有的mutations找對應的mutation函數(shù),沒有就賦值空數(shù)組
          const entry = store._mutations[type] || (store._mutations[type] = [])
          // 最后 mutation
          entry.push(function wrappedMutationHandler (payload) {
          /**
          * mutations: {
          * pushProductToCart (state, { id }) {
          * console.log(state);
          * }
          * }
          * 也就是為什么用戶定義的 mutation 第一個參數(shù)是state的原因,第二個參數(shù)是payload參數(shù)
          */
          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 用戶自定義的函數(shù)
          * @param {Object} local local 對象
          */
          function registerAction (store, type, handler, local) {
          const entry = store._actions[type] || (store._actions[type] = [])
          // payload 是actions函數(shù)的第二個參數(shù)
          entry.push(function wrappedActionHandler (payload) {
          /**
          * 也就是為什么用戶定義的actions中的函數(shù)第一個參數(shù)有
          * { 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 中處理異步函數(shù)
          也就是為什么構(gòu)造函數(shù)中斷言不支持promise報錯的原因
          vuex需要Promise polyfill
          assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
          */
          if (!isPromise(res)) {
          res = Promise.resolve(res)
          }
          // devtool 工具觸發(fā) vuex:error
          if (store._devtoolHook) {
          // catch 捕獲錯誤
          return res.catch(err => {
          store._devtoolHook.emit('vuex:error', err)
          // 拋出錯誤
          throw err
          })
          } else {
          // 然后函數(shù)執(zhí)行結(jié)果
          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 函數(shù)
          * @examples 比如 cartProducts: (state, getters, rootState, rootGetters) => {}
          * @param {Object} local 本地 local 對象
          */
          function registerGetter (store, type, rawGetter, local) {
          // 類型如果已經(jīng)存在,報錯:已經(jīng)存在
          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 函數(shù)

          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 函數(shù)
          * 執(zhí)行函數(shù) 返回一個新函數(shù)
          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 存儲用戶設(shè)置的靜默模式配置
          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
          // 開啟嚴格模式 執(zhí)行這句
          // 用 $watch 觀測 state,只能使用 mutation 修改 也就是 _withCommit 函數(shù)
          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.
          // 設(shè)置 oldVm._data.$$state = null
          store._withCommit(() => {
          oldVm._data.$$state = null
          })
          }
          // 實例銷毀
          Vue.nextTick(() => oldVm.$destroy())
          }
          }

          到此,構(gòu)造函數(shù)源代碼看完了,接下來看 Vuex.Store 的 一些 API 實現(xiàn)。

          Vuex.Store 實例方法

          Vuex API 文檔

          commit

          提交 mutation

          commit (_type, _payload, _options) {
          // check object-style commit
          // 統(tǒng)一成對象風格
          const {
          type,
          payload,
          options
          } = unifyObjectStyle(_type, _payload, _options)

          const mutation = { type, payload }
          // 取出處理后的用戶定義 mutation
          const entry = this._mutations[type]
          // 省略 非生產(chǎn)環(huán)境的警告代碼 ...
          this._withCommit(() => {
          // 遍歷執(zhí)行
          entry.forEach(function commitIterator (handler) {
          handler(payload)
          })
          })
          // 訂閱 mutation 執(zhí)行
          this._subscribers.forEach(sub => sub(mutation, this.state))

          // 省略 非生產(chǎn)環(huán)境的警告代碼 ...
          }

          commit 支持多種方式。比如:

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

          unifyObjectStyle函數(shù)將參數(shù)統(tǒng)一,返回 { type, payload, options }。

          dispatch

          分發(fā) action

          dispatch (_type, _payload) {
          // check object-style dispatch
          // 獲取到type和payload參數(shù)
          const {
          type,
          payload
          } = unifyObjectStyle(_type, _payload)

          // 聲明 action 變量 等于 type和payload參數(shù)
          const action = { type, payload }
          // 入口,也就是 _actions 集合
          const entry = this._actions[type]
          // 省略 非生產(chǎn)環(huán)境的警告代碼 ...
          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 的根狀態(tài),僅用狀態(tài)合并或時光旅行調(diào)試。

          replaceState (state) {
          this._withCommit(() => {
          this._vm._data.$$state = state
          })
          }

          watch

          響應式地偵聽 fn 的返回值,當值改變時調(diào)用回調(diào)函數(shù)。

          /**
          * 觀測某個值
          * @param {Function} getter 函數(shù)
          * @param {Function} cb 回調(diào)
          * @param {Object} options 參數(shù)對象
          */
          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 } : fn
          return genericSubscribe(subs, this._actionSubscribers)
          }

          registerModule

          注冊一個動態(tài)模塊。

          /**
          * 動態(tài)注冊模塊
          * @param {Array|String} path 路徑
          * @param {Object} rawModule 原始未加工的模塊
          * @param {Object} options 參數(shù)選項
          */
          registerModule (path, rawModule, options = {}) {
          // 如果 path 是字符串,轉(zhuǎn)成數(shù)組
          if (typeof path === 'string') path = [path]

          // 省略 非生產(chǎn)環(huán)境 報錯代碼

          // 手動調(diào)用 模塊注冊的方法
          this._modules.register(path, rawModule)
          // 安裝模塊
          installModule(this, this.state, path, this._modules.get(path), options.preserveState)
          // reset store to update getters...
          // 設(shè)置 resetStoreVM
          resetStoreVM(this, this.state)
          }

          unregisterModule

          卸載一個動態(tài)模塊。

          /**
          * 注銷模塊
          * @param {Array|String} path 路徑
          */
          unregisterModule (path) {
          // 如果 path 是字符串,轉(zhuǎn)成數(shù)組
          if (typeof path === 'string') path = [path]

          // 省略 非生產(chǎn)環(huán)境 報錯代碼 ...

          // 手動調(diào)用模塊注銷
          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

          熱替換新的 action 和 mutation。

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

          組件綁定的輔助函數(shù)

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

          mapState

          為組件創(chuàng)建計算屬性以返回 Vuex store 中的狀態(tài)。

          export const mapState = normalizeNamespace((namespace, states) => {
          const res = {}
          // 非生產(chǎn)環(huán)境 判斷參數(shù) states 必須是數(shù)組或者是對象
          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
          // 傳了參數(shù) 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 標準化統(tǒng)一命名空間

          function normalizeNamespace (fn) {
          return (namespace, map) => {
          // 命名空間沒傳,交換參數(shù),namespace 為空字符串
          if (typeof namespace !== 'string') {
          map = namespace
          namespace = ''
          } else if (namespace.charAt(namespace.length - 1) !== '/') {
          // 如果是字符串,最后一個字符不是 / 添加 /
          // 因為 _modulesNamespaceMap 存儲的是這樣的結(jié)構(gòu)。
          /**
          * _modulesNamespaceMap:
          cart/: {}
          products/: {}
          }
          * */
          namespace += '/'
          }
          return fn(namespace, map)
          }
          }
          // 校驗是否是map 是數(shù)組或者是對象。
          function isValidMap (map) {
          return Array.isArray(map) || isObject(map)
          }
          /**
          * Normalize the map
          * 標準化統(tǒng)一 map,最終返回的是數(shù)組
          * 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 中 mapState、mapGetters、mapMutations、mapActions四個輔助函數(shù)使用的。

          // 在構(gòu)造函數(shù)中 installModule 中
          const local = module.context = makeLocalContext(store, namespace, path)

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

          getModuleByNamespace

          function getModuleByNamespace (store, helper, namespace) {
          // _modulesNamespaceMap 這個變量在 class Store installModule 函數(shù)中賦值的
          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
          }),
          }

          沒有命名空間的情況下,最終會轉(zhuǎn)換成這樣

          computed: {
          checkoutStatus: this.$store.state.checkoutStatus
          }

          假設(shè)有命名空間'ruochuan',

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

          則會轉(zhuǎn)換成:

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

          mapGetters

          為組件創(chuàng)建計算屬性以返回 getter 的返回值。

          export const mapGetters = normalizeNamespace((namespace, getters) => {
          const res = {}
          // 省略代碼:非生產(chǎn)環(huán)境 判斷參數(shù) getters 必須是數(shù)組或者是對象
          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'
          })
          },

          最終轉(zhuǎn)換成:

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

          mapActions

          創(chuàng)建組件方法分發(fā) action。

          export const mapActions = normalizeNamespace((namespace, actions) => {
          const res = {}
          // 省略代碼:非生產(chǎn)環(huán)境 判斷參數(shù) actions 必須是數(shù)組或者是對象
          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

          創(chuàng)建組件方法提交 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

          mapMutations、mapActions 舉例:

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

          最終轉(zhuǎn)換成

          {
          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))
          }
          }
          }

          由此可見:這些輔助函數(shù)極大地方便了開發(fā)者。

          createNamespacedHelpers

          創(chuàng)建基于命名空間的組件綁定輔助函數(shù)。

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

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

          插件

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

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

          總結(jié)

          文章比較詳細的介紹了vuex、vue源碼調(diào)試方法和 Vuex 原理。并且詳細介紹了 Vuex.use 安裝和 new Vuex.Store 初始化、Vuex.Store 的全部API(如dispatch、commit等)的實現(xiàn)和輔助函數(shù) mapStatemapGetters、 mapActions、mapMutations createNamespacedHelpers

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

          git clone https://github.com/lxchuan12/vuex-analysis.git

          先把 Store 實例打印出來,看具體結(jié)構(gòu),再結(jié)合實例斷點調(diào)試,事半功倍。

          Vuex 源碼相對不多,打包后一千多行,非常值得學習,也比較容易看完。

          如果讀者發(fā)現(xiàn)有不妥或可改善之處,再或者哪里沒寫明白的地方,歡迎評論指出。另外覺得寫得不錯,對您有些許幫助,可以點贊、評論、轉(zhuǎn)發(fā)分享,也是對筆者的一種支持,萬分感謝。

          推薦閱讀

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

          筆者精選文章

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

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

          面試官問:JS的繼承

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

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

          學習underscore源碼整體架構(gòu),打造屬于自己的函數(shù)式編程類庫

          學習 lodash 源碼整體架構(gòu),打造屬于自己的函數(shù)式編程類庫

          學習 sentry 源碼整體架構(gòu),打造屬于自己的前端異常監(jiān)控SDK

          關(guān)于

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

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

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

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


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


          ?? 看完兩件事

          如果你覺得這篇內(nèi)容對你挺有益,我想邀請你幫我兩個小忙:

          1. 點個「在看」,讓更多的人也能看到這篇內(nèi)容

          2. 關(guān)注公眾號「全棧大佬的修煉之路」,每周學習一個新技術(shù)。





          瀏覽 53
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色小电影黄色小视频 | 亚洲成人av一区二区三区 | 国产黄色精品 | 色色一级免费电影 | 亚洲视频观看免费 |