Skip to content

异步计算

引言

AutoStore提供了非常强大的异步计算属性特性,使用computedasyncComputed来声明创建一个异步计算属性。

异步计算有两种,分别使用computedasyncComputed声明。

工作原理

创建异步计算属性的基本方法是直接在State中任意位置使用computedasyncComputed进行声明。

  • computed: 声明简单计算属性
  • asyncComputed: 声明增强计算属性
tsx
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(相对路径依赖,见依赖收集)。
loading

重点:

当创建异步计算属性,内部主要做了两件事:

  • 1. 原地替换为计算值

经过AutoStore处理后,store.state.order.total2的值会被替换为AsyncComputedValue类型的值,即:

json
{
    "loading": false,
    "timeout": 0,
    "retry": 0,
    "error": null,
    "value": 10,
    "progress": 0
}

当异步计算的依赖发生变化时,会自动触发计算属性的重新计算,并更新value以及loadingerrorprogress等状态。详见下文高级特性。 经过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的简单示例:

loading
  • 以上fullName是一个异步计算属性,手动指定其依赖于user.firstName./lastName(相对路径)。
  • 依赖可以使用绝对路径或相对路径,使用.作为路径分割符,./指的是当前对象,../指的是父对象,详见依赖收集
  • 当在输入框架中修改firstNamelastName时,fullName会自动重新计算。
  • 计算属性的结果保存在state.user.fullName.value中。
  • 当计算属性正在计算时,state.user.fullName.loadingtrue。计算完成后,state.user.fullName.loadingfalse

提示

本示例使用了@autostorejs/react,该库是对AutoStore的简单封装,工作原理是一样的。

指南

computed

computed是一个普通的函数,即可以用于声明同步计算属性,也可以声明异步计算属性,异步计算属性的函数签名如下:

ts
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的计算属性具有加载状态、执行进度、超时控制、倒计时、重试、可取消等高级特性。

asyncComputedcomputed的函数签名基本一致,差别在于使用asyncComputed时,getter函数参数中缺少相关的高级特性控制参数,如abortSignal

异步计算函数

getter参数(即异步计算函数),其返回值将更新到状态中的computed声明的路径上,详见介绍

指定依赖

  • depends:依赖收集,用来指定依赖的状态路径。如何指定依赖详见依赖收集
  • options:异步计算属性的一些选项,详见选项

重点

不同于同步计算属性,同步计算属性可以通过执行一次来自动收集依赖,但是异步计算属性必须显式指定依赖

简写异步计算

大部份情况下,异步计算属性均应该使用computedasyncComputed进行声明,但也可以直接使用一个异步函数。

ts
const order = {
    bookName: "ZhangFisher",
    price: 100,
    count: 3,
    total: async (order) => {
        return order.price * order.count;
    },
};

上述简单的异步声明方式等效于以下方式:

tsx
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所在的对象。
  • 其依赖是空,所以不会自动收集依赖,也不会自动重新计算。也就是说上例中的pricecount变化时,total不会自动重新计算。但是在会在第一次访问时自动计算一次。
  • 如果需要重新计算,可以手动执行store.state.total.run()store.computedObjects.get(<id>).run()

初始值

异步计算属性可以通过initial参数指定初始值。

ts
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 在创建异步计算时不马上执行一次,后续仅在依赖变化时执行

结果响应式

默认情况下,计算结果也会进行响应式处理,例如:

ts
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包裹bookbook将不再Proxy,也就无法store.watch("book.name")来监听变化,这有助于提高性能。

ts
const store = new AutoStore({
    book: computed(
        () => {
            return {
                name: "AutoStore",
                price: 100,
            };
        },
        [],
        {
            raw: true,
        },
    ),
});
//等效于 markRaw(store.state.book)

计算报告

增强性异步计算属性具备丰富的重试超时加载中错误等高级特性,而简单异步计算属性不支持此特性,但是提供计算过程报告功能。

通过options.reports可以在计算前后提供加载过程的报告。

