news 2026/7/7 12:47:29

WASM 与 JS 互操作中的异步模型:Promise 和 Future 的桥梁怎么搭

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WASM 与 JS 互操作中的异步模型:Promise 和 Future 的桥梁怎么搭

WASM 与 JS 互操作中的异步模型:Promise 和 Future 的桥梁怎么搭

一、当 Rust 的 async 遇上 JS 的 Promise

用 Rust 写 WASM 模块最让我感到别扭的地方,就是异步模型的不匹配。

Rust 用的是 async/await + Future 这套体系,JS 用的是 Promise + async/await。表面上await这个关键字两边都有,但底层的调度机制完全不同。Rust 的 Future 是惰性的(需要被 poll 才会执行),JS 的 Promise 是热启动的(创建后立即开始执行)。

这种差异带来了一个实际问题:Rust 编译成的 WASM 不能直接await一个 JS 的 Promise。如果你在 Rust 的 async 函数里想调用一个 JS 的异步 API(比如fetch),必须通过某种机制把 Promise 转成 Rust 能理解的 Future,或者反过来。

wasm-bindgenwasm-bindgen-futures就是做这个转换的桥梁。这篇文章聊聊它们的工作原理和具体用法。

二、Promise 和 Future 之间的转换过程

要理解这个桥梁是怎么工作的,先看看从 Rust 调 JS Promise 时的完整时序:

sequenceDiagram participant Rust as Rust async fn participant Bridge as wasm-bindgen-futures participant JS as JavaScript Promise participant EventLoop as JS 事件循环 Rust->>Bridge: .await 一个 JsFuture Bridge->>JS: 调用 JS 函数,得到 Promise JS->>EventLoop: Promise 进入 pending 状态 Bridge->>Bridge: 注册 .then() 回调 Bridge->>Rust: Future 返回 Poll::Pending Note over Rust: Rust 挂起,控制权交给 JS EventLoop->>EventLoop: 继续处理其他事件 EventLoop-->>JS: Promise resolve JS->>Bridge: 触发 .then() 回调 Bridge->>Rust: wake() 通知 Future 就绪 Rust->>Bridge: 再次 poll Bridge->>Rust: Poll::Ready(结果) Note over Rust: Rust 恢复执行

这个流程的关键是:当 Rust 的 Future 还没就绪时(返回Poll::Pending),控制权会还给 JS 的事件循环。等到 JS 那边 Promise resolve 了,再通过回调通知 Rust 侧的 Future 可以继续执行了。

三、代码实战

3.1 从 Rust 调用 JS 的异步 API

最常见的场景是在 Rust 的 WASM 模块里调用浏览器 API,比如fetch

use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use web_sys::{window, Request, RequestInit, Response}; /// 在 Rust 的 async 函数中调用 JS 的 fetch API pub async fn fetch_json(url: &str) -> Result<JsValue, JsValue> { // 1. 创建 RequestInit 配置对象 let mut opts = RequestInit::new(); opts.method("GET"); opts.mode(web_sys::RequestMode::Cors); // 2. 创建 Request 对象 let request = Request::new_with_str_and_init(url, &opts)?; // 3. 调用 window.fetch() —— 这会返回一个 JS Promise let window = window().ok_or("无法获取 window 对象")?; let promise = window.fetch_with_request(&request); // 4. 将 JS Promise 转换为 Rust Future // JsFuture::from() 是关键,它把 Promise 包装成一个实现了 Future trait 的类型 let response = JsFuture::from(promise).await?; // 5. 解析 Response 对象 let response: Response = response.dyn_into()?; let json_promise = response.json()?; // response.json() 也返回 Promise let json = JsFuture::from(json_promise).await?; Ok(json) } // 在 WASM 入口中使用 #[wasm_bindgen] pub async fn load_user_data() -> Result<JsValue, JsValue> { // 直接在 Rust 中 await,不需要手动处理 Promise let data = fetch_json("https://api.example.com/user/42").await?; // 解析 JSON 数据 let name = js_sys::Reflect::get(&data, &JsValue::from_str("name"))?; console::log_1(&format!("用户名: {}", name.as_string().unwrap_or_default()).into()); Ok(data) }

