vuex源碼簡析

来源:https://www.cnblogs.com/guolao/archive/2020/02/08/12284393.html
-Advertisement-
Play Games

前言 基於 vuex 3.1.2 按如下流程進行分析: Vue.use(Vuex) Vue.use() 會執行插件的 install 方法,並把插件放入緩存數組中。 而 Vuex 的 install 方法很簡單,保證只執行一次,以及使用 applyMixin 初始化。 applyMixin 方法定義 ...


前言


基於 vuex 3.1.2 按如下流程進行分析:

Vue.use(Vuex);

const store = new Vuex.Store({
  actions,
  getters,
  state,
  mutations,
  modules
  // ...
});
            
new Vue({store});

Vue.use(Vuex)


Vue.use() 會執行插件的 install 方法,並把插件放入緩存數組中。

而 Vuex 的 install 方法很簡單,保證只執行一次,以及使用 applyMixin 初始化。

export function install (_Vue) {
  // 保證只執行一次
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  
  // 初始化
  applyMixin(Vue)
}

applyMixin 方法定義在 vuex/src/mixin.js,vuex 還相容了 vue 1 的版本,這裡只關註 vue 2 的處理。

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    // 混入一個 beforeCreate 方法
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // 這裡是 vue 1 的相容處理
  }

  // 這裡的邏輯就是將 options.store 保存在 vue 組件的 this.$store 中,
  // options.store 就是 new Vue({...}) 時傳入的 store,
  // 也就是 new Vuex.Store({...}) 生成的實例。
  function vuexInit () {
    const options = this.$options
    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
    }
  }
}

new Vuex.Store({...})


先看 Store 的構造函數:

constructor (options = {}) {
  const { plugins = [], strict = false } = options

  this._committing = false
  this._actions = Object.create(null)
  this._actionSubscribers = []
  this._mutations = Object.create(null)
  this._wrappedGetters = Object.create(null)
  // 構建 modules
  this._modules = new ModuleCollection(options)
  this._modulesNamespaceMap = Object.create(null)
  this._subscribers = []
  // store.watch
  this._watcherVM = new Vue()
  this._makeLocalGettersCache = Object.create(null)

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

  // strict mode
  this.strict = strict
    // 安裝模塊
  const state = this._modules.root.state
  installModule(this, state, [], this._modules.root)

  // 實現狀態的響應式
  resetStoreVM(this, state)

  // apply plugins
  plugins.forEach(plugin => plugin(this))

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

this._modules

vuex 為了讓結構清晰,允許我們將 store 分割成模塊,每個模塊擁有自己的 statemutationactiongetter,而且模塊自身也可以擁有子模塊。

const moduleA = {...}
const moduleB = {...}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的狀態
store.state.b // -> moduleB 的狀態

而模塊在 vuex 的構造函數中通過new ModuleCollection(options)生成,依然只看構造函數:

// vuex/src/module/module-collection.js
export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

  register (path, rawModule, runtime = true) {
    // 生成一個 module
    const newModule = new Module(rawModule, runtime)
    // new Vuex.Store() 生成的是根模塊
    if (path.length === 0) {
      // 根模塊
      this.root = newModule
    } else {
      // 生成父子關係
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // 註冊嵌套的模塊
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

register 接收 3 個參數,其中 path 表示路徑,即模塊樹的路徑,rawModule 表示傳入的模塊的配置,runtime 表示是否是一個運行時創建的模塊。

register首先 new Module() 生成一個模塊。

// vuex/src/module/module.js
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // 子模塊
    this._children = Object.create(null)
    // module 原始配置
    this._rawModule = rawModule
    const rawState = rawModule.state
    // state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
}

實例化一個 module 後,判斷當前的 path 的長度,如果為 0,就是是一個根模塊,所以把 newModule 賦值給 this.root,而 new Vuex.Store() 生成的就是根模塊。

如果不為 0,就建立父子模塊的關係:

const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)

首先根據路徑獲取父模塊,然後再調用父模塊的 addChild 方法將子模塊加入到 this._children 中,以此建立父子關係。

register 最後會檢測是否有嵌套的模塊,然後進行註冊嵌套的模塊:

if (rawModule.modules) {
  forEachValue(rawModule.modules, (rawChildModule, key) => {
    this.register(path.concat(key), rawChildModule, runtime)
  })
}

遍歷當前模塊定義中的所有 modules,根據 key 作為 path,遞歸調用 register 方法進行註冊。

installModule

註冊完模塊就會開始安裝模塊:

const state = this._modules.root.state
installModule(this, state, [], this._modules.root)

installModule 方法支持 5 個參數: store,state,path(模塊路徑),module(根模塊),hot(是否熱更新)。

預設情況下,模塊內部的 action、mutation 和 getter 是註冊在全局命名空間的,這樣使得多個模塊能夠對同一 mutation 或 action 作出響應。但是如果有同名的 mutation 被提交,會觸發所有同名的 mutation。

因此 vuex 提供了 namespaced: true 讓模塊成為帶命名空間的模塊,當模塊被註冊後,它的所有 action、mutation 和 getter 都會自動根據模塊註冊的路徑調整命名。例如:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      // 進一步嵌套命名空間
      posts: {
        namespaced: true,
        getters: {
          popular () { ... } // -> getters['account/posts/popular']
        }
      }
    }
  }
})

