You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何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();
}

核心思路:

  1. 用RefCell<Option<Sender>>包裹通道发送端,利用内部可变性突破FnMut的不可变借用限制。
  2. 在执行器闭包内调用take()取出Sender,同时将Option设为None——这样即使闭包被意外多次调用(符合FnMut的契约),后续调用会触发panic,但根据Promise的规范,这种情况不会发生。
  3. 使用Closure::once_into_js创建一次性回调,替代手动forget(),更安全地管理JS侧的内存。

内容的提问来源于stack exchange,提问作者Huckle

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 16:55:13