Vuex
示意圖在說以前先來看一張官方文檔提供的一張圖html
State
內部預先定義的屬性。State
內部屬性,則須要調度(Dispatch
)觸發 Action
響應狀態變化(也可直接提交 mutation
)。mutation
去更改狀態(相似發事件)。State
內部屬性的更改,觸發依賴其屬性的組件從新渲染。瞭解了大體流程,接下來咱們就以基礎示例入手,剖析其實現了哪些功能,根據其實現的功能逐步去捋清其代碼實現vue
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
})
複製代碼
在看具體的代碼實現以前,咱們大體的先了解一下整個 Vuex
入口文件內的大體內容:git
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
}
複製代碼
在瞭解了其內部構造,咱們就根據上述梳理,逐點分析其實現。github
咱們知道,Vue
的插件都須要給 Vue
提供一個註冊鉤子函數 installl
, 執行 Vue.use(Vuex)
實際內部走的是 install
函數的內部調用。vuex
// 插件註冊: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
api
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
版本的更迭。promise
根據上述分析咱們知道,在註冊插件時,根據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"
]),
}
};
複製代碼
/** * 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] }))
}
複製代碼