如何从JavaScript取消Rust编写的Wasm长耗时函数?
优化Wasm长耗时函数的UI取消方案
针对你提出的问题,这里提供几个比setTimeout(0)更优雅、高效的实现方案:
方案1:用queueMicrotask替代setTimeout(0)
queueMicrotask是浏览器原生提供的轻量事件让出API,它会将回调加入微任务队列,比setTimeout(0)的宏任务开销更小,响应更及时,能更精准地让浏览器处理完当前待执行的UI事件。
修改handle_js_events函数如下:
use js_sys::{Function, Promise}; use wasm_bindgen_futures::JsFuture; #[wasm_bindgen] extern "C" { fn queueMicrotask(closure: &Function); } async fn handle_js_events() { JsFuture::from(Promise::new(&mut |resolve: Function, _reject: Function| { queueMicrotask(&resolve); })) .await .unwrap(); }
方案2:基于Promise的被动取消(无需轮询)
当前轮询检查取消状态的方式可以优化为被动监听取消信号,避免不必要的循环检查。调整JsCancellationToken的实现,让它在取消时主动触发Promise完成:
use js_sys::{Function, Promise}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use std::cell::RefCell; #[wasm_bindgen] pub struct JsCancellationToken { cancel_promise: Promise, resolve_cancel: RefCell<Option<Function>>, } #[wasm_bindgen] impl JsCancellationToken { pub fn new() -> JsCancellationToken { let mut resolve_cancel = RefCell::new(None); let cancel_promise = Promise::new(&mut |resolve: Function, _reject: Function| { *resolve_cancel.borrow_mut() = Some(resolve); }); JsCancellationToken { cancel_promise, resolve_cancel, } } pub fn cancel(&self) { if let Some(resolve) = self.resolve_cancel.borrow_mut().take() { resolve.call0(&JsValue::undefined()).unwrap(); } } pub fn cancellation_future(&self) -> JsFuture { JsFuture::from(self.cancel_promise.clone()) } }
修改长耗时函数,用futures::future::race同时执行任务逻辑和取消信号:
use futures::future::{self, Either}; #[wasm_bindgen] pub async fn long_running_function(ct: &JsCancellationToken) -> u32 { // 定义分步执行的任务逻辑 let task = async { let mut counter = 0; loop { // 执行单次任务步骤 counter += 1; // 每步后让出微任务队列,保证UI事件能被处理 JsFuture::from(Promise::new(&mut |resolve, _| { queueMicrotask(&resolve); })).await.unwrap(); // 任务完成条件 if counter >= 1000 { break counter; } } }; match future::race(task, ct.cancellation_future()).await { Either::Left(result) => result, // 任务正常完成 Either::Right(_) => 0, // 取消触发,返回取消标识 } }
这个方案的核心是取消信号由UI事件主动触发,无需轮询检查,性能和代码优雅度都更优。
方案3:用AtomicBool实现高性能状态检查
如果你的函数需要极致性能,且可以接受周期性主动让出事件循环,可改用AtomicBool替代Cell<bool>,取消状态检查无需await,只在关键步骤主动让出事件循环:
use std::sync::atomic::{AtomicBool, Ordering}; use wasm_bindgen::prelude::*; #[wasm_bindgen] pub struct JsCancellationToken { token: AtomicBool, } #[wasm_bindgen] impl JsCancellationToken { pub fn new() -> JsCancellationToken { JsCancellationToken { token: AtomicBool::new(false), } } pub fn cancel(&self) { self.token.store(true, Ordering::SeqCst); } pub fn is_cancellation_requested(&self) -> bool { self.token.load(Ordering::SeqCst) } } #[wasm_bindgen] pub async fn long_running_function(ct: &JsCancellationToken) -> u32 { let mut counter = 0; while !ct.is_cancellation_requested() { counter += 1; // 每执行N步让出一次事件循环,避免长时间阻塞 if counter % 100 == 0 { handle_js_events().await; } } counter }
这里的handle_js_events仍使用queueMicrotask实现,兼顾性能和事件循环的响应性。
内容的提问来源于stack exchange,提问作者Dune
相关产品推荐
相关产品推荐

