在上一篇笔记中:Vuex 4源码学习笔记 - Store 构造函数都干了什么(四)
我们通过查看Store 构造函数的源代码可以看到主要做了三件事情:
- 初始化一些内部变量以外
- 执行installModule来初始化module以及子module,
- 执行resetStoreState函数通过Vue3的reactive使store的状态实现“响应式”)。
今天我们我们通过官方的购物车示例来查看Vuex内部的工作流程,示例代码在examples/composition/shopping-cart
文件夹内
Vuex配置代码如下:
import { createStore, createLogger } from 'vuex'
import cart from './modules/cart'
import products from './modules/products'
const debug = process.env.NODE_ENV !== 'production'
export default createStore({
modules: {
cart,
products
},
strict: debug,
plugins: debug ? [createLogger()] : []
})
Vuex组件module中各模块state配置代码部分:
/**
cart.js
*/
const state = {
items: [],
checkoutStatus: null
}
/**
products.js
*/
const state = {
all: []
}
页面加载成功后可以看到state和getters
state和getters都是按照配置中module path的规则来划分的
然后我们看在ProductList.vue组件中,通过store.dispatch方法来调用vuex中actions,其他不重要的代码省略了。
<script>
import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
//...
store.dispatch('products/getAllProducts')
//...
}
}
</script>
这是products.js中的actions
const actions = {
getAllProducts ({ commit }) {
shop.getProducts(products => {
commit('setProducts', products)
})
}
}
第一步:store.dispatch将走到Store 构造函数内的this.dispatch函数
this.dispatch
重写了Store类的原型prototype上的dispatch方法,根据优先级规则,会优先找实例上的属性方法,再去找prototype原型上的属性方法。
/* 将dispatch与commit调用的this绑定为store对象本身*/
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
第二步:dispatch.call将走到Store 的原型方法dispatch
dispatch (_type, _payload) {
//...
}
在dispatch函数内:
- 第一,首先通过
unifyObjectStyle
函数来抹平成统一化的参数,因为actions和mutation都支持两种参数方式调用
// check object-style dispatch
// 抹平数据格式,dispatch调用可能是以载荷形式分发,也可能是以对象形式分发
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
// 建立action对象
const action = { type, payload }
两种传递参数形式:
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
- 第二,然后在
this._actions
中通过type
来找到对应的actions函数数组,然后通过if做了判断
// 在this._actions中寻找对应的action数组
const entry = this._actions[type]
if (!entry) {
if (__DEV__) {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
- 第三,之后的try/catch内的代码主要是用来执行action订阅器数组中的函数,比如在logger.js的插件中,我们就通过
store.subscribeAction
方法来订阅了action,那么在action运行时,就会执行我们订阅器函数。action订阅的状态主要有三种,分别为:before(action执行之前)、after(action执行之后)、error(action执行失败)。
try {
// 浅拷贝以防止迭代器失效,如果订阅者同步调用取消订阅
this._actionSubscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}
- 第四,然后就是运行actions函数,如果是多个actions使用Promise.all来处理,如果是单个就直接运行
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
- 第五,最后,action会返回一个promise,如果成功或者失败都会调用action订阅器,我们可以在插件中订阅它。
return new Promise((resolve, reject) => {
result.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
resolve(res)
}, error => {
try {
this._actionSubscribers
.filter(sub => sub.error)
.forEach(sub => sub.error(action, this.state, error))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in error action subscribers: `)
console.error(e)
}
}
reject(error)
})
})
- 第六步,在上面第四步调用的action函数为我们昨天看到的installModule函数中registerAction函数包装而成。
下面的wrappedActionHandler函数就是经过包装的函数,通过call设置handler函数内的this指向为store对象,然后传入一个带有当前模块的dispatch、commit、getters、state和根模块rootGetters、rootState的对象。
接着如果函数返回的不是Promise,用Promise.resolve进行了包装,因为上面的action函数需要promise调用。
function registerAction (store, type, handler, local) {
// 取出对应type的actions-handler集合
const entry = store._actions[type] || (store._actions[type] = [])
// 存储新的封装过的action-handler
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)
// action需要支持promise进行链式调用,这里进行兼容处理
if (!isPromise(res)) {
res = Promise.resolve(res)
}
// 开发者工具
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
- 第七步:上面的
handler.call
的handler
就是getAllProducts函数,函数的参数就是我们上面的对象,所以可以通过解构出commit供使用,在这个action中我们调用shop.getProducts
这个异步的操作来获取数据,在获取完成数据之后,通过commit来调用mutation来更改数据。
const actions = {
getAllProducts ({ commit }) {
shop.getProducts(products => {
commit('setProducts', products)
})
}
}
- 第八步:上面的commit调用mutation会走到products模块的commit方法中,因为它是一个子模块,主要通过
type = namespace + type
,将它的namespace和type进行拼接,拼接完成后调用store上的commit方法。
/**
* make localized dispatch, commit, getters and state
* if there is no namespace, just use root ones
* 生成局部的dispatch, commit, getters 和 state
* 如果没有命名空间,就使用根命名空间
*/
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ''
const local = {
//...
commit: noNamespace ? store.commit : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args // 这时type为:setProducts
if (!options || !options.root) {
type = namespace + type // 这时type为:products/setProducts
if (__DEV__ && !store._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
//...
return local
}
第九步:和action相同,commit方法同理
/* 将dispatch与commit调用的this绑定为store对象本身*/
const store = this
const { dispatch, commit } = this
//...
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
第十步:走到commit方法
commit (_type, _payload, _options) {
// ...
}
第十一步:和action相同,先抹平数据
// check object-style commit
// 抹平数据格式,mutation调用可能是以载荷形式分发,也可能是以对象形式分发
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
第十二步:在this._mutations中找到mutation函数的数组,然后是if判断,然后通过this._withCommit
来运行每个mutation,
// 建立mutation对象
const mutation = { type, payload }
// 根据type在this._mutations中寻找对应的mutation数组
const entry = this._mutations[type]
if (!entry) {
if (__DEV__) {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
_withCommit
是一个代理方法,所有触发mutation的进行state修改的操作都经过它,由此来统一管理监控state状态的修改。实现代码如下。
_withCommit (fn) {
// 保存之前的提交状态
const committing = this._committing
// 进行本次提交,若不设置为true,直接修改state,strict模式下,Vuex将会产生非法修改state的警告
this._committing = true
// 执行state的修改操作
fn()
// 修改完成,还原本次修改之前的状态
this._committing = committing
}
第十三步:与action相同,mutation也有个订阅器,可以在插件中进行订阅
this._subscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.forEach(sub => sub(mutation, this.state))
if (
__DEV__ &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
第十四步:上面_withCommit中运行的函数就是我们包装后的wrappedMutationHandler函数,和action差不多,也是绑定this,然后传入state,和payload
function registerMutation (store, type, handler, local) {
// 取出对应type的mutations-handler集合
const entry = store._mutations[type] || (store._mutations[type] = [])
// commit实际调用的不是我们传入的handler,而是经过封装的
entry.push(function wrappedMutationHandler (payload) {
// 调用handler并将state传入
handler.call(store, local.state, payload)
})
}
第十五步:上面的handler.call
的handler就是下面的mutations中的setProducts函数,在setProducts函数中我们修改了state中的all字段的值。
// mutations
const mutations = {
setProducts (state, products) {
state.all = products
},
//...
}
到这里state的值其实已经被修改了,那么页面是如何做到自动变更的呢?
这就要找到Store构造函数中的resetStoreState函数调用
export function resetStoreState (store, state, hot) {
//...
/* 这里使用Vue3的reactive来将state实现为响应式对象 */
store._state = reactive({
data: state
})
//...
}
重点在这里,Vuex将state通过Vue3的reactive函数将数据设置为响应式数据,所以在mutation中我们修改了state,由于其为reactive的数据,所以依赖它的页面或者组件也会自动变更。
今天我们通过dispatch的调用,一步步的看到Vuex的内部工作流程,这也是看源码的最好的方式,只要捋清楚大概主流程后,再去看那些细枝末节就容易多了。
一起学习更多前端知识,微信搜索【小帅的编程笔记】,每天更新