說明
- 以下內容均是以 Vuex 2.0.0 版本展開分析。
- 此篇文章,是自己對 Vuex 實現分析的記錄性文章,如有任何錯誤,歡迎指正交流。
Vuex
示意圖
在說之前先來看一張官方文件提供的一張圖
- 首先元件渲染依賴了部分全域性的
State
內部預先定義的屬性。 - 元件內部需要更新之前依賴的
State
內部屬性,則需要排程(Dispatch
)觸發Action
響應狀態變化(也可直接提交mutation
)。 - 響應狀態變化函式內部必須提交
mutation
去更改狀態(類似發事件)。 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:
- 首先在建構函式內部定義了一些屬性(這裡註釋比較清楚)
- 執行了一些初始化
installModule
、resetStoreVM
等方法,下面將著重看一下其內部實現。
-
為
state
定義了取值函式(getter
)和存值函式(setter
),做一層代理(防止意外的修改)。 -
定義了一些例項方法
commit
、dispatch
等(之後根據實際呼叫,具體分析)。
具體實現:
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)
可知其入參,下面就看看各個選項註冊的程式碼實現。
Mutation
、Action
、getter
註冊
/**
* 註冊 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 })
}
複製程式碼
在講解具體用例前,先來看看 dispatch
、commit
的程式碼實現:
dispatch
、commit
- 排程和提交
/**
* 更改 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 。建議將其程式碼拉下來,根據其用例,本地跑起來,斷點去調式,結合文件慢慢去看,相信一定收穫巨多!