JsFuture::from(promise)是核心。它在内部做了几件事:

  • 把 JS Promise 注册到浏览器的微任务队列
  • 给 Promise 挂上.then().catch()回调
  • 返回一个实现了Futuretrait 的 Rust 对象
  • 当 Promise 完成时,回调会触发 Rust Future 的wake(),让它可以被重新 poll

3.2 从 JS 调用 Rust 的异步函数

反过来,JS 也可以调用 Rust 的异步函数:

use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; /// 一个耗时的 Rust 异步计算函数 #[wasm_bindgen] pub async fn heavy_computation(input: u32) -> Result<u32, JsValue> { // 模拟耗时操作 let mut result = input; for i in 0..1000 { result = result.wrapping_mul(i + 1).wrapping_add(i); // 主动 yield,让浏览器有时间处理 UI 事件 if i % 100 == 0 { // 使用微任务 yield,避免长时间阻塞主线程 wasm_bindgen_futures::JsFuture::from( js_sys::Promise::resolve(&JsValue::NULL) ).await?; } } Ok(result) } /// 在 Rust 侧启动异步任务(不阻塞 JS 的主线程) #[wasm_bindgen] pub fn start_background_task() { // spawn_local 在浏览器的主线程上启动异步任务 // 任务会自动被浏览器的微任务队列调度 spawn_local(async { match heavy_computation(42).await { Ok(result) => { console::log_1(&format!("计算结果: {}", result).into()); } Err(e) => { console::error_1(&format!("计算失败: {:?}", e).into()); } } }); }

JS 侧直接调用:

