为何js_sys的Promise::new要求使用FnMut而非FnOnce?
问题:js_sys的Promise执行器为何定义为FnMut而非FnOnce?
js_sys通过pub fn new(cb: &mut dyn FnMut(Function, Function)) -> Promise;暴露JavaScript的Promise构造函数。根据MDN文档,Promise的执行器函数只会被调用一次,但js_sys却将其参数类型定义为FnMut而非FnOnce。
这给一些简单场景的实现带来了麻烦——比如我想通过通道向未来的自身发送消息时,理想状态下应该能把通道的sender移入闭包,同时让Promise::new接受FnOnce类型的闭包,但当前API不支持:
async fn send_to_future_self() { use std::sync::mpsc::{Sender, Receiver, channel}; let (sender, receiver) = channel().split(); // 计划在未来发送一条消息 // 选项A:移入`sender` → 闭包会变成`FnOnce`,因为它消耗了`sender` // 选项B:引用`sender` → 但借用生命周期会超过`sender`的存活期 let mut sleep_100 = move?? |accept: Function, _reject: Function| { // 这个Rust闭包会被导出到JavaScript... let callback = Closure::__??__(move?? || { // 通过通道发送消息 sender.send(()).unwrap(); let result = js_sys::Array::new(); // 这里传什么值不重要,只是为了同步 result.push(&JsValue::from(true)); accept.apply(&JsValue::undefined(), &result).unwrap(); }); web_sys::window().unwrap() .set_timeout_with_callback_and_timeout_and_arguments_0( callback.as_ref().unchecked_ref(), 100 ).unwrap(); // ...所以要故意遗忘它,否则它会在这个块结束时被销毁, // 但此时JavaScript还在引用它。 callback.forget(); }; // 将JS Promise转换为Rust future // 这里需要一个FnMut(Function, Function)类型的参数 let future: JsFuture = js_sys::Promise::new(&mut sleep_100).into(); let _ = future.await; receiver.try_recv().unwrap(); }
解决方案:用内部可变性适配FnMut要求
既然js_sys的API要求必须传入FnMut,我们可以用内部可变性来模拟FnOnce的行为,确保sender只会被消耗一次:
use std::cell::RefCell; use std::sync::mpsc::{channel, Sender}; use js_sys::{Function, Promise, Array, JsValue}; use wasm_bindgen::closure::Closure; use wasm_bindgen_futures::JsFuture; use web_sys::window; async fn send_to_future_self() { let (sender, receiver) = channel(); // 用RefCell包裹Sender,通过Option实现一次性取出 let sender = RefCell::new(Some(sender)); let mut sleep_100 = move |accept: Function, _reject: Function| { // 取出Sender,同时将RefCell内的Option置为None,确保只能用一次 let sender = sender.take().expect("Promise executor should only run once"); // 创建一次性JS回调,避免内存泄漏 let callback = Closure::once_into_js(move || { sender.send(()).unwrap(); let result = Array::new(); result.push(&JsValue::from(true)); accept.apply(&JsValue::undefined(), &result).unwrap(); }); window().unwrap() .set_timeout_with_callback_and_timeout_and_arguments_0( callback.unchecked_ref(), 100, ) .unwrap(); }; let future: JsFuture = Promise::new(&mut sleep_100).into(); let _ = future.await; receiver.try_recv().unwrap(); }
核心思路:
- 用
RefCell<Option<Sender>>包裹通道发送端,利用内部可变性突破FnMut的不可变借用限制。 - 在执行器闭包内调用
take()取出Sender,同时将Option设为None——这样即使闭包被意外多次调用(符合FnMut的契约),后续调用会触发panic,但根据Promise的规范,这种情况不会发生。 - 使用
Closure::once_into_js创建一次性回调,替代手动forget(),更安全地管理JS侧的内存。
内容的提问来源于stack exchange,提问作者Huckle
相关产品推荐
相关产品推荐

