Pinia中实现监听action的原理
# 概述
在vue3 + Pinia项目中,可以监听store中定义的每个action执行。
# 正文
# 示例介绍
通过一个示例介绍如何监听store中的action。
1、定义一个userStore,如下:
const useUserStore = defineStore('userStore',{
state:()=>{
return {
name:'',
}
}
actions:{
setName(name){
this.name=name;
return true;
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
userStore中的state包含一个name属性,以及一个action``setName方法用于设置name。
2、在组件中监听setName:
const userStore = useUserStore();
useStore.$onAction(({name,after,store,onError,args})=>{
if(name == 'setName'){
after((arg)=>{
if(arg){
console.log('set name success!')
}
})
}
})
// 执行action
userStore.setName('Zhang San')
2
3
4
5
6
7
8
9
10
11
12
13
14
在组件中通过$onAction方法监听,$onAction方法接收一个回调函数callback作为参数,callback有四个参数,
name:actions中的属性名,即方法名称after:在action调用完成后会触发,其arg参数为action方法的返回值store:即userStore,onError:在action调用时,若出现错误,则会触发args:表示action方法被调用时的参数数组
# 原理解析
# 订阅事件$onAction
store.$onAction(callback)可以理解成订阅store中的所有action事件,当actions中的事件被触发时,都会执行callback回调,而用name可以区分是哪一个方法被触发。
在Pinia中的源码中,使用defineStore定义store时,store就定义了$onAction属性,如下:
const partialStore = {
$onAction: addSubscription.bind(null,actionSubscriptions)
}
const store = vueDemi.reactive(partialStore);
return store;
2
3
4
5
6
7
所以在vue组件中store.$onAction(callback),就相当于执行addSubscription(actionSubscriptions,callback),而addSubscription的实现如下:
const noop = () => { };
function addSubscription(subscriptions, callback, detached, onCleanup = noop) {
subscriptions.push(callback);
const removeSubscription = () => {
const idx = subscriptions.indexOf(callback);
if (idx > -1) {
subscriptions.splice(idx, 1);
onCleanup();
}
};
if (!detached && vueDemi.getCurrentScope()) {
vueDemi.onScopeDispose(removeSubscription);
}
return removeSubscription;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
addSubscription方法就是将监听action的回调callback推入到actionSubscriptions数组中,还定义了清除监听的函数removeSubscription,并且若参数detached不存在或为false,则在作用域销毁时清理,最后返回清除函数。
即每个store中有一个数组actionSubscriptions变量用于存放回调函数callback,当action被触发时就会触发回调。但是,在store的actions中定义事件时,就是很简单地写一个对象的属性方法。这是因为Pinia给这些属性方法重新包裹了一层。
# actions属性方法包装
Pinia会给每一个actions的属性方法进行一层包装,在Pinia中定义了一个action函数,其实现如下:
const action = (fn, name = '') => {
// fn:actions中的属性值
// name:actions中的属性名(即后面用于区分的方法名称)
if (ACTION_MARKER in fn) {
// 判断fn方法是否被标记过,若被标记过,则给其赋值,并返回
fn[ACTION_NAME] = name;
return fn;
}
// 定义`wrappedAction`方法
const wrappedAction = function () {
setActivePinia(pinia);
const args = Array.from(arguments);
const afterCallbackList = [];
const onErrorCallbackList = [];
function after(callback) {
afterCallbackList.push(callback);
}
function onError(callback) {
onErrorCallbackList.push(callback);
}
triggerSubscriptions(actionSubscriptions, {
args,
name: wrappedAction[ACTION_NAME],
store,
after,
onError,
});
let ret;
try {
ret = fn.apply(this && this.$id === $id ? this : store, args);
}
catch (error) {
triggerSubscriptions(onErrorCallbackList, error);
throw error;
}
if (ret instanceof Promise) {
return ret
.then((value) => {
triggerSubscriptions(afterCallbackList, value);
return value;
})
.catch((error) => {
triggerSubscriptions(onErrorCallbackList, error);
return Promise.reject(error);
});
}
triggerSubscriptions(afterCallbackList, ret);
return ret;
};
// 标记方法,表示方法已经被包装过了
wrappedAction[ACTION_MARKER] = true;
// 赋值name,用于在wrappedAction中获取name
wrappedAction[ACTION_NAME] = name;
// 最后返回`wrappedAction`
return wrappedAction;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
store中的actions属性对象,会进行for...in遍历,调用action方法进行包装,在action中定义了wrappedAction方法,用于封装属性方法,最后将其返回。
因此在如上组件中调用userStore.setName实际上执行的不是我们所定义的那个setName属性方法,而是经过action封装后的方法。
# actions属性方法的执行
本质上就是执行wrappedAction,其流程如下:
(1)在wrappedAction方法中定义了两个数组:
afterCallbackList:fn执行完后需要执行的事件队列onErrorCallbackList:若fn的返回值是Promise对象且运行报错后,需要执行的错误事件队列
并且在wrappedAction中还定义了after和onError方法,分别用于将对应的事件放到队列中。然后会调用triggerSubscriptions方法
function triggerSubscriptions(subscriptions, ...args) {
subscriptions.slice().forEach((callback) => {
callback(...args);
});
}
2
3
4
5
(2)triggerSubscriptions方法会去逐个遍历actionSubscriptions数组中的callback,进行调用,即执行store.$onAction(callback)中的callback。callback中的参数after或onError实际上就是在wrappedAction中定义的两个方法,因此在执行过程中,若callback中有after(cb)或onError(cb),则会将cb回调存放对应的队列数组中,以便在fn执行完后使用。
(3)在try...catch中通过apply执行fn方法(即在store的actions中定义的属性方法)得到其返回值ret,
(4)若catch捕获到错误,则调用triggerSubscriptions(onErrorCallbackList, error)执行onErrorCallbackList队列中的事件,并返回错误;
(5)若ret是一个Promise对象,则执行它,当它被resolve时,调用triggerSubscriptions(afterCallbackList, value),即执行afterCallbackList队列中的事件;若它被reject,则执行onErrorCallbackList队列中的事件;
(6)否则调用triggerSubscriptions执行afterCallbackList队列中的事件
(7)最后返回ret
# 总结
store.$onAction的监听原理本质上还是订阅-触发,$onAction就是将整个callback存放在store的actionSubscriptions队列中,而actions中所有的属性方法在定义store时都会被action方法重新包装成wrappedAction;执行wrappedAction时,就会先触发actionSubscriptions中的callback监听,在这个过程中若存在after(cb)或onError(cb),则会将cb放到afterCallbackList或onErrorCallbackList队列中;然后执行fn,在合适的时机调用cb。
- 02
- patch中的双端比较快速算法09-25
- 03
- patchDOM元素的更新解析09-25