sync-construction-async-property-ui-render-gate-pattern
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSync Construction, Async Property
同步构造,异步属性
The initialization of the client is synchronous. The async work is stored as a property you can await, while passing the reference around.
客户端的初始化是同步的。异步操作被存储为一个可等待的属性,同时可以传递对象引用。
When to Apply This Pattern
何时应用此模式
Use this when you have:
- Async client initialization (IndexedDB, server connection, file system)
- Module exports that need to be importable without
await - UI components that want sync access to the client
- SvelteKit apps where you want to gate rendering on readiness
Signals you're fighting async construction:
- patterns everywhere
await getX() - Top-level await complaints from bundlers
- Getter functions wrapping singleton access
- Components that can't import a client directly
当你遇到以下场景时可以使用该模式:
- 客户端需要异步初始化(如IndexedDB、服务器连接、文件系统)
- 模块导出的内容无需即可导入
await - UI组件需要同步访问客户端
- 在SvelteKit应用中,希望在准备就绪后再开始渲染
以下信号表明你正在与异步构造作斗争:
- 到处都是的模式
await getX() - 打包工具对顶层报错
await - 使用 getter 函数封装单例访问
- 组件无法直接导入客户端
The Problem
存在的问题
Async constructors can't be exported:
typescript
// This doesn't work
export const client = await createClient(); // Top-level await breaks bundlersSo you end up with getter patterns:
typescript
let client: Client | null = null;
export async function getClient() {
if (!client) {
client = await createClient();
}
return client;
}
// Every consumer must await
const client = await getClient();Every call site needs . You're passing promises around instead of objects.
await异步构造函数无法直接导出:
typescript
// 这种写法无法正常工作
export const client = await createClient(); // 顶层await会导致打包工具出错于是你最终会使用 getter 模式:
typescript
let client: Client | null = null;
export async function getClient() {
if (!client) {
client = await createClient();
}
return client;
}
// 所有使用方都必须添加await
const client = await getClient();每个调用位置都需要,你传递的是Promise而非对象。
awaitThe Pattern
解决方案(模式内容)
Make construction synchronous. Attach async work to the object:
typescript
// client.ts
export const client = createClient();
// Sync access works immediately
client.save(data);
client.load(id);
// Await the async work when you need to
await client.whenSynced;Construction returns immediately. The async initialization (loading from disk, connecting to servers) happens in the background and is tracked via .
whenSynced让构造过程同步进行,将异步操作附加到对象上:
typescript
// client.ts
export const client = createClient();
// 可立即进行同步访问
client.save(data);
client.load(id);
// 需要时再等待异步操作完成
await client.whenSynced;构造函数会立即返回。异步初始化操作(从磁盘加载、连接服务器等)在后台执行,并通过属性跟踪状态。
whenSyncedThe UI Render Gate
UI渲染闸门
In Svelte, await once at the root:
svelte
<!-- +layout.svelte -->
<script>
import { client } from '$lib/client';
</script>
{#await client.whenSynced}
<LoadingSpinner />
{:then}
{@render children?.()}
{/await}The gate guarantees: by the time any child component's script runs, the async work is complete. Children use sync access without checking readiness.
在Svelte中,只需在根节点处等待一次:
svelte
<!-- +layout.svelte -->
<script>
import { client } from '$lib/client';
</script>
{#await client.whenSynced}
<LoadingSpinner />
{:then}
{@render children?.()}
{/await}这个闸门可以保证:当任何子组件的脚本运行时,异步操作已经完成。子组件可以直接同步访问客户端,无需检查是否就绪。
Implementation
实现方式
The fluent builder attaches async work to a sync-constructed object:
withCapabilities()typescript
function createClient() {
const state = initializeSyncState();
return {
save(data) {
/* sync method */
},
load(id) {
/* sync method */
},
withCapabilities({ persistence }) {
const whenSynced = persistence(state);
return Object.assign(this, { whenSynced });
},
};
}
// Usage
export const client = createClient().withCapabilities({
persistence: (state) => loadFromIndexedDB(state),
});使用流畅构建器将异步操作附加到同步构造的对象上:
withCapabilities()typescript
function createClient() {
const state = initializeSyncState();
return {
save(data) {
/* 同步方法 */
},
load(id) {
/* 同步方法 */
},
withCapabilities({ persistence }) {
const whenSynced = persistence(state);
return Object.assign(this, { whenSynced });
},
};
}
// 使用示例
export const client = createClient().withCapabilities({
persistence: (state) => loadFromIndexedDB(state),
});Before and After
前后对比
| Aspect | Async Construction | Sync + whenSynced |
|---|---|---|
| Module export | Can't export directly | Export the object |
| Consumer code | | Direct import, sync use |
| UI integration | Awkward promise handling | Single |
| Type signature | | |
| 维度 | 异步构造 | 同步构造+whenSynced |
|---|---|---|
| 模块导出 | 无法直接导出 | 可直接导出对象 |
| 消费端代码 | 到处都是 | 直接导入,同步使用 |
| UI集成 | 繁琐的Promise处理 | 单个 |
| 类型签名 | | 带 |
Real-World Example: y-indexeddb
真实案例:y-indexeddb
The Yjs ecosystem uses this pattern everywhere:
typescript
const provider = new IndexeddbPersistence('my-db', doc);
// Constructor returns immediately
provider.on('update', handleUpdate); // Sync access works
await provider.whenSynced; // Wait when you need toThey never block construction. The async work is always deferred to a property you can await.
Yjs生态系统中广泛使用了此模式:
typescript
const provider = new IndexeddbPersistence('my-db', doc);
// 构造函数立即返回
provider.on('update', handleUpdate); // 可直接同步访问
await provider.whenSynced; // 需要时再等待他们从不阻塞构造过程,异步操作始终延迟到可等待的属性中。
Related Patterns
相关模式
- Lazy Singleton — when you need race-condition-safe lazy initialization
- Don't Use Parallel Maps — attach state to instances instead of tracking separately
- 懒加载单例 — 当你需要避免竞态条件的懒加载初始化时使用
- 不要使用并行映射 — 将状态附加到实例上,而非单独跟踪
References
参考资料
- Full article — detailed explanation with diagrams
- Comprehensive guide — 480-line deep dive