关于 async-preact-signals
为使用 Preact Signals 的开发者提供一种处理 Promise 异步数据的模式。它通过 asyncSignal 和状态类表示加载、数据与错误,并支持取消先前请求及映射状态。
主要功能
- 01以状态类表示加载、数据和错误
- 02通过 asyncSignal 创建异步信号
- 03依赖变化时取消先前请求
- 04将状态映射为其他值
适用场景
- 01在 Signals UI 中展示请求状态
- 02在依赖变化时更新异步数据
- 03复用多个请求的加载与错误处理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rodydavis/ skills --skill async-preact-signals安装后在 Claude Code 中输入/async-preact-signals确认已加载。
详细介绍
异步 Preact Signals
在 JavaScript 中使用 signals 时,经常需要处理来自 Promises 的异步数据。
异步与同步
但与其他状态管理库不同,signals 没有_异步_状态图,所有值都必须_同步_计算。
刚开始使用 signals 时,人们往往只想在函数回调中添加 async,但这会破坏其底层工作方式,并导致行为未定义。☹️
异步函数是一种有泄漏的抽象,会迫使你在整个状态图中处理异步。异步也并非总是更好,还可能带来性能影响。😬
使用 Promises
我们仍然可以用同步操作完成很多事情,并让常见异步模式更易于使用。
例如,使用 fetch 发起 http 请求时,你会希望返回 Promise 中的数据并更新一些 UI。
const el = document.querySelector('#output');
let postId = '123';
fetch(`/posts/${postId}`).then(res => res.json()).then(post => {
el.innerText = post.title;
})
现在添加 signals 后,每当 post id 发生变化,我们都可以重新运行 fetch。
import { effect, signal } from "@preact/signals-core";
const el = document.querySelector('#output');
const postId = signal( '123');
effect(() => {
fetch(`/posts/${postId.value}`).then(res => res.json()).then(post => {
el.innerText = post.title;
});
});
这样更好了,但现在我们需要处理一种情况:如果前一个 fetch 尚未完成,post id 就发生了变化,我们需要停止前一个请求。
import { effect, signal } from "@preact/signals-core";
const el = document.querySelector('#output');
const postId = signal( '123');
let controller;
effect(() => {
if (controller) {
controller.abort();
}
controller = new AbortController();
const signal = controller.signal;
try {
fetch(`/posts/${postId.value}`, { signal }).then(res => res.json()).then(post => {
el.innerText = post.title;
});
} catch (err) {
// todo: show error message
}
});
不过,这仍然跳过了许多我们通常希望展示的内容,例如加载状态和错误状态。
import { effect, signal, batch } from "@preact/signals-core";
const el = document.querySelector('#output');
const postId = signal( '123');
const postData = signal({});
const errorMessage = signal('');
const loading = signal(false);
let controller;
effect(() => {
if (controller) {
controller.abort();
}
controller = new AbortController();
const signal = controller.signal;
batch(() => {
loading.value = true;
errorMessage.value = '';
postData.value = {};
});
try {
fetch(`/posts/${postId.value}`, { signal }).then(res => res.json()).then(post => {
batch(() => {
postData.value = post;
loading.value = false;
});
});
} catch (err) {
errorMessage.value = err.message;
}
});
effect(() => {
if (loading.value) {
el.innerText = 'Loading...';
} else if (errorMessage.value) {
el.innerText = `Error: ${errorMessage.value}`;
} else {
el.innerText = postData.value.title;
}
});
现在我们可以展示恰当的状态了,但这只适用于一个请求……
我们可以将这些逻辑封装到一个类中以便复用,或者创建一种能够处理异步数据的新 signal 类型。
AsyncState
我们想要一个基类,让加载状态可以轻松继承它:
export class AsyncState<T> {
constructor() {}
get value(): T | null {
return null;
}
get requireValue(): T {
throw new Error("Value not set");
}
get error(): any {
return null;
}
get isLoading(): boolean {
return false;
}
get hasValue(): boolean {
return false;
}
get hasError(): boolean {
return false;
}
map<R>(builders: {
onLoading: () => R;
onError: (error: any) => R;
onData: (data: T) => R;
}): R {
if (this.hasError) {
return builders.onError(this.error);
}
if (this.hasValue) {
return builders.onData(this.requireValue);
}
return builders.onLoading();
}
}
这个类实际上来自我创建的 preact signals 的 Dart 移植版。
这样我们就可以轻松检查是否有实际值、错误或是否正在加载。它还提供了一个简单的构建器方法,可以将状态映射为另一个值。🤩
AsyncData
加载状态继承 AsyncState,并在构造函数中传入值,以供重写的方法使用。
export class AsyncData<T> extends AsyncState<T> {
private _value: T;
constructor(value: T) {
super();
this._value = value;
}
get requireValue(): T {
return this._value;
}
get hasValue(): boolean {
return true;
}
toString() {
return `AsyncData{${this._value}}`;
}
}
AsyncLoading
对于加载状态,我们像 AsyncData 一样重写这些方法。
export class AsyncLoading<T> extends AsyncState<T> {
get value(): T | null {
return null;
}
get isLoading(): boolean {
return true;
}
toString() {
return `AsyncLoading{}`;
}
}
AsyncError
对于错误状态,我们可以传入任意类型的对象,将错误作为值返回,而不是抛出异常(类似 Go)。
export class AsyncError<T> extends AsyncState<T> {
private _error: any;
constructor(error: any) {
super();
this._error = error;
}
get error(): any {
return this._error;
}
get hasError(): boolean {
return true;
}
toString() {
return `AsyncError{${this._error}}`;
}
}
asyncSignal
现在我们创建好了这些状态类,就可以创建一个函数来创建异步 signal,并包含前面讨论的所有逻辑。
我们需要随时展示同步值,并且能够中止之前的请求。
export function asyncSignal<T>(
cb: () => Promise<T>
): ReadonlySignal<AsyncState<T>> {
const loading = new AsyncLoading<T>();
const reset = Symbol("reset");
const s = signal<AsyncState<T>>(loading);
const c = computed<Promise<T>>(cb);
let controller: AbortController | null;
let abortSignal: AbortSignal | null;
function execute(cb: Promise<T>, cancel: AbortSignal) {
(async () => {
s.value = loading;
try {
const result = await new Promise<T>(async (resolve, reject) => {
if (cancel.aborted) {
reject(cancel.reason);
}
cancel.addEventListener("abort", () => {
reject(cancel.reason);
});
try {
const result = await cb;
if (cancel.aborted) {
reject(cancel.reason);
return;
}
resolve(result);
} catch (error) {
reject(error);
}
});
s.value = new AsyncData<T>(result);
} catch (error) {
if (error === reset) {
s.value = loading;
} else {
s.value = new AsyncError<T>(error);
}
}
})();
}
effect(() => {
if (controller != null) {
controller.abort(reset);
}
controller = new AbortController();
abortSignal = controller.signal;
execute(c.value, abortSignal);
});
return s;
}
这样就可以非常轻松地创建多个异步 signals,也可以在应用中其他使用 signals 的地方使用,例如 effects 和 computeds。
const el = document.querySelector('#output');
const postId = signal('123');
const result = asyncSignal(() => fetch(`/posts/${postId.value}`).then(res => res.json()));
effect(() => {
el.innerText = result.value.map({
onLoading: () => 'Loading...',
onError: (err) => `Error: ${err}`,
onData: (post) => post.title,
});
});
postId.value = '456';
总结
我已经在 这里发起了 Preact Signals 的 GitHub 讨论,你也可以在这里找到最终的源代码 gist。🎉
这让处理异步数据变得轻松了许多,也很期待听到你对如何改进它的想法 👀
如果你还想了解 Angular 如何处理异步 signals,可以查看 resource signal 和 computedFrom/Async signal。