import init, { heavy_computation, start_background_task } from './pkg/my_wasm.js'; async function main() { await init(); // 方式1: 直接 await Rust 的异步函数 // #[wasm_bindgen] 标记的 async fn 会被自动包装为返回 Promise const result = await heavy_computation(42); console.log('直接调用结果:', result); // 方式2: 不 await,让 Rust 在后台运行 start_background_task(); // 这行会立即执行,不会等 background task 完成 console.log('后台任务已启动,主线程继续执行'); } main();

3.3 同时 await 多个 Promise

Rust 的并发能力在 WASM 中也适用:

use wasm_bindgen_futures::JsFuture; use web_sys::window; /// 并发调用多个 API,等所有结果都返回后再继续 pub async fn fetch_multiple() -> Result<Vec<JsValue>, JsValue> { let window = window().ok_or("无法获取 window")?; // 创建三个并发的 fetch 请求 let p1 = window.fetch_with_str("https://api.example.com/users"); let p2 = window.fetch_with_str("https://api.example.com/posts"); let p3 = window.fetch_with_str("https://api.example.com/comments"); // 使用 tokio::join! —— 在 WASM 中表现为并发而非并行 // 注意:WASM 目前不支持真正的多线程(除非使用 Web Workers) let (r1, r2, r3) = tokio::join!( JsFuture::from(p1), JsFuture::from(p2), JsFuture::from(p3), ); Ok(vec![r1?, r2?, r3?]) }

3.4 使用 gloo-timers 处理延时

WASM 中不能使用tokio::time::sleep(因为 Tokio 的定时器依赖操作系统),而要用浏览器原生的setTimeout

use gloo_timers::future::TimeoutFuture; /// 在 WASM 中使用异步延时 pub async fn delayed_greeting(name: &str) { console::log_1(&"3秒后问好...".into()); // 使用浏览器的 setTimeout,不阻塞主线程 TimeoutFuture::new(3000).await; console::log_1(&format!("你好, {}!", name).into()); }

四、边界与注意事项

WASM 单线程模型的限制。目前主流的 WASM 运行时(包括浏览器中的)都是单线程的(SharedArrayBuffer 和 Web Workers 除外)。这意味着tokio::join!虽然可以让多个 Future 并发执行,但它们是交替进行的,不是真正并行的。如果一个 Future 里有纯 CPU 计算,它会阻塞其他 Future。

不能在 WASM 中阻塞等待std::thread::sleep和其他阻塞操作在 WASM 中会冻住整个浏览器页面。所有的"等待"操作都必须通过异步实现,或者使用gloo-timers这类浏览器 API 的封装。

宏任务 vs 微任务。JS 的事件循环区分宏任务(setTimeout、I/O)和微任务(Promise、queueMicrotask)。JsFuture使用的是微任务队列,这通常没问题,但如果有大量并发 Promise,可能需要关注微任务队列的堆积。

文件系统访问。WASM 默认没有文件系统访问权限。如果需要读写文件,必须通过 JS 桥接层使用浏览器的 File API 或 IndexedDB。

异步桥接还有一个性能边界:JsFuture::from()本身有固定的转换开销,大约每次调用几微秒。如果在一个紧密循环里反复调用 JS 异步函数,转换开销会堆积。高频场景下,可以批量收集请求,一次join!多个 Future,减少桥接次数。微优化积累起来,能省不少总耗时。

还有一个序列化边界:wasm-bindgenJsValue和 Rust 类型互转时,serde_wasm_bindgen::from_value对大对象(>1MB)的解析会很慢。如果数据量超过这个阈值,考虑用SharedArrayBuffer做零拷贝传输。

五、总结

Rust WASM 和 JS 的异步桥接,核心就靠两个 crate:wasm-bindgen(定义桥接接口)和wasm-bindgen-futures(Promise ↔ Future 转换)。

大多数情况下,你只需要把 JS 返回的 Promise 包一层JsFuture::from(),然后正常.await就行。反过来,#[wasm_bindgen]标记的 Rust async 函数会自动在 JS 侧暴露为返回 Promise 的函数。转换过程虽然是自动的,但理解背后的机制(特别是单线程事件循环模型)对排查性能问题很有帮助。

实际开发中我踩过的最大坑是在 WASM 里试图用tokio::time::sleep——这在非 WASM 环境下完全正常,但在浏览器里直接 panic。凡是涉及延时的操作,一律用gloo-timers或其他浏览器 API 封装。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/7 12:36:00

随机森林特征重要性分析:3种方法对比与医疗诊断数据集实战

随机森林特征重要性分析&#xff1a;3种方法对比与医疗诊断数据集实战在医疗诊断领域&#xff0c;模型的可解释性往往与预测准确性同等重要。当医生需要根据AI模型的建议做出关键决策时&#xff0c;了解模型依赖哪些特征进行判断至关重要。本文将深入探讨随机森林算法的特征重要…

作者头像 李华
网站建设 2026/7/7 12:34:44

WT2000A3高品质AI降噪录音芯片:蓝牙6.0边录边传方案设计

在录音设备的开发过程中&#xff0c;一个长期存在的难题始终困扰着工程师&#xff1a;如何在保证高品质录音的同时&#xff0c;实现音频数据的实时无线传输。传统方案往往需要两颗独立芯片分别处理录音和蓝牙传输&#xff0c;不仅增加了BOM成本&#xff0c;还面临时钟同步、数据…

作者头像 李华
网站建设 2026/7/7 12:32:53

MiMo-V2.5 深度评测:参数解析与实战表现全记录

在技术选型的关键节点&#xff0c;面对琳琅满目的大语言模型&#xff0c;开发者往往容易陷入参数表的迷宫。我们常常看到各种评测报告罗列着惊人的数字&#xff0c;却很难直接映射到实际开发场景中&#xff1a;这个模型到底能不能读懂我那段复杂的遗留代码&#xff1f;在处理几…

作者头像 李华
网站建设 2026/7/7 12:30:50

锂电池组均衡管理:BQ25887与PIC18F45K80的混合控制方案

1. 电池管理系统中的单元平衡挑战在锂电池组应用中&#xff0c;各电芯间的性能差异会导致充电不均衡&#xff0c;这种不均衡主要体现在三个方面&#xff1a;容量差异&#xff08;Capacity Variance&#xff09;、内阻偏差&#xff08;Internal Resistance Deviation&#xff09…

作者头像 李华
网站建设 2026/7/7 12:29:50

审核被驳回,重提还要再交钱?别被忽悠了,这钱其实不用花

最近跟不少独立开发者和小工作室聊天&#xff0c;发现大家对一个事儿特别犯嘀咕&#xff1a;游戏提交上架审核&#xff0c;结果被官方打回来了&#xff0c;改完再提&#xff0c;还要不要再交一份审核费&#xff1f;这事儿其实没那么复杂&#xff0c;我把各大主流应用商店、安卓…

作者头像 李华