vuex 原始碼簡析

cllemon發表於2019-07-13

說明

  • 以下內容均是以 Vuex 2.0.0 版本展開分析。
  • 此篇文章,是自己對 Vuex 實現分析的記錄性文章,如有任何錯誤,歡迎指正交流。

Vuex 示意圖

在說之前先來看一張官方文件提供的一張圖

vuex

  1. 首先元件渲染依賴了部分全域性的 State 內部預先定義的屬性。
  2. 元件內部需要更新之前依賴的 State 內部屬性,則需要排程(Dispatch)觸發 Action 響應狀態變化(也可直接提交 mutation)。
  3. 響應狀態變化函式內部必須提交 mutation 去更改狀態(類似發事件)。
  4. State 內部屬性的更改,觸發依賴其屬性的元件重新渲染。

基礎示例剖析

瞭解了大致流程,接下來我們就以基礎示例入手,剖析其實現了哪些功能,根據其實現的功能逐步去捋清其程式碼實現

import Vue from 'vue'
import Vuex from 'vuex'

// 註冊外掛
Vue.use(Vuex)

// 根狀態物件。每個Vuex例項只是一個狀態樹。
const state = { count: 0 }

// mutations 實際上是改變狀態的操作。每個 mutation 處理程式都將整個狀態樹作為第一個引數,然後是附加的有效負載引數。
// mutations 必須是同步的,並且可以通過外掛記錄下來,以便除錯。
const mutations = {
  increment (state) {
    state.count++
  },
  decrement (state) {
    state.count--
  }
}

// actions 是導致副作用並可能涉及非同步操作的函式。
const actions = {
  increment: ({ commit }) => commit('increment'),
  decrement: ({ commit }) => commit('decrement'),
  incrementIfOdd ({ commit, state }) {
    if ((state.count + 1) % 2 === 0) {
      commit('increment')
    }
  },
  incrementAsync ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('increment')
        resolve()
      }, 1000)
    })
  }
}

// getters are functions
const getters = {
  evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
}

// 模組
const moduleDemo = {
  state: { moduleCount: 1 },
  mutations: {
    // state: 模組的區域性狀態物件。
    moduleIncrement(state) {
      state.moduleCount++
    },
  },
  actions: {
    moduleIncrement: ({ commit }) => commit('moduleIncrement'),
  },
  getters: {
    moduleCountPlus: ({ moduleCount }) => moduleCount++
  }
}

// Vuex例項是通過組合 state、 mutations 、actions 和 getter 建立的。
const store = new Vuex.Store({
  state,
  getters,
  actions,
  mutations,
  modules: {
    moduleDemo
  },
})

new Vue({
  el: '#app',
  store
})

複製程式碼
  • 根據上述基礎使用,我們大概可以梳理出以下幾點:
  • 註冊外掛。
  • 定義 store 所需配置引數。
  • 例項化 Store 類.
  • 例項化 Vue 並傳入 store。

在看具體的程式碼實現之前,我們大致的先了解一下整個 Vuex 入口檔案內的大致內容:

import devtoolPlugin from './plugins/devtool'
import applyMixin from './mixin'
import { mapState, mapMutations, mapGetters, mapActions } from './helpers'
import { isObject, isPromise, assert } from './util'

let Vue // 繫結安裝

// Store 全域性單例模式管理
class Store { ... }

// 更新模組
function updateModule(targetModule, newModule) { ... }

// 重置 Store
function resetStore(store) { ... }

// 重置 Store 上 Vue 例項
function resetStoreVM(store, state) { ... }

// 安裝模組
function installModule(store, rootState, path, module, hot) { ... }

// 註冊 mutations 構造器選項
function registerMutation(store, type, handler, path = []) { ... }

// 註冊 action
function registerAction(store, type, handler, path = []) { ... }

// 包裝 getters
function wrapGetters(store, moduleGetters, modulePath) { ... }

