您现在的位置是:首页 >技术杂谈 >一篇文章让你彻底了解vuex的使用及原理(上)网站首页技术杂谈
一篇文章让你彻底了解vuex的使用及原理(上)
vuex详解
文章讲解的Vuex
的版本为4.1.0
,会根据一些api
来深入源码讲解,帮助大家更快掌握vuex
的使用。
vuex的使用
使用Vue
实例的use
方法把Vuex
实例注入到Vue
实例中。
const store = createStore({...})
createApp(App).use(store)
use
方法执行的是插件的中的install
方法
src/store.js
export class Store {
// ...
// app 是vue实例
install(app, injectKey) {
app.provide(injectKey || storeKey, this)
app.config.globalProperties.$store = this
...
}
}
从上面可以看到Vue
实例通过 provide
方法把 store
实例 provide
到了根实例中。同时添加了一个全局变量$store
,在每个组件中都可以通过this.$store
来访问store
实例。
app.provide
是给 Composition API
方式编写的组件用的,因为一旦使用了 Composition API
,我们在组件中想访问 store
的话会在 setup
函数中通过 useStore API
拿到.
import { useStore } from 'vuex'
export default {
setup() {
const store = useStore()
}
}
useStore
的实现也在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
实现了在组件中访问到 store
,由于是通过 app.provide
方法把 store
实例 provide
到根实例中,所以在 app
内部的任意组件中都可以 inject store
实例并访问了。
除了 Composition API
,Vue3.x
依然支持 Options API
的方式去编写组件,在 Options API
组件中我们就可以通过 this.$store
访问到 store
实例,因为实例的查找最终会找到全局 globalProperties
中的属性(globalProperties
添加一个可以在应用的任何组件实例中访问的全局 property
。组件的 property
在命名冲突具有优先权)。
provide/inject
在 Vuex
中的作用就是让组件可以访问到 store
实例。
响应式状态
state
在state
中设置的属性是响应式的
使用:
// 设置state
const state = createStore({
state: {
value: 1
}
})
// 在组件上使用
const Com = {
template: `<div>{{ $store.state.value }}</div>`,
}
TODO 在Store
里会初始化一个ModuleCollection
对象(详情请看下面的模块化部分),里边会对传入的state
进行判断(函数或者对象)处理。
之后会在Store
类中调用一个resetStoreState
方法,将传入的state
通过reactive
方法设置成响应式。
export function resetStoreState (store, state, hot) {
...
store._state = reactive({
data: state
})
...
}
getters
可以从 store
中的 state
中派生出一些状态。
// 设置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>`
}
也可以通过让getter
返回一个函数来实现给getter
传递参数。
// 设置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>`
}
同时vuex
内部对getter
有进行缓存处理,只有当依赖的state
发生改变后才会重新收集。
来看看vuex
内部是如何实现getter
的:
- 类
Store
中会调用installModule
会对传入的模块进行处理(详情请看下面的模块化部分,里边会对模块里的getters
处理)
// src/store-util.js
// 对所有模块中的getters进行处理
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实例的_wrappedGetters存储getters,这样就可以通过store.getters['xx/xxx']来访问
store._wrappedGetters[type] = function wrappedGetter (store) {
// 调用模块中设置的getter方法,可以看到这里是传入了4个参数
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
- 在类
Store
中调用resetStoreState
方法中对使用computed
方法对getter
进行计算后缓存。
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
中的状态的唯一方法是提交 mutation
。Vuex
中的 mutation
非常类似于事件:每个 mutation
都有一个事件类型 (type
)和一个回调函数 (handler
)。这个回调函数就是实际进行状态更改的地方。
const store = createStore({
state: {
value: 1
},
mutations: {
increment (state, payload) {
state.value++
}
}
})
在installModule
方法中,对模块中的mutations
进行注册,
// 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
实例中的_mutations
对象中,将mutations
中的this
指向Store
实例,并传入两个参数:
state
: 当前模块的state
(具体详解请看模块化部分)payload
: 通过commit
中传入的参数。
像上面的demo
中_mutations
值为{increment: [fn]}
commit触发mutation
语法:
commit(type: string, payload?: any, options?: Object)
commit(mutation: Object, options?: Object)
通过使用commit
方法来触发对应的mutation
store.commit('increment');
commit
可以接受额外的参数(payload
)来为mutation
传入参数。
const store = createStore({
state: {
value: 1
},
mutations: {
increment (state, num) {
state.value += num;
}
}
})
store.commit('increment', 2);
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation
会更易读
在类Store
中定义了commit
方法,因为commit
方法内部需要使用到Store
实例中的方法,因此需要使用call
方法把this
指向Store
实例。
// 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
的参数时Vuex
进行了处理。可以往commit
传入对象形式的配置。
const store = createStore({
state: {
value: 1
},
mutations: {
increment (state, payload) {
state.value += payload.value;
}
}
})
store.commit({
type: 'increment',
value: 1
})
mutation
中必须是同步函数,如果mutation
是一个异步函数,异步修改状态,虽然也会使状态正常更新,但是会导致开发者工具有时无法追踪到状态的变化,调试起来就会很困难。
订阅mutations
Vuex
提供了subscribe
方法用来订阅 store
的 mutation
,当mutations
执行完后就会触发订阅回调。
语法:subscribe(handler, options)
handler
: 处理函数options
: 配置-
prepend
:Boolean
值,把处理函数添加到订阅函数链的最开始。
const store = createStore(...)
// subscribe方法的返回值是一个取消订阅的函数
const unsubscribe = store.subscribe((mutation, state) => {
console.log(mutation)
})
// 你可以调用 unsubscribe 来停止订阅。
// unsubscribe()
const unsubscribe2 = store.subscribe((mutation, state) => {
console.log('在订阅函数链头部,最先执行')
}, {prepend: true})
订阅的源码实现非常简单,使用一个数组来维护订阅函数。在触发commit
方法内部添加执行订阅方法即可。
// src/store.js
export class Store {
constructor (options = {}) {
this._subscribers = [];
...
}
commit() {
...
const mutation = { type, payload }
// 执行mutation
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 在mutation执行完后执行订阅
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 把处理函数添加到订阅函数链的最开始
options && options.prepend
? subs.unshift(fn)
: subs.push(fn)
}
// 返回一个取消订阅的方法
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}
Actions
Action
类似于 mutation
,不同在于:
Action
提交的是mutation
,而不是直接变更状态。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中的数据,但是最好不要这样使用,在严格模式下控制台会抛异常且action是异步的,不方便DevTool 调试
// context.state.value++;
}
},
})
在installModule
方法中,对模块中的actions
进行注册。跟mutations
一样的是也是也把所有的action
方法放在一个变量_actions
下,同时this
也指向Store
实例,并传入两个参数:
context
: 当前模块的上下文(具体详解请看模块化部分)payload
: 通过dispatch
中传入的参数。
TODO action.handler怎么传值
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)
// 强行转化为promise,在dispatch就可以统一处理
if (!isPromise(res)) {
res = Promise.resolve(res)
}
...
return res
})
}
值得注意的是在action
执行完后是返回一个Promise
格式的值。详细看下面的dispatch
部分。
dipatch分发actions
语法:
dispatch(type: string, payload?: any, options?: Object): Promise<any>
dispatch(action: Object, options?: Object): Promise<any>
store.dispatch('emitIncrement', {value: 1})
// 对象形式
store.dispatch({
type: 'emitIncrement',
value: 1
})
通过使用dispatch
方法来触发对应的action
// src/store.js
export class Store {
dispatch (_type, _payload) {
// 跟mutations一样处理传入对象的形式
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)
...
}
}
可以看到在执行action
时如果对应的action
数组中存在多个,用了Promise.all
来处理,这是为了当type
对应的所有的action
都执行完后才继续执行这样才能保证订阅是在action
执行之后触发。
TODO 多个mutation和action是什么情况?
订阅actions
跟mutation
一样,action
也可以添加订阅
语法:subscribeAction(handler: Function, options?: Object): Function
返回值也是一个取消订阅方法。
handler
: 处理函数options
: 配置-
before
: 在action
执行前触发(默认)
-
after
: 在action
执行后触发
-
error
: 捕获分发action
的时候被抛出的错误
-
prepend
:Boolean
值,把处理函数添加到订阅函数链的最开始。
相比起mutation
的订阅,action
的订阅较为复杂一点。
store.subscribeAction((action, state) => {
console.log(action)
})
store.subscribeAction((action, state) => {
console.log('在订阅函数链头部,最先执行')
}, {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)
}
})
来看看订阅action
的添加和触发相关的实现原理:
export class Store {
subscribeAction (fn, options) {
// 当传入的第一个参数为函数时,封装成{before:fn}的形式
const subs = typeof fn === 'function' ? { before: fn } : fn
// 把订阅方法放入_actionSubscribers数组中
return genericSubscribe(subs, this._actionSubscribers, options)
}
dispatch (_type, _payload) {
const action = { type, payload }
const entry = this._actions[type]
...
// 执行before相关订阅
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)
// 返回一个promise
return new Promise((resolve, reject) => {
// 对于同步函数而已,因此在注册阶段就直接使用Promise.resolve处理能异步函数了,所以统一使用then获取执行结果
result.then(res => {
try {
// 执行after订阅者
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
}
...
resolve(res)
}, error => {
try {
// 执行error订阅者
this._actionSubscribers
.filter(sub => sub.error)
.forEach(sub => sub.error(action, this.state, error))
}
...
reject(error)
})
})
}
}
模块化
如果只使用单一状态树,应用的所有状态就会集中到一个比较大的对象,store
对象就有可能变得相当臃肿。Vuex
可以允许将 store
分割成模块(module
)。每个模块甚至是嵌套子模块拥有 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的状态
模块解析
在Vuex
实例创建时,会对传入的模块进行解析
export class Store {
constructor(options) {
this._modules = new ModuleCollection(options)
installModule(this, state, [], this._modules.root)
}
}
初始化ModuleCollection
时会对模块进行注册:
// src/module/module-collection.js
export default class ModuleCollection {
constructor (rawRootModule) {
this.register([], rawRootModule, false)
}
register (path, rawModule, runtime = true) {
// 将每个模块使用一个module对象来展示
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)
}
// 注册嵌套模块
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
}
最后形成一个便于Vuex
处理的对象:
this._modules = {
root: {
runtime: Boolean,
state: {},
_children: [],
_rawModule: {},
namespaced: Boolean
}
}
installModule
方法非常重要,里边会对嵌套module
进行解析,使得嵌套内部可以在调用dispatch
、commit
的时候能找到对应的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
,根模块的会根据模块的modules
字段中的key
作为索引将子模块中mutations
、actions
、getters
存储在根模块中。例如:mutations: {'childModule/module1', [fn]}
在调用
commit
或者dispatch
方法的时候就可以根据这个key
找到对应子模块中的mutation
跟action
如果子模块中没有设置namespaced: true
,那么在根模块中的_actions
等字段的key
就为对应的子模块中的actions
里定义的key
,如果模块非常多的时候,容易造成命名冲突。而vuex
也考虑到这种情况,因此key
对应的值的类型为数组,这样即使冲突了,也会执行数组中所有的方法,不过这样就会导致触发其他模块中非必要的action
、mutation
等。// 如两个子模块都存在一个increment的mutation,同时都没有设置 namespaced: true,那么在根模块的_mutations就为 _mutations: { 'increment', [fn1, fn2] } // 调用 store.commit('increment'),fn1, fn2都会执行
将子模块的state
添加到父模块的state
中,这样就可以通过store.state.childModule.state.xxx
进行访问。
为模块创建了一个执行上下文,当触发commit
或者dispatch
时就会根据namespaced
找到对应的模块中的mutations
或者actions
中的方法。
// src/store-utils.js installModule
// 为当前模块添加一个上下文
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 类似
}
...
return local
}