回到 installModule 方法本身:

function installModule (store, rootState, path, module, hot) {
  // 是否為根模塊
  const isRoot = !path.length
  // 獲取命名空間,
  // 比如說 { a: moduleA } => 'a/',
  // root 沒有 namespace
  const namespace = store._modules.getNamespace(path)

  // 將有 namespace 的 module 緩存起來
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  if (!isRoot && !hot) {
    // 給 store.state 添加屬性,
    // 假如有 modules: { a: moduleA },
    // 則 state: { a: moduleA.state }
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }

  // local 上下文,
  // 本質上是重寫 dispatch 和 commit 函數,
  // 舉個例子,modules: { a: moduleA },
  // moduleA 中有名為 increment 的 mutation,
  // 通過 makeLocalContext 函數,會將 increment 變成
  // a/increment,這樣就可以在 moduleA 中找到定義的函數
  const local = module.context = makeLocalContext(store, namespace, path)

  // 下麵這幾個遍歷迴圈函數,
  // 都是在註冊模塊中的 mutation、action 等等,
  // moduleA 中有名為 increment 的 mutation,
  // 那麼會將 namespace + key 拼接起來,
  // 變為 'a/increment'
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    // 會用 this._mutations 將每個 mutation 存儲起來,
    // 因為允許同一 namespacedType 對應多個方法,
    // 所以同一 namespacedType 是用數組存儲的,
    // store._mutations[type] = []
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    // 和上面一樣,用 this._actions 將 action 存儲起來
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    // 會用 this._wrappedGetters 存儲起來
    // getters 有一點不一樣,
    // 這裡保存的是一個返回 getters 的函數,
    // 而且同一 namespacedType 只能定義一個。
    registerGetter(store, namespacedType, getter, local)
  })

  // 遞歸安裝子模塊
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

makeLocalContext

function makeLocalContext (store, namespace, path) {
  // 判斷是否有 namespace
  const noNamespace = namespace === ''

  const local = {
    // 重寫 dispatch
    // 為什麼要重寫,
    // 舉個例子 modules: { a: moduleA }
    // 在 moduleA 的 action 中使用 dispatch 時,
    // 並不會傳入完整的 path,
    // 只有在 vue 實例里調用 store.dispatch 才會傳入完整路徑
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
      }

      return store.dispatch(type, payload)
    },

    // 重寫 commit 方法
    // 同上
    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
      }

      store.commit(type, payload, options)
    }
  }

  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        // local.getters 本質上是通過匹配 namespace,
        // 從 store.getters[type] 中獲取
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      // local.state 本質上是通過解析 path,
      // 從 store.state 中獲取
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

resetStoreVM(this, state)

初始化 store._vm,利用 Vue 將 store.state 進行響應式處理,並且將 getters 當作 Vue 的計算屬性來進行處理:

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 遍歷 store._wrappedGetters 屬性
  forEachValue(wrappedGetters, (fn, key) => {
    // 這裡的 partial 其實就是創建一個閉包環境,
    // 保存 fn, store 兩個變數,並賦值給 computed
    computed[key] = partial(fn, store)
    // 重寫 get 方法,
    // store.getters.key 其實是訪問了 store._vm[key],
    // 也就是去訪問 computed 中的屬性
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // 實例化一個 Vue 實例 store._vm,
  // 用它來保存 state,computed(getters),
  // 也就是利用 Vue 的進行響應式處理
  const silent = Vue.config.silent
  Vue.config.silent = true
  // 訪問 store.state 時,
  // 其實是訪問了 store._vm._data.$$state
  store._vm = new Vue({
    data: {
      $$state: state
    },
    // 這裡其實就是上面的 getters
    computed
  })
  Vue.config.silent = silent

  // 開啟 strict mode,
  // 只能通過 commit 的方式改變 state
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
      // 熱重載
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    // 這裡其實是動態註冊模塊,
    // 將新的模塊內容加入後生成了新的 store._vm,
    // 然後將舊的銷毀掉
    Vue.nextTick(() => oldVm.$destroy())
  }
}

partial

export function partial (fn, arg) {
  return function () {
    return fn(arg)
  }
}

常用 api


commit

commit (_type, _payload, _options) {
  // check object-style commit
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  const entry = this._mutations[type]
  if (!entry) {
    // 沒有 mutation 會報錯並退出
    return
  }
  this._withCommit(() => {
    // 允許同一 type 下,有多個方法,
    // 所以迴圈數組執行 mutation,
    // 實際上執行的就是安裝模塊時註冊的 mutation
    // handler.call(store, local.state, payload)
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })

  // 觸發訂閱了 mutation 的所有函數
  this._subscribers
    .slice()
    .forEach(sub => sub(mutation, this.state))
}

dispatch

dispatch (_type, _payload) {
    // check object-style dispatch
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    // 從 Store._actions 獲取
    const entry = this._actions[type]
    if (!entry) {
            // 找不到會報錯
      return
    }

    // 在 action 執行之前,
    // 觸發監聽了 action 的函數
    try {
      this._actionSubscribers
        .slice() 
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
        console.error(e)
    }

    // action 用 Promise.all 非同步執行,
    // 實際上執行的就是安裝模塊時註冊的 action
    /* 
    handler.call(store, {dispatch, commit, getters, state, rootGetters, rootState}, payload, cb)        
    */
    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    return result.then(res => {
      // 在 action 執行之後,
        // 觸發監聽了 action 的函數
      try {
        this._actionSubscribers
          .filter(sub => sub.after)
          .forEach(sub => sub.after(action, this.state))
      } catch (e) {
          console.error(e)
      }
      return res
    })
  }

watch

watch (getter, cb, options) {
  // new Vuex.Store() 時創建 _watcherVM,
  // this._watcherVM = new Vue(),
  // 本質就是調用 vue 的 api
  return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}

registerModule

registerModule (path, rawModule, options = {}) {
  if (typeof path === 'string') path = [path]
    
  // 註冊模塊
  this._modules.register(path, rawModule)
  // 安裝模塊
  installModule(this, this.state, path, this._modules.get(path), options.preserveState)
  // 重新生成 vue 實例掛載到 store 上,
  // 然後銷毀舊的
  resetStoreVM(this, this.state)
}

## 備註

希望疫情儘快過去吧。——2020/02/08 元宵


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • redis中壓縮列表ziplist相關的文件為:ziplist.h與ziplist.c 壓縮列表是redis專門開發出來為了節約記憶體的記憶體編碼數據結構。源碼中關於壓縮列表介紹的註釋也寫得比較詳細。 一、數據結構 壓縮列表的整體結構如下(借用redis源碼註釋): 1 /* 2 <zlbytes> < ...
  • 實例代碼: import React, { Component , PropTypes} from 'react'; import { AppRegistry, StyleSheet, Text, View, TextInput, TouchableOpacity } from 'react-nat ...
  • 慕課網- LovelyChubby-Jetpack全組件實戰 開發短視頻應用App-348元 筆記 微雲:https://share.weiyun.com/81aa12bb98016e200add31fb8e191cdf百度網盤:鏈接:https://pan.baidu.com/s/1IiClTkQ ...
  • Kotlin Android項目可用的靜態檢查工具: Android官方的Lint, 第三方的ktlint和detekt. ...
  • SublimeText3 Emmet安裝問題網上已經很多帖子了,這個簡單,主要對win7 64位我本人遇到的Emmet好多快捷功能無法用(比如ul>li*4 Tab無法生成)問題做了整理!搜了好多文章最終找到問題所在! 希望能幫到大家,也給自己做個記錄! 軟體下載: 這篇文章提供的是Windows ...
  • 時間限制:C/C++ 1秒,其他語言2秒 空間限制:C/C++ 256M,其他語言512M 小易給你一個包含n個數字的數組。你可以對這個數組執行任意次以下交換操作: 對於數組中的兩個下標i,j(1<=i,j<=n),如果為奇數,就可以交換和。 現在允許你使用操作次數不限,小易希望你能求出在所有能通過 ...
  • 作為開發人員,我喜歡在編碼時聽音樂。管弦樂使我可以更加專註於自己的工作。有一天,我註意到我的手指隨著音樂節奏在鍵盤上跳舞。喜歡彈鋼琴。代碼中的每個單詞或符號都和諧地書寫。然後我想...聽起來如何...我每天編寫的代碼? 這個想法誕生了。 ⭐️ 繼續在soundcode.now.sh上 直播,放置您的 ...
  • 事件的移除 removeEventListener() 第二個參數需要指定要移除的事件句柄,不能是匿名函數,因為無法識別 想要移除成功,那麼三個參數必須跟addEventListener中的三個完全一致 <!DOCTYPE html> <html lang="en"> <head> <meta ch ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...