vuex詳解
文章講解的Vuex
的版本為4.1.0
,會(huì)根據(jù)一些api
來(lái)深入源碼講解,幫助大家更快掌握vuex
的使用。
vuex的使用
使用Vue
實(shí)例的use
方法把Vuex
實(shí)例注入到Vue
實(shí)例中。
const store = createStore({...})
createApp(App).use(store)
use
方法執(zhí)行的是插件的中的install
方法
src/store.js
export class Store {
// ...
// app 是vue實(shí)例
install(app, injectKey) {
app.provide(injectKey || storeKey, this)
app.config.globalProperties.$store = this
...
}
}
從上面可以看到Vue
實(shí)例通過(guò) provide
方法把 store
實(shí)例 provide
到了根實(shí)例中。同時(shí)添加了一個(gè)全局變量$store
,在每個(gè)組件中都可以通過(guò)this.$store
來(lái)訪問(wèn)store
實(shí)例。
app.provide
是給 Composition API
方式編寫(xiě)的組件用的,因?yàn)橐坏┦褂昧?Composition API
,我們?cè)诮M件中想訪問(wèn) store
的話會(huì)在 setup
函數(shù)中通過(guò) useStore API
拿到.
import { useStore } from 'vuex'
export default {
setup() {
const store = useStore()
}
}
useStore
的實(shí)現(xiàn)也在src/injectKey.js
文件中:
import { inject } from 'vue'
export const storeKey = 'store'
export function useStore (key = null) {
return inject(key !== null ? key : storeKey)
}
Vuex
就是利用了 provide/inject
依賴注入的 API
實(shí)現(xiàn)了在組件中訪問(wèn)到 store
,由于是通過(guò) app.provide
方法把 store
實(shí)例 provide
到根實(shí)例中,所以在 app
內(nèi)部的任意組件中都可以 inject store
實(shí)例并訪問(wèn)了。
除了 Composition API
,Vue3.x
依然支持 Options API
的方式去編寫(xiě)組件,在 Options API
組件中我們就可以通過(guò) this.$store
訪問(wèn)到 store
實(shí)例,因?yàn)閷?shí)例的查找最終會(huì)找到全局 globalProperties
中的屬性(globalProperties
添加一個(gè)可以在應(yīng)用的任何組件實(shí)例中訪問(wèn)的全局 property
。組件的 property
在命名沖突具有優(yōu)先權(quán))。
provide/inject
在 Vuex
中的作用就是讓組件可以訪問(wèn)到 store
實(shí)例。
響應(yīng)式狀態(tài)
state
在state
中設(shè)置的屬性是響應(yīng)式的
使用:
// 設(shè)置state
const state = createStore({
state: {
value: 1
}
})
// 在組件上使用
const Com = {
template: `<div>{{ $store.state.value }}</div>`,
}
之后會(huì)在Store
類(lèi)中調(diào)用一個(gè)resetStoreState
方法,將傳入的state
通過(guò)reactive
方法設(shè)置成響應(yīng)式。
export function resetStoreState (store, state, hot) {
...
store._state = reactive({
data: state
})
...
}
getters
可以從 store
中的 state
中派生出一些狀態(tài)。
// 設(shè)置state和getters
const store = new Vuex.Store({
state: {
value: 1,
},
getters: {
getterVal(state, getters) {
return state.value + 1;
}
}
})
// 在組件上使用
const Com = {
template: `<div>{{$store.getters.getterVal}}</div>`
}
也可以通過(guò)讓getter
返回一個(gè)函數(shù)來(lái)實(shí)現(xiàn)給getter
傳遞參數(shù)。
// 設(shè)置state和getters
const store = new Vuex.Store({
state: {
value: 1,
},
getters: {
getterVal(state, getters) {
return function(num) {
return state.val + num;
};
}
}
})
// 在組件上使用
const Com = {
template: `<div>{{$store.getters.getterVal(3)}}</div>`
}
同時(shí)vuex
內(nèi)部對(duì)getter
有進(jìn)行緩存處理,只有當(dāng)依賴的state
發(fā)生改變后才會(huì)重新收集。
來(lái)看看vuex
內(nèi)部是如何實(shí)現(xiàn)getter
的:
- 類(lèi)
Store
中會(huì)調(diào)用installModule
會(huì)對(duì)傳入的模塊進(jìn)行處理(詳情請(qǐng)看下面的模塊化部分,里邊會(huì)對(duì)模塊里的getters
處理)
// src/store-util.js
// 對(duì)所有模塊中的getters進(jìn)行處理
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
function registerGetter (store, type, rawGetter, local) {
// 去重處理
if (store._wrappedGetters[type]) {
if (__DEV__) {
console.error(`[vuex] duplicate getter key: ${type}`)
}
return
}
// 往store實(shí)例的_wrappedGetters存儲(chǔ)getters,這樣就可以通過(guò)store.getters['xx/xxx']來(lái)訪問(wèn)
store._wrappedGetters[type] = function wrappedGetter (store) {
// 調(diào)用模塊中設(shè)置的getter方法,可以看到這里是傳入了4個(gè)參數(shù)
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
- 在類(lèi)
Store
中調(diào)用resetStoreState
方法中對(duì)使用computed
方法對(duì)getter
進(jìn)行計(jì)算后緩存。
export function resetStoreState (store, state, hot) {
...
const wrappedGetters = store._wrappedGetters
const scope = effectScope(true)
scope.run(() => {
forEachValue(wrappedGetters, (fn, key) => {
computedObj[key] = partial(fn, store)
computedCache[key] = computed(() => computedObj[key]())
Object.defineProperty(store.getters, key, {
get: () => computedCache[key].value,
enumerable: true
})
})
})
...
}
Mutations
更改 Vuex
的 store
中的狀態(tài)的唯一方法是提交 mutation
。Vuex
中的 mutation
非常類(lèi)似于事件:每個(gè) mutation
都有一個(gè)事件類(lèi)型 (type
)和一個(gè)回調(diào)函數(shù) (handler
)。這個(gè)回調(diào)函數(shù)就是實(shí)際進(jìn)行狀態(tài)更改的地方。
const store = createStore({
state: {
value: 1
},
mutations: {
increment (state, payload) {
state.value++
}
}
})
在installModule
方法中,對(duì)模塊中的mutations
進(jìn)行注冊(cè),
// src/store-util.js
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
}
把模塊中的mutations
全部添加到Store
實(shí)例中的_mutations
對(duì)象中,將mutations
中的this
指向Store
實(shí)例,并傳入兩個(gè)參數(shù):
-
state
: 當(dāng)前模塊的state
(具體詳解請(qǐng)看模塊化部分) -
payload
: 通過(guò)commit
中傳入的參數(shù)。
像上面的demo
中_mutations
值為{increment: [fn]}
commit觸發(fā)mutation
語(yǔ)法:commit(type: string, payload?: any, options?: Object)
commit(mutation: Object, options?: Object)
通過(guò)使用commit
方法來(lái)觸發(fā)對(duì)應(yīng)的mutation
store.commit('increment');
commit
可以接受額外的參數(shù)(payload
)來(lái)為mutation
傳入?yún)?shù)。
const store = createStore({
state: {
value: 1
},
mutations: {
increment (state, num) {
state.value += num;
}
}
})
store.commit('increment', 2);
在大多數(shù)情況下,載荷應(yīng)該是一個(gè)對(duì)象,這樣可以包含多個(gè)字段并且記錄的 mutation
會(huì)更易讀
在類(lèi)Store
中定義了commit
方法,因?yàn)?code>commit方法內(nèi)部需要使用到Store
實(shí)例中的方法,因此需要使用call
方法把this
指向Store
實(shí)例。
// src/store.js
export class Store {
constructor (options = {}) {
this._mutations = Object.create(null)
// 改變this
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
...
commit (_type, _payload, _options) {
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
const entry = this._mutations[type]
...
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
...
}
}
// src/store-util.js
export function unifyObjectStyle (type, payload, options) {
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
return { type, payload, options }
}
可以看到在處理傳入commit
的參數(shù)時(shí)Vuex
進(jìn)行了處理??梢酝?code>commit傳入對(duì)象形式的配置。
const store = createStore({
state: {
value: 1
},
mutations: {
increment (state, payload) {
state.value += payload.value;
}
}
})
store.commit({
type: 'increment',
value: 1
})
mutation
中必須是同步函數(shù),如果mutation
是一個(gè)異步函數(shù),異步修改狀態(tài),雖然也會(huì)使?fàn)顟B(tài)正常更新,但是會(huì)導(dǎo)致開(kāi)發(fā)者工具有時(shí)無(wú)法追蹤到狀態(tài)的變化,調(diào)試起來(lái)就會(huì)很困難。
訂閱mutations
Vuex
提供了subscribe
方法用來(lái)訂閱 store
的 mutation
,當(dāng)mutations
執(zhí)行完后就會(huì)觸發(fā)訂閱回調(diào)。
語(yǔ)法:subscribe(handler, options)
-
handler
: 處理函數(shù) -
options
: 配置 -
-
prepend
:Boolean
值,把處理函數(shù)添加到訂閱函數(shù)鏈的最開(kāi)始。
-
const store = createStore(...)
// subscribe方法的返回值是一個(gè)取消訂閱的函數(shù)
const unsubscribe = store.subscribe((mutation, state) => {
console.log(mutation)
})
// 你可以調(diào)用 unsubscribe 來(lái)停止訂閱。
// unsubscribe()
const unsubscribe2 = store.subscribe((mutation, state) => {
console.log('在訂閱函數(shù)鏈頭部,最先執(zhí)行')
}, {prepend: true})
訂閱的源碼實(shí)現(xiàn)非常簡(jiǎn)單,使用一個(gè)數(shù)組來(lái)維護(hù)訂閱函數(shù)。在觸發(fā)commit
方法內(nèi)部添加執(zhí)行訂閱方法即可。
// src/store.js
export class Store {
constructor (options = {}) {
this._subscribers = [];
...
}
commit() {
...
const mutation = { type, payload }
// 執(zhí)行mutation
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 在mutation執(zhí)行完后執(zhí)行訂閱
this._subscribers
.slice()
.forEach(sub => sub(mutation, this.state))
}
subscribe (fn, options) {
return genericSubscribe(fn, this._subscribers, options)
}
...
}
// src/store-util.js
export function genericSubscribe (fn, subs, options) {
if (subs.indexOf(fn) < 0) {
// 如果傳入了prepend: true 把處理函數(shù)添加到訂閱函數(shù)鏈的最開(kāi)始
options && options.prepend
? subs.unshift(fn)
: subs.push(fn)
}
// 返回一個(gè)取消訂閱的方法
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}
Actions
Action
類(lèi)似于 mutation
,不同在于:
-
Action
提交的是mutation
,而不是直接變更狀態(tài)。 -
Action
可以包含任意異步操作。
const store = createStore({
state: {
value: 1
},
mutations: {
increment (state, payload) {
state.value += payload.value
}
},
actions: {
emitIncrement(context, payload) {
context.commit('increment', payload);
// action同樣可以改變state中的數(shù)據(jù),但是最好不要這樣使用,在嚴(yán)格模式下控制臺(tái)會(huì)拋異常且action是異步的,不方便DevTool 調(diào)試
// context.state.value++;
}
},
})
在installModule
方法中,對(duì)模塊中的actions
進(jìn)行注冊(cè)。跟mutations
一樣的是也是也把所有的action
方法放在一個(gè)變量_actions
下,同時(shí)this
也指向Store
實(shí)例,并傳入兩個(gè)參數(shù):
-
context
: 當(dāng)前模塊的上下文(具體詳解請(qǐng)看模塊化部分) -
payload
: 通過(guò)dispatch
中傳入的參數(shù)。
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload) {
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload)
// 強(qiáng)行轉(zhuǎn)化為promise,在dispatch就可以統(tǒng)一處理
if (!isPromise(res)) {
res = Promise.resolve(res)
}
...
return res
})
}
值得注意的是在action
執(zhí)行完后是返回一個(gè)Promise
格式的值。詳細(xì)看下面的dispatch
部分。
dipatch分發(fā)actions
語(yǔ)法:dispatch(type: string, payload?: any, options?: Object): Promise<any>
dispatch(action: Object, options?: Object): Promise<any>
store.dispatch('emitIncrement', {value: 1})
// 對(duì)象形式
store.dispatch({
type: 'emitIncrement',
value: 1
})
通過(guò)使用dispatch
方法來(lái)觸發(fā)對(duì)應(yīng)的action
// src/store.js
export class Store {
dispatch (_type, _payload) {
// 跟mutations一樣處理傳入對(duì)象的形式
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type]
...
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
...
}
}
可以看到在執(zhí)行action
時(shí)如果對(duì)應(yīng)的action
數(shù)組中存在多個(gè),用了Promise.all
來(lái)處理,這是為了當(dāng)type
對(duì)應(yīng)的所有的action
都執(zhí)行完后才繼續(xù)執(zhí)行這樣才能保證訂閱是在action
執(zhí)行之后觸發(fā)。
訂閱actions
跟mutation
一樣,action
也可以添加訂閱
語(yǔ)法:subscribeAction(handler: Function, options?: Object): Function
返回值也是一個(gè)取消訂閱方法。
-
handler
: 處理函數(shù) -
options
: 配置 -
-
before
: 在action
執(zhí)行前觸發(fā)(默認(rèn))
-
-
-
after
: 在action
執(zhí)行后觸發(fā)
-
-
-
error
: 捕獲分發(fā)action
的時(shí)候被拋出的錯(cuò)誤
-
-
-
prepend
:Boolean
值,把處理函數(shù)添加到訂閱函數(shù)鏈的最開(kāi)始。
-
相比起mutation
的訂閱,action
的訂閱較為復(fù)雜一點(diǎn)。
store.subscribeAction((action, state) => {
console.log(action)
})
store.subscribeAction((action, state) => {
console.log('在訂閱函數(shù)鏈頭部,最先執(zhí)行')
}, {prepend: true})
store.subscribeAction({
before: (action, state) => {
console.log(`before action`)
},
after: (action, state) => {
console.log(`after action`)
},
error: (action, state, error) => {
console.log(`error action`)
console.error(error)
}
})
來(lái)看看訂閱action
的添加和觸發(fā)相關(guān)的實(shí)現(xiàn)原理:
export class Store {
subscribeAction (fn, options) {
// 當(dāng)傳入的第一個(gè)參數(shù)為函數(shù)時(shí),封裝成{before:fn}的形式
const subs = typeof fn === 'function' ? { before: fn } : fn
// 把訂閱方法放入_actionSubscribers數(shù)組中
return genericSubscribe(subs, this._actionSubscribers, options)
}
dispatch (_type, _payload) {
const action = { type, payload }
const entry = this._actions[type]
...
// 執(zhí)行before相關(guān)訂閱
try {
this._actionSubscribers
.slice()
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
}
...
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
// 返回一個(gè)promise
return new Promise((resolve, reject) => {
// 對(duì)于同步函數(shù)而已,因此在注冊(cè)階段就直接使用Promise.resolve處理能異步函數(shù)了,所以統(tǒng)一使用then獲取執(zhí)行結(jié)果
result.then(res => {
try {
// 執(zhí)行after訂閱者
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
}
...
resolve(res)
}, error => {
try {
// 執(zhí)行error訂閱者
this._actionSubscribers
.filter(sub => sub.error)
.forEach(sub => sub.error(action, this.state, error))
}
...
reject(error)
})
})
}
}
模塊化
如果只使用單一狀態(tài)樹(shù),應(yīng)用的所有狀態(tài)就會(huì)集中到一個(gè)比較大的對(duì)象,store
對(duì)象就有可能變得相當(dāng)臃腫。Vuex
可以允許將 store
分割成模塊(module
)。每個(gè)模塊甚至是嵌套子模塊擁有 state
、getters
等。
例如
const nestedModule = {
namespaced: true,
state: {
a: 1
},
getters: {},
mutations: {},
actions: {},
}
const module = {
state: {},
getters: {},
mutations: {},
actions: {},
modules: {
nested: nestedModule
}
}
const store = createStore(module);
store.state.nested // {a: 1} 獲取嵌套module的狀態(tài)
模塊解析
在Vuex
實(shí)例創(chuàng)建時(shí),會(huì)對(duì)傳入的模塊進(jìn)行解析
export class Store {
constructor(options) {
this._modules = new ModuleCollection(options)
installModule(this, state, [], this._modules.root)
}
}
初始化ModuleCollection
時(shí)會(huì)對(duì)模塊進(jìn)行注冊(cè):
// src/module/module-collection.js
export default class ModuleCollection {
constructor (rawRootModule) {
this.register([], rawRootModule, false)
}
register (path, rawModule, runtime = true) {
// 將每個(gè)模塊使用一個(gè)module對(duì)象來(lái)展示
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} else {
// 將嵌套模塊添加到父模塊的_children屬性中
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// 注冊(cè)嵌套模塊
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
}
最后形成一個(gè)便于Vuex
處理的對(duì)象:
this._modules = {
root: {
runtime: Boolean,
state: {},
_children: [],
_rawModule: {},
namespaced: Boolean
}
}
installModule
方法非常重要,里邊會(huì)對(duì)嵌套module
進(jìn)行解析,使得嵌套內(nèi)部可以在調(diào)用dispatch
、commit
的時(shí)候能找到對(duì)應(yīng)的action
和mutation
。
// src/store-utils.js installModule
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
...
// 將子模塊的state添加到父模塊的state中
parentState[moduleName] = module.state
})
}
namespaced
的作用:
在子模塊中添加namespaced: true
,根模塊的會(huì)根據(jù)模塊的modules
字段中的key
作為索引將子模塊中mutations
、actions
、getters
存儲(chǔ)在根模塊中。例如:mutations: {'childModule/module1', [fn]}
在調(diào)用
commit
或者dispatch
方法的時(shí)候就可以根據(jù)這個(gè)key
找到對(duì)應(yīng)子模塊中的mutation
跟action
如果子模塊中沒(méi)有設(shè)置namespaced: true
,那么在根模塊中的_actions
等字段的key
就為對(duì)應(yīng)的子模塊中的actions
里定義的key
,如果模塊非常多的時(shí)候,容易造成命名沖突。而vuex
也考慮到這種情況,因此key
對(duì)應(yīng)的值的類(lèi)型為數(shù)組,這樣即使沖突了,也會(huì)執(zhí)行數(shù)組中所有的方法,不過(guò)這樣就會(huì)導(dǎo)致觸發(fā)其他模塊中非必要的action
、mutation
等。// 如兩個(gè)子模塊都存在一個(gè)increment的mutation,同時(shí)都沒(méi)有設(shè)置 namespaced: true,那么在根模塊的_mutations就為 _mutations: { 'increment', [fn1, fn2] } // 調(diào)用 store.commit('increment'),fn1, fn2都會(huì)執(zhí)行
將子模塊的state
添加到父模塊的state
中,這樣就可以通過(guò)store.state.childModule.state.xxx
進(jìn)行訪問(wèn)。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-422278.html
為模塊創(chuàng)建了一個(gè)執(zhí)行上下文,當(dāng)觸發(fā)commit
或者dispatch
時(shí)就會(huì)根據(jù)namespaced
找到對(duì)應(yīng)的模塊中的mutations
或者actions
中的方法。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-422278.html
// src/store-utils.js installModule
// 為當(dāng)前模塊添加一個(gè)上下文
const local = module.context = makeLocalContext(store, namespace, path)
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// src/store-utils.js
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ''
const local = {
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
...
if (!options || !options.root) {
type = namespace + type
...
}
return store.dispatch(type, payload)
},
// commit 類(lèi)似
}
...
return local
}
到了這里,關(guān)于一篇文章讓你徹底了解vuex的使用及原理(上)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!