ts
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": 同样,当异步计算出错时,

提示

  • loadingerror用于指定一个状态路径,在异步计算过程中进行状态汇报。状态路径可以使用相对路径或绝对路径,参考依赖收集中的路径
  • 只有简单异步计算属性才支持此特性,增强异步计算属性不支持。

高级特性🔥

提示

高级特性适用于使用asyncComputed声明的增强异步计算属性。

加载状态

异步计算属性的加载状态保存在AsyncComputedValue对象的loading属性中。

  • loading=true时,代表异步计算正在进行中。
  • loading=false时,代表异步计算已经完成。

以下是一个异步计算加载状态的例子:

loading
  • useAsyncReactive用来返回异步计算属性的状态数据。

执行进度

异步计算属性允许控制计算的进度,执行进度保存在AsyncComputedValue对象的progress属性中,当progress0-100时,代表异步计算的进度。开发者可以根据进度值来展示进度条等。

使用方法如下:

loading
  • 当调用getProgressbar函数时会启动进度条功能,可以控制进度条的进度。
  • getProgressbar函数返回一个进度条对象,该对象有两个方法:valueendvalue用来设置进度值,end用来结束进度条。

超时处理

在创建computed时可以指定超时参数(单位为ms),实现超时处理倒计时功能。基本过程是这样的。

  1. 指定options.timeout=超时时间
  2. 当异步计算开始时,会启动一个定时器时,并更新AsyncComputedValue对象的timeout属性。
  3. 当超时触发时会触发TIMEOUT错误,将错误更新到AsyncComputedValue.error属性中。
loading

倒计时

超时功能中不会自动更新timeout属性,可以通过timeout=[超时时间,间隔更新时长]来启用倒计时功能。

基本过程如下:

  1. 指定options.timoeut=[超时时间,间隔更新时长]
  2. 当异步计算开始时,会启动一个定时器,更新AsyncComputedValue对象的timeout属性。
  3. 然后每隔间隔更新时长就更新一次AsyncComputedValue.timoeut
  4. 当超时触发时会触发TIMEOUT错误,将错误更新到AsyncComputedValue.error属性中。

例如:options.timoeut=[5*1000,5]代表超时时间为5秒,每1000ms更新一次timeout属性,倒计时5次。

loading

重试

在创建computed时可以指定重试参数,实现出错重试执行的功能。基本过程是这样的。

  • 指定options.retry=[重试次数,重试间隔ms]
  • 当开始执行异步计算前,会更新AsyncComputedValue.retry属性。
  • 当执行出错时,会同步更新AsyncComputedValue.retry属性为重试次数。
loading

说明

  • 重试次数为0时,不会再次重试。重试次数为N时,实际会执行N+1次。
  • 重试期间error会更新为最后一次错误信息。

取消

在创建computed时可以传入一个abortSignal参数,该参数返回一个AbortSignal,用来取消计算操作。

基本操作方法是:

  • computed中传入abortSignal参数,该参数是一个AbortSignal,可用来订阅abort信号或者传递给fetchaxios等。
  • 取消时可以调用AsyncComputedObject.cancel()方法来触发一个AbortSignal信号。如下例中调用state.order.total.cancel()
loading

注意

  • abortSignal参数是一个AbortSignal对象,可以用来订阅abort信号或者传递给fetchaxios等。
  • 需要注意的,如果想让计算函数是可取消的,则当调用AsyncComputedObject.cancel()时,计算函数应该在接收到abortSignal信号时,主动结束退出计算函数。如果计算函数没有订阅abort信号,调用AsyncComputedObject.cancel()是不会生效的。

不可重入

默认情况下,每当依赖发生变化时均会执行异步计算函数,在连续变化时就会重复执行异步计算函数。

在声明时,允许指定options.reentry=false来防止重入,如果重入则只会在控制台显示一个警告。

extras

extras选项用于为计算属性提供额外参数,可以在Getter函数为获取到该值。

ts
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

ts
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}),这样就可以正确识别为异步函数。