异步计算
引言
AutoStore提供了非常强大的异步计算属性特性,使用computed或asyncComputed来声明创建一个异步计算属性。
异步计算有两种,分别使用computed和asyncComputed声明。
工作原理
创建异步计算属性的基本方法是直接在State中任意位置使用computed或asyncComputed进行声明。
computed: 声明简单计算属性asyncComputed: 声明增强计算属性
import { AutoStore, asyncComputed, computed } from "autostore";
const store = new AutoStore({
order: {
price: 10,
count: 1,
// 简单计算属性
total: computed(
async (scope) => {
return scope.price * scope.count;
},
["./price", "./count"],
),
// 增强型计算属性
total2: asyncComputed(
async (scope) => {
return scope.price * scope.count;
},
["./price", "./count"],
),
},
});- 以上
total是一个简单步计算属性,total2是一个增强异步计算属性,并且手动指定依赖了./price和./count(相对路径依赖,见依赖收集)。
重点:
当创建异步计算属性,内部主要做了两件事:
- 1. 原地替换为计算值
经过AutoStore处理后,store.state.order.total2的值会被替换为AsyncComputedValue类型的值,即:
{
"loading": false,
"timeout": 0,
"retry": 0,
"error": null,
"value": 10,
"progress": 0
}当异步计算的依赖发生变化时,会自动触发计算属性的重新计算,并更新value以及loading、error、progress等状态。详见下文高级特性。
经过AutoStore处理后,store.state.order.total的值会被替换为AsyncComputedValue类型的值,即:
如果使用computed声明异步计算,则store.state.order.total=10。
- 2. 创建
AsyncComputedObject对象
同时会创建一个名称为声明所在路径名称的AsyncComputedObject对象保存在store.computedObjects中。
因此,在上例中,store.computedObjects.get("order.total")就是AsyncComputedObject对象。
异步计算属性的创建与同步计算一样均是使用computed来声明,但是最重要的一点是异步计算需要显式指定依赖。
以下是一个使用asyncComputed的简单示例:
- 以上
fullName是一个异步计算属性,手动指定其依赖于user.firstName和./lastName(相对路径)。 - 依赖可以使用绝对路径或相对路径,使用
.作为路径分割符,./指的是当前对象,../指的是父对象,详见依赖收集。 - 当在输入框架中修改
firstName或lastName时,fullName会自动重新计算。 - 计算属性的结果保存在
state.user.fullName.value中。 - 当计算属性正在计算时,
state.user.fullName.loading为true。计算完成后,state.user.fullName.loading为false
提示
本示例使用了@autostorejs/react,该库是对AutoStore的简单封装,工作原理是一样的。
指南
computed
computed是一个普通的函数,即可以用于声明同步计算属性,也可以声明异步计算属性,异步计算属性的函数签名如下:
function computed<Value = any, Scope = any>(
getter: AsyncComputedGetter<Value, Scope>,
depends: ComputedDepends,
options?: ComputedOptions<Value, Scope>,
): ComputedDescriptorBuilder<Value, Scope>;参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
getter | AsyncComputedGetter | 异步计算函数 |
depends | ComputedDepends | 声明依赖 |
options | ComputedOptions | 异步计算属性相关参数 |
asyncComputed
asyncComputed用于声明增强计算属性,使用asyncComputed的计算属性具有加载状态、执行进度、超时控制、倒计时、重试、可取消等高级特性。
asyncComputed和computed的函数签名基本一致,差别在于使用asyncComputed时,getter函数参数中缺少相关的高级特性控制参数,如abortSignal
异步计算函数
getter参数(即异步计算函数),其返回值将更新到状态中的computed声明的路径上,详见介绍。
指定依赖
重点
不同于同步计算属性,同步计算属性可以通过执行一次来自动收集依赖,但是异步计算属性必须显式指定依赖。
简写异步计算
大部份情况下,异步计算属性均应该使用computed或asyncComputed进行声明,但也可以直接使用一个异步函数。
const order = {
bookName: "ZhangFisher",
price: 100,
count: 3,
total: async (order) => {
return order.price * order.count;
},
};上述简单的异步声明方式等效于以下方式:
import { AutoStore, computed } from "autostore";
const store = new AutoStore({
bookName: "ZhangFisher",
price: 100,
count: 3,
total: computed(async (order) => {
return order.price * order.count;
}, []), // 依赖是空的
});当不使用computed进行异步计算属性声明时,需要注意以下几点:
- 默认
scope指向的是current,即total所在的对象。 - 其依赖是空,所以不会自动收集依赖,也不会自动重新计算。也就是说上例中的
price和count变化时,total不会自动重新计算。但是在会在第一次访问时自动计算一次。 - 如果需要重新计算,可以手动执行
store.state.total.run()或store.computedObjects.get(<id>).run()。
初始值
异步计算属性可以通过initial参数指定初始值。
const store = new AutoStore({
bookName: "ZhangFisher",
price: 100,
count: 3,
total: computed(
async (order) => {
return order.price * order.count;
},
["./count", "./price"],
{
initial: 300, // 初始值
},
),
});初次计算
同步计算属性会在初始化时自动运行用于收集依赖,而异步计算属性则通过immediate来控制如何进行初次计算。
| 名称 | 说明 |
|---|---|
auto | 当提供initial初始值时不会马上执行,如果initial==undefined时会马上执行一次 |
true | 创建异步计算时马上执行一次 |
false | 在创建异步计算时不马上执行一次,后续仅在依赖变化时执行 |
结果响应式
默认情况下,计算结果也会进行响应式处理,例如:
const store = new AutoStore({
book: computed(() => {
return {
name: "AutoStore",
price: 100,
};
}),
});计算属性book也是一个响应式对象,即通过Proxy代理,允许通过store.watch("book.name")来监听变化,通过store.state.book.name="xxx"更新状态时也会触发事件。
在某些情况下,这种行为可能是不必要的,特别在如果计算结果是一个大型对象时,可能无需代理整个对象时能提高性能。此时可以显式指定raw=true来禁用此行为。
通过显式指定raw=true,可以标识为非响应式。启用用会使用markRaw包裹book,book将不再Proxy,也就无法store.watch("book.name")来监听变化,这有助于提高性能。
const store = new AutoStore({
book: computed(
() => {
return {
name: "AutoStore",
price: 100,
};
},
[],
{
raw: true,
},
),
});
//等效于 markRaw(store.state.book)计算报告
增强性异步计算属性具备丰富的重试、超时、加载中、错误等高级特性,而简单异步计算属性不支持此特性,但是提供计算过程报告功能。
通过options.reports可以在计算前后提供加载过程的报告。
const store = new AutoStore({
order: {
price: 100,
count: 20,
loading: false as boolean,
error: null as null | string,
total: computed(
async (order: any) => {
await delay();
return order.count * order.price;
},
["./count"],
{
reports: {
loading: "./loading",
error: "./error",
},
},
),
},
});上例中,我们创建了一个简单异步计算属性,并且指定了reports参数。
loading="./loading":代表执行异步计算函数时会更新order.loading=true/false,通过store.watch("order.loading")就可以获取异步计算状态。error="./error": 同样,当异步计算出错时,
提示
loading和error用于指定一个状态路径,在异步计算过程中进行状态汇报。状态路径可以使用相对路径或绝对路径,参考依赖收集中的路径- 只有简单异步计算属性才支持此特性,增强异步计算属性不支持。
高级特性🔥
提示
高级特性适用于使用asyncComputed声明的增强异步计算属性。
加载状态
异步计算属性的加载状态保存在AsyncComputedValue对象的loading属性中。
- 当
loading=true时,代表异步计算正在进行中。 - 当
loading=false时,代表异步计算已经完成。
以下是一个异步计算加载状态的例子:
useAsyncReactive用来返回异步计算属性的状态数据。
执行进度
异步计算属性允许控制计算的进度,执行进度保存在AsyncComputedValue对象的progress属性中,当progress为0-100时,代表异步计算的进度。开发者可以根据进度值来展示进度条等。
使用方法如下:
- 当调用
getProgressbar函数时会启动进度条功能,可以控制进度条的进度。 getProgressbar函数返回一个进度条对象,该对象有两个方法:value和end,value用来设置进度值,end用来结束进度条。
超时处理
在创建computed时可以指定超时参数(单位为ms),实现超时处理和倒计时功能。基本过程是这样的。
- 指定
options.timeout=超时时间 - 当异步计算开始时,会启动一个定时器时,并更新
AsyncComputedValue对象的timeout属性。 - 当超时触发时会触发
TIMEOUT错误,将错误更新到AsyncComputedValue.error属性中。
倒计时
在超时功能中不会自动更新timeout属性,可以通过timeout=[超时时间,间隔更新时长]来启用倒计时功能。
基本过程如下:
- 指定
options.timoeut=[超时时间,间隔更新时长] - 当异步计算开始时,会启动一个定时器,更新
AsyncComputedValue对象的timeout属性。 - 然后每隔
间隔更新时长就更新一次AsyncComputedValue.timoeut - 当超时触发时会触发
TIMEOUT错误,将错误更新到AsyncComputedValue.error属性中。
例如:options.timoeut=[5*1000,5]代表超时时间为5秒,每1000ms更新一次timeout属性,倒计时5次。
重试
在创建computed时可以指定重试参数,实现出错重试执行的功能。基本过程是这样的。
- 指定
options.retry=[重试次数,重试间隔ms] - 当开始执行异步计算前,会更新
AsyncComputedValue.retry属性。 - 当执行出错时,会同步更新
AsyncComputedValue.retry属性为重试次数。
说明
- 重试次数为
0时,不会再次重试。重试次数为N时,实际会执行N+1次。 - 重试期间
error会更新为最后一次错误信息。
取消
在创建computed时可以传入一个abortSignal参数,该参数返回一个AbortSignal,用来取消计算操作。
基本操作方法是:
- 在
computed中传入abortSignal参数,该参数是一个AbortSignal,可用来订阅abort信号或者传递给fetch或axios等。 - 取消时可以调用
AsyncComputedObject.cancel()方法来触发一个AbortSignal信号。如下例中调用state.order.total.cancel()
注意:
abortSignal参数是一个AbortSignal对象,可以用来订阅abort信号或者传递给fetch或axios等。- 需要注意的,如果想让计算函数是可取消的,则当调用
AsyncComputedObject.cancel()时,计算函数应该在接收到abortSignal信号时,主动结束退出计算函数。如果计算函数没有订阅abort信号,调用AsyncComputedObject.cancel()是不会生效的。
不可重入
默认情况下,每当依赖发生变化时均会执行异步计算函数,在连续变化时就会重复执行异步计算函数。
在声明时,允许指定options.reentry=false来防止重入,如果重入则只会在控制台显示一个警告。
extras
extras选项用于为计算属性提供额外参数,可以在Getter函数为获取到该值。
const store = new AutoStore({
firstName: "Zhang",
lastName: "Fisher",
fullName: computed(
async (user, { extras }) => {
console.log(extras); // 100
},
["user.firstName", "user.lastName"],
{
extras: 100,
},
),
});
const obj = store.computedObjects.get("fullName");
// 手动执行计算属性时可以传入该值
obj.run({ extras: 200 });注意事项
- 当异步计算函数返回一个
Promise时的问题
computed内部使用isAsync来判断传入的getter函数是否是一个异步函数,以采取不同的处理逻辑。
但是在低版本JS场景下,这个判断可能不正确。
比如在进行babel将代码转译到es5等低版本代码时,异步函数可能会被转译为同步函数,此时需要显式指定options.async=true。
const store = new AutoStore({
firstName: "Zhang",
lastName: "Fisher",
fullName: computed(
async (user) => {
return user.firstName + user.lastName;
},
["user.firstName", "user.lastName"],
{
async: true,
},
),
});显式指定computed(async ()=>{...},[...],{async:true}),这样就可以正确识别为异步函数。