// 啟用嚴格模式
function enableStrictMode(store) {}

// 獲取巢狀的狀態
function getNestedState(state, path) {}

// 外掛註冊方法
function install(_Vue) {}

// 自動註冊外掛
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

export default {
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions
}
複製程式碼

在瞭解了其內部構造,我們就根據上述梳理,逐點分析其實現。

註冊外掛

我們知道,Vue 的外掛都需要給 Vue 提供一個註冊鉤子函式 installl, 執行 Vue.use(Vuex) 實際內部走的是 install 函式的內部呼叫。

install


// 外掛註冊:vue 內部在呼叫會把 Vue 透傳過來
function install(_Vue) {
  // 避免重複註冊
  if (Vue) {
    console.error(
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  // 繫結安裝
  Vue = _Vue
  // 應用全域性 Vue.mixins
  applyMixin(Vue)
}

// 自動序號產生器制
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

複製程式碼

applyMixin

export default function (Vue) {
  // 獲取 Vue 版本號
  const version = Number(Vue.version.split('.')[0])

  // 版本號為 2.x
  if (version >= 2) {
    // 若存在 init 鉤子則把 VuexInit 混入 初始化階段
    // 其它混入 beforeCreate 階段
    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  } else {
    // 覆蓋 init 併為 1.x 注入 vuex init 過程。 向後相容性。
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init鉤子,注入到每個例項init鉤子列表中。
   */
  function vuexInit() {
    // 獲取例項配置引數
    const options = this.$options
    // 注入 store 例項
    if (options.store) {
      this.$store = options.store
      // 若不存在,則去尋找父級 store 例項
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

複製程式碼

注:_lifecycleHooks

  • Vue 內部配置項,引用其生命週期相關鉤子函式的函式名,由於遺留原因而暴露的配置項。

    • Vue 2.0.0 ⬆️ 其引用為:
      [
        'beforeCreate',
        'created',
        'beforeMount',
        'mounted',
        'beforeUpdate',
        'updated',
        'beforeDestroy',
        'destroyed',
        'activated', // 啟用
        'deactivated', // 停用
        'errorCaptured' // 捕獲錯誤
      ]
    複製程式碼
    • Vue v2.0.0-alpha.6 ⬇️ 其引用為:
     /**
      * List of lifecycle hooks.
      */
      _lifecycleHooks: [
        'init',
        'created',
        'beforeMount',
        'mounted',
        'beforeUpdate',
        'updated',
        'beforeDestroy',
        'destroyed',
        'activated',
        'deactivated'
      ]
    複製程式碼

若對此有興趣,可以研究一下 Vue.js 版本的更迭。

根據上述分析我們知道,在註冊外掛時,根據Vue的不同版本選擇合適的混入時機,使得建立的每個 Vue 例項在 “初始化階段” 做些預處理(在例項新增$store 屬性,其值為 Store 例項)。那麼接下來我們就具體來看看 Store 內部做了些什麼?

Store

/**
 * Store
 *
 * @class Store 全域性單例模式管理
 */
class Store {
  constructor(options = {}) {
    assert(Vue, `在建立商店例項之前必須呼叫 Vue.use(Vuex)`)
    assert(typeof Promise !== 'undefined', `vuex 需要一個 promise polyfill 在這個瀏覽器。`)

    const {
      state = {}, // Object | Function Vuex store 例項的根 state 物件
      plugins = [], // Array<Function> 一個陣列,包含應用在 store 上的外掛方法。這些外掛直接接收 store 作為唯一引數,可以監聽 mutation
      strict = false // 嚴格模式下,任何 mutation 處理函式以外修改 Vuex state 都會丟擲錯誤。
    } = options // Vuex.Store 構造器選項

    // store 內部狀態
    this._options = options
    // 是否正在提交
    this._committing = false
    // 儲存著 所有 actions
    this._actions = Object.create(null)
    // 儲存著 所有 mutations
    this._mutations = Object.create(null)
    // 儲存著 所有 Getters
    this._wrappedGetters = Object.create(null)
    this._runtimeModules = Object.create(null)
    // 訂閱函式池
    this._subscribers = []
    // 儲存著 Vue 例項
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    const store = this
    const { dispatch, commit } = this
    // 例項方法 - 分發 action 返回一個解析所有被觸發的 action 處理器的 Promise。
    this.dispatch = function boundDispatch(type, payload) {
      return dispatch.call(store, type, payload)
    }
    // 例項方法 - 提交 mutation
    this.commit = function boundCommit(type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // 嚴格模式
    this.strict = strict

    // init root 模組。這還遞迴地註冊所有子模組,並在 this._wrappedgechers 中收集所有模組 getter
    installModule(this, state, [], options)

    // 初始化負責反應性的儲存vm(也將_wrappedgechers註冊為計算屬性)
    resetStoreVM(this, state)

    // 注入應用外掛
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }

  get state() {
    return this._vm.state
  }

  set state(v) {
    assert(false, `使用 store.replacestate() 顯式替換儲存狀態。`)
  }

  /**
   * 更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。
   * 如:store.commit('increment')
   * 每個 mutation 都有一個字串的 事件型別 (type) 和 一個 回撥函式 (handler)。
   * 這個回撥函式就是我們實際進行狀態更改的地方,並且它會接受 state 作為第一個引數
   *
   * @param {String} type
   * @param {Object} payload
   * @param {Object} options
   * @memberof Store
   */
  commit(type, payload, options) { ... }

  //  分發 action
  dispatch(type, payload) { ... }

  subscribe(fn) { ... }

  watch(getter, cb, options) { ... }

  // 替換 State
  replaceState(state) { ... }

  // 註冊模組
  registerModule(path, module) { ... }

  // 登出模組
  unregisterModule(path) { ... }

  hotUpdate(newOptions) { ... }

  // 提交 mutation。
  _withCommit(fn) {
    // 儲存當前提交狀態
    const committing = this._committing
    // 置為提交狀態
    this._committing = true
    // 呼叫更改狀態函式
    fn()
    // 把提交狀態置回原來的狀態
    this._committing = committing
  }
}

複製程式碼

簡單分析梳理:

  • constructor:

    • 首先在建構函式內部定義了一些屬性(這裡註釋比較清楚)
    • 執行了一些初始化 installModuleresetStoreVM 等方法,下面將著重看一下其內部實現。
  • state 定義了取值函式(getter)和存值函式(setter),做一層代理(防止意外的修改)。

  • 定義了一些例項方法 commitdispatch 等(之後根據實際呼叫,具體分析)。

具體實現:

installModule - 安裝模組

init root 模組。這還遞迴地註冊所有子模組,並在 this._wrappedgechers 中收集所有模組 getter

/**
 * 安裝模組
 *
 * @param {Object} store Store 例項
 * @param {Object | Function} rootState Vuex store 例項的根 state 物件
 * @param {Array} path
 * @param {Object} module Vuex.Store 構造器選項
 * @param {*} hot
 */
function installModule(store, rootState, path, module, hot) {
  const isRoot = !path.length // 是否是根還是模組
  // 從 Vuex.Store 構造器選項解構出相關選項
  const {
    state, // Vuex store 例項的根 state 物件。
    actions, // 在 store 上註冊 action。處理函式總是接受 context 作為第一個引數,payload 作為第二個引數(可選)。
    mutations, // 在 store 上註冊 mutation,處理函式總是接受 state 作為第一個引數(如果定義在模組中,則為模組的區域性狀態),payload 作為第二個引數(可選)。
    getters, // 在 store 上註冊 getter,getter 方法接受以下引數: state, // 如果在模組中定義則為模組的區域性狀態. getters, // 等同於 store.getters.
    modules  // 包含了子模組的物件,會被合併到 store
  } = module

  // 設定 module 的 state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    // 為根 State 新增模組的 state
    store._withCommit(() => {
      Vue.set(parentState, moduleName, state || {})
    })
  }

  // 若存在 mutations 構造器選項 則將其全部選項註冊
  if (mutations) {
    Object.keys(mutations).forEach(key => {
      registerMutation(store, key, mutations[key], path)
    })
  }

  // 註冊 action
  if (actions) {
    Object.keys(actions).forEach(key => {
      registerAction(store, key, actions[key], path)
    })
  }

  // 包裝 getters
  if (getters) {
    wrapGetters(store, getters, path)
  }

  // 安裝模組
  if (modules) {
    Object.keys(modules).forEach(key => {
      // 遞迴呼叫註冊每一個模組
      installModule(store, rootState, path.concat(key), modules[key], hot)
    })
  }
}
複製程式碼

由上述初始化呼叫 installModule(this, state, [], options) 可知其入參,下面就看看各個選項註冊的程式碼實現。

MutationActiongetter 註冊

/**
 * 註冊 mutations 構造器選項
 *
 * @param {*} store
 * @param {*} type
 * @param {*} handler
 * @param {*} [path=[]]
 */
function registerMutation(store, type, handler, path = []) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler(payload) {
    handler(getNestedState(store.state, path), payload)
  })
}

/**
 * 註冊 action
 *
 * @param {Object} store
 * @param {String} type
 * @param {Function} handler
 * @param {Array} [path=[]]
 */
function registerAction(store, type, handler, path = []) {
  const entry = store._actions[type] || (store._actions[type] = [])
  const { dispatch, commit } = store
  entry.push(function wrappedActionHandler(payload, cb) {
    // 注意這裡透傳的context: 不是 store 例項本身。
      dispatch,
      commit,
      getters: store.getters,
      state: getNestedState(store.state, path),
      rootState: store.state
    }, payload, cb)
    // 判斷是否是 promise
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    // 處理應用的 devtools 外掛
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

/**
 * 包裝 getters
 *
 * @param {Object} store
 * @param {Object} moduleGetters
 * @param {Array modulePath}
 */
function wrapGetters(store, moduleGetters, modulePath) {
  Object.keys(moduleGetters).forEach(getterKey => {
    const rawGetter = moduleGetters[getterKey]
    if (store._wrappedGetters[getterKey]) {
      console.error(`[vuex] 重複的getter關鍵: ${getterKey}`)
      return
    }
    store._wrappedGetters[getterKey] = function wrappedGetter(store) {
      return rawGetter(
        getNestedState(store.state, modulePath), // local state
        store.getters, // getters
        store.state // root state
      )
    }
  })
}

/**
 * 獲取巢狀的 state
 *
 * @param {Object} state
 * @param {Array} path
 * @returns
 */
function getNestedState(state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}

複製程式碼
  • 上述程式碼實現邏輯比較清晰,就是把註冊資訊新增到 _mutations_actions_wrappedGetters 統一管理。

  • 若存在模組則會將其state新增到 root state 中。

  • 上述基礎示例,最終安裝結果如下:

    
      _mutations: {
        decrement: [
          ƒ wrappedMutationHandler(payload)
        ],
        increment: [
          ƒ wrappedMutationHandler(payload)
        ],
        moduleIncrement: [
          ƒ wrappedMutationHandler(payload)
        ]
      }
    
      _actions: {
        decrement: [
          ƒ wrappedActionHandler(payload, cb)
        ],
        increment: [
          ƒ wrappedActionHandler(payload, cb)
        ],
        incrementAsync: [
          ƒ wrappedActionHandler(payload, cb)
        ],
        incrementIfOdd: [
          ƒ wrappedActionHandler(payload, cb)
        ],
        moduleIncrement: [
          ƒ wrappedActionHandler(payload, cb)
        ]
      }
    
      _wrappedGetters: {
        evenOrOdd: ƒ wrappedGetter(store),
        moduleCountPlus: ƒ wrappedGetter(store)
      }
    
      // root state
      state: {
        count: 0,
        moduleDemo: {
          moduleCount: 1
        }
      }
    
    複製程式碼

resetStoreVM - 重置 Store 上 Vue 例項


/**
 * 重置 Store 上 Vue 例項
 *
 * @param {*} store
 * @param {*} state
 */
function resetStoreVM(store, state) {
  // 取之前的 vue 例項
  const oldVm = store._vm

  // 繫結儲存公共 getter
  store.getters = {}
  // 獲取 Store 中所有 getter
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 代理取值函式
  Object.keys(wrappedGetters).forEach(key => {
    const fn = wrappedGetters[key]
    // 利用 computed 的延遲快取機制
    computed[key] = () => fn(store)
    // 在公共 getter 上定義之前合併的 getter,並做一層取值代理,實際上取得是計算屬性定義的 key 值。
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key]
    })
  })

  // 使用 Vue 例項儲存狀態樹抑制警告,以防使用者新增了一些 funky global mixins
  const silent = Vue.config.silent
  // 關閉 Vue 內部的警告
  Vue.config.silent = true
  // 新增 _vm 屬性,值為 Vue 例項
  store._vm = new Vue({
    data: { state },
    computed
  })
  // 開啟 Vue 內部的警告
  Vue.config.silent = silent

  // 啟用嚴格模式 for new vm
  // 嚴格模式下在非提交的情況下修改 state,丟擲錯誤。
  if (store.strict) {
    enableStrictMode(store)
  }

  // 若存在之前的Vue例項
  if (oldVm) {
    // 在所有訂閱的觀察者中分派更改,以強制 getter 重新評估。
    store._withCommit(() => {
      oldVm.state = null
    })
    // 在下個更新佇列之後銷燬之前的 Vue 例項
    Vue.nextTick(() => oldVm.$destroy())
  }
}

/**
 * 啟用嚴格模式
 *
 * @param {Object} store
 * @returns {void}
 * 注:使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函式以外修改 Vuex state 都會丟擲錯誤。
 */
function enableStrictMode(store) {
  store._vm.$watch('state', () => {
    assert(store._committing, `不要在 mutation 處理程式之外對 vuex 儲存狀態進行改變;更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。`)
  }, { deep: true, sync: true })
}

複製程式碼

在講解具體用例前,先來看看 dispatchcommit 的程式碼實現:

dispatchcommit - 排程和提交

/**
   * 更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。
   * 如:store.commit('increment')
   * 每個 mutation 都有一個字串的 事件型別 (type) 和 一個 回撥函式 (handler)。
   * 這個回撥函式就是我們實際進行狀態更改的地方,並且它會接受 state 作為第一個引數
   *
   * @param {*} type
   * @param {*} payload
   * @param {*} options
   * @memberof Store
   */
  commit(type, payload, options) {
    // 檢查物件樣式提交 如:
    // store.commit({ type: 'increment',  amount: 10 })
    if (isObject(type) && type.type) {
      options = payload
      payload = type // 使用物件風格的提交方式,整個物件都作為載荷傳給 mutation 函式
      type = type.type
    }
    const mutation = { type, payload }
    const entry = this._mutations[type] // 查詢 mutation
    // 若不存在則丟擲錯誤
    if (!entry) {
      console.error(`[vuex] 未知 mutation 型別: ${type}`)
      return
    }
    // 提交 mutation
    this._withCommit(() => {
      entry.forEach(function commitIterator(handler) {
        handler(payload)
      })
    })
    // 若滿足該條件,則:呼叫訂閱池內所有的訂閱函式
    if (!options || !options.silent) {
      this._subscribers.forEach(sub => sub(mutation, this.state))
    }
  }

  /**
   * 分發 action
   *
   * @param {*} type
   * @param {*} payload
   * @returns {Promise} 解析所有被觸發的 action 處理器的 Promise
   * @memberof Store
   */
  dispatch(type, payload) {
    // check object-style dispatch
    // 同上解釋
    if (isObject(type) && type.type) {
      payload = type
      type = type.type
    }
    const entry = this._actions[type]
    if (!entry) {
      console.error(`[vuex] 未知 action 型別: ${type}`)
      return
    }
    return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
  }
複製程式碼
  • 講到這裡整個流程就已經分析的差不多了。

  • 這裡順便提一下:Mutation 必須是同步函式

    • 若是非同步這會使 devtool 中的 mutation 日誌變得不可追蹤。【參閱】
  • 以下用例演示了從 dispatch(排程) action其內部觸發 commit(提交) 進而呼叫 mutation 狀態修改函式, 來達到更新狀態。相當清晰?

    <template>
      <div id="app">
        <div class="root">
          Clicked: {{ $store.state.count }} times, count is {{ $store.getters.evenOrOdd }}.
          <button @click="$store.dispatch('increment')">+</button>
        </div>
        <div class="module">
          Clicked: {{ $store.state.moduleDemo.moduleCount }} times
          <button @click="$store.dispatch('moduleIncrement')">+</button>
        </div>
      </div>
    </template>
    複製程式碼

    點選 “+” 排程actions內部對應的處理函式,其內部去提交狀態改變(類似分發事件)在 mutations 內部去執行響應的函式,真正改變狀態。狀態的改變,導致依賴這些狀態的元件更新。“Clicked: 1”

    const state = {
      count: 0
    }
    
    const actions = {
      increment: ({ commit }) => commit('increment'),
      ...
    }
    
    const mutations = {
      increment (state) {
        state.count++
      },
      ...
    }
    
    const moduleDemo = {
      state: { moduleCount: 1 },
      mutations: {
        moduleIncrement(state) {
          state.moduleCount++
        },
      },
      actions: {
        moduleIncrement: ({ commit }) => commit('moduleIncrement'),
      },
      ...
    }
    
    複製程式碼

其它細節

接下來我們就來看看我們在元件中經常使用的輔助函式實現如:

import {
  mapActions,
  mapActions,
  mapMutations,
  mapGetters
} from "vuex";

export default {
  computed: {
    ...mapState([
      'count' // 對映 this.count 為 this.$store.state.count
    ]), // 或 ...mapState({ count: state => state.count })

    ...mapGetters(["evenOrOdd"]),
  },
  methods: {
    // 必須同步提交
    ...mapMutations([
      'increment', // 將 `this.increment()` 對映為 `this.$store.commit('increment')`
      // `mapMutations` 也支援載荷:
      'decrement' // 將 `this.decrement(amount)` 對映為 `this.$store.commit('decrement', amount)`
    ]),
    // 處理非同步
    ...mapActions([
      "increment",
      "decrement",
      "incrementIfOdd",
      "incrementAsync"
    ]),
  }
};

複製程式碼

mapState

/**
 * state 對映處理函式
 *
 * @export
 * @param {Array | Object} states
 * @returns {Object}
 */
export function mapState (states) {
  const res = {}
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      return typeof val === 'function'
        ? val.call(this, this.$store.state, this.$store.getters)
        : this.$store.state[val]
    }
  })
  return res
}

/**
 * 規範引數型別
 *
 * @param {*} map
 * @returns {Array}
 */
function normalizeMap(map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}
複製程式碼
  • 我們能看出來,這些輔助函式,主要是做了一層對映,”解決重複和冗餘,讓你少按幾次鍵“。
  • 這裡只是提取了一個進行講解,其它思路差不多,這裡就不多說了。
  • 最後,本文著重點放在貫通流程,及常用實現,裡面還有許多細節沒有提及。
  • 若有興趣請參閱 vuex 。建議將其程式碼拉下來,根據其用例,本地跑起來,斷點去調式,結合文件慢慢去看,相信一定收穫巨多!

參閱

相關文章