C# async await 到 JavaScript Promise:从状态机到原生 await
C# async/await → JavaScript Promise:从状态机到原生 await
Roslyn 已经在最终
Compilation中完成async语义绑定;Jazor 将绑定后的IOperation直接 lowering 为 JavaScriptasync/await。
一、两套异步模型,一个翻译目标
C# 的异步和 JavaScript 的异步在"语法糖"层面几乎一模一样——async 标记方法、await 暂停等待。但如果你看编译器底层,它们是两套完全不同的机制。
C# 编译器在运行时执行路径上会为 async 方法生成状态机;但 Jazor 的输入不是这份 IL,也不会反向分析 IAsyncStateMachine。RazorVue 和普通 C# 模块都把最终 Roslyn Compilation 的绑定语义交给 SemanticWalker,因此 await 仍以 IAwaitOperation 的形式可见。
JavaScript 没有这层展开。JS 引擎原生理解 async function 和 await——它是语言内置的,不是编译器骗术。
所以 Jazor 做的是语义 lowering:把 Roslyn 已绑定的 await 操作直接表达成 JavaScript 的 async function 和 await。
二、两段检测:我怎么知道这里要生成 async
在 SemanticWalker 里,需要判断一个函数体是否包含异步语义的入口有两个场景:匿名函数/Lambda 和方法体。
检测逻辑有两段:
第一段:IOperation 级别的检测。 遍历整个函数体的 IOperation 树,找有没有 IAwaitOperation 节点:
private static bool ContainsAwaitOperation(IOperation? operation)
=> operation is not null &&
(ContainsOperation(operation, static op => op.Kind == OperationKind.Await) ||
ContainsAwaitSyntax(operation.Syntax));
第二段:Syntax 级别的兜底。 如果 IOperation 树里没找到 Await 节点(某些边角情况下 Roslyn 可能不生成),就回到语法树里再搜一遍 AwaitExpressionSyntax:
private static bool ContainsAwaitSyntax(SyntaxNode syntax)
{
return syntax.DescendantNodes(descendIntoChildren: node =>
node is not AnonymousFunctionExpressionSyntax
and not ...
and not LocalFunctionStatementSyntax)
.Any(static node => node is AwaitExpressionSyntax);
}
注意 descendIntoChildren 的条件:碰到嵌套函数边界就停止向下搜索。 这是正确的做法——内部嵌套的 async 函数不应该让外层函数也变成 async。
两段搜索有一处命中,就把这个函数标记为 async。
三、Lambda 和匿名函数:最自然的 async 传递
当 ContainsAwaitOperation 返回 true 时,匿名函数和 Lambda 的生成逻辑非常简单——在 ESTree 的 ArrowFunctionExpression 上把 async 属性设为 true:
// C#:
Func<Task<int>> f = async () => {
await Task.Delay(1000);
return 42;
};
// JS:
const f = async () => {
await new Promise(r => setTimeout(r, 1000));
return 42;
};
async 属性是一个布尔值,直接传给 Acornima AST 构造函数。不需要额外的包装。
四、方法体:同样是给 FunctionDeclaration 加 async
对于完整的类方法,生成逻辑同样直接——在 FunctionDeclaration 上设置 async: true:
// C#:
[ECMAScriptModule]
public static class MyModule
{
public static async Task<string> FetchAsync(string url)
{
await Task.Delay(1);
return url;
}
}
// 编译输出:
export async function fetchAsync(url) {
const response = await httpClient.getAsync(url);
return await response.readAsStringAsync();
}
这里的关键是,Jazor 不需要处理 IAsyncStateMachine 的任何细节。Roslyn 已经把方法识别为 async,SemanticWalker 只需要从绑定后的操作和符号得到异步语义,再把 async 标记传递到输出的 AST 节点。运行时状态机不属于 Jazor 的输入,也不会被 Jazor 重新解析。
五、Task<T> 到 Promise<T> 的类型映射
异步机制是语法层面的,但类型系统也有对应的映射。C# 里的 Task<T> 在 Jazor 中映射到 IPromise<T>:
// CLR 映射:
// System.Threading.Tasks.Task<T> → IPromise<T>
// System.Threading.Tasks.Task → IPromise<void>
IPromise<T> 是 Jazor 的 ECMAScript 绑定层定义的一个接口,它直接对应 JavaScript 的 Promise:
[ECMAScript]
public interface IPromise<T>
{
IPromise<TResult> Then<TResult>(Func<T, TResult> onFulfilled);
IPromise<TResult> Then<TResult>(Func<T, IPromise<TResult>> onFulfilled);
IPromise<T> Catch(Func<object, T> onRejected);
// ...
}
当你在 C# 里写 Task<int> 作为返回类型时,Jazor 会自动生成返回 Promise<number> 的 JavaScript 函数。这个映射对调用方完全透明——C# 代码看起来在操作 Task<T>,编译到 JS 后就是标准的 Promise 链。
六、await 表达式的翻译
VisitAwait 方法处理 IAwaitOperation。翻译逻辑非常直接:
public override Node? VisitAwait(IAwaitOperation operation, SenseArgument argument)
{
var operand = Translate<Expression>(operation.Operation, argument);
return new AwaitExpression(operand);
}
没有状态机编号、没有 MoveNext、没有 SetResult。就是简单地把 C# 的 await 变成 JavaScript 的 await。
这之所以可行,是因为 Jazor 有两个前提保证:
-
C# 的
async方法已经由 Roslyn 完成了语义分析。 Jazor 拿到的是经过绑定和验证的 IOperation 树,await点的位置和操作数类型都是确定的。 -
JS 的
await语义是 C#await的子集。 C# 的await能等待任意 awaitable;Jazor 对已映射的 Task surface 直接生成 JSawait,包括ConfigureAwait的 Promise lowering。
异步 Lambda 的变量捕获
C# 的 async Lambda 会捕获外部变量(closure)。在 IOperation 里,被捕获的变量表现为 IParameterReferenceOperation 或 IFieldReferenceOperation,Roslyn 已经标记了哪些变量被提升到了闭包。Jazor 在翻译时不需要手动做变量提升——因为 JS 的 closure 语义天然就支持捕获外部变量,不需要像 C# 编译器那样生成 display class。
// C#:
int factor = 2;
Func<Task<int>> f = async () => {
await Task.Delay(100);
return factor * 10; // factor 被捕获
};
// JS:
let factor = 2;
const f = async () => {
await new Promise(r => setTimeout(r, 100));
return factor * 10; // JS closure 直接捕获,不需要 display class
};
这是 Jazor 在异步 lowering 上的另一个"免费午餐"——JS 的闭包模型和 C# 的 lambda 捕获高度兼容,不需要额外翻译。
七、switch 表达式里的异步检测
这是模式匹配 blog 里也提到过的一个交叉点。如果 switch 表达式的某个 arm 里包含了 await,整个 switch 表达式会被包进一个 async IIFE:
// C#:
var result = await (obj switch { ... });
// 如果检测到 ContainsAwaitOperation:
var result = await (async () => { ... })();
检测时机是在把 switch 表达式 Lower 成 IIFE 的时候——如果 ContainsAwaitOperation(operation) 返回 true,IIFE 的箭头函数就标记为 async。
八、什么场景会失败:异步的边界
Jazor 目前对异步的支持有明确的边界:
- 已落在当前 lowering 支持范围内的
async方法/Lambda:生成async function或async () => {};具体方法体仍受普通 C#、CLR 映射和使用点边界约束。 Task/Task<T>返回类型:支持。自动映射到Promise。await表达式:支持。直接翻译为 JSawait。ConfigureAwait(bool/options):当前 CLR 映射将其规约到 Promise 语义;它不会在 JS 中保留 .NET 的同步上下文捕获。- 未被 CLR 白名单映射的自定义
awaitable:不支持;是否可用由具体映射和使用点裁决。 async迭代器(IAsyncEnumerable<T>):不支持。JS 有for await...of,但 Jazor 还没打通这条链路。
边界策略跟 Jazor 的整体哲学一致:支持的是"能干净映射"的部分,不支持的就明确报错,不静默回退。
九、小结
C# async/await 到 JavaScript 的翻译是 Jazor 里最"自然"的一块 lowering——不是因为简单,而是因为两个语言的异步模型在语法层面高度相似。
但"相似"背后有一个容易被忽略的事实:Jazor 处理的是 Roslyn 保留下来的源级绑定语义,而不是 C# 编译器生成的状态机 IL。 它只需把 IAwaitOperation 和已映射的异步类型用 JS 的原生语法表达出来。
整个过程的支柱是 ContainsAwaitOperation 和 ContainsAwaitSyntax 两个检测方法:它们扫描 IOperation 树和语法树,在函数边界内识别异步特征,然后把 async 标记传递给输出的 AST 节点。不需要重写状态机、不需要模拟 MoveNext——JS 的 await 本身就是我们要的东西。

浙公网安备 33010602011771号