Skip to content

异步任务

在讨论 AsyncTask 之前,我们需要先讨论 Task

Task

附加模块通常需要利用 libuv 中的异步助手作为其实现的一部分, 这样,它们就可以安排工作在异步执行,以便它们的方法可以在工作完成之前返回, 这样就可以避免阻塞 Node.js 应用程序的整体执行。

Task 特征提供了一种定义这样的异步任务的方法,该任务需要在 libuv 线程中运行,您可以实现 compute 方法,该方法将在 libuv 线程中调用。

lib.rs
rust
use napi::{Task, Env, Result, JsNumber};

struct AsyncFib {
  input: u32,
}

impl Task for AsyncFib {
  type Output = u32;
  type JsValue = JsNumber;

  fn compute(&mut self) -> Result<Self::Output> { 
    Ok(fib(self.input)) 
  } 

  fn resolve(&mut self, env: Env, output: u32) -> Result<Self::JsValue> {
    env.create_uint32(output)
  }
}

fn compute 在 libuv 线程中运行,您可以在这里运行一些繁重的计算,这不会阻塞 JavaScript 主线程。

你可能会注意到 Task 特征上有两个关联类型,type Outputtype JsValueOutputcompute 方法的返回类型,JsValueresolve 方法的返回类型。

TIP

我们需要分开 type Outputtype JsValue,因为我们无法在 fn compute 中回调 JavaScript 函数,它不在主线程上执行, 所以我们需要在主线程上运行的 fn resolve,根据 OutputEnv 创建 JsValue 并在 JavaScript 中回调它。

你可以使用底层 API Env::spawn 在 libuv 线程池中生成一个定义的 Task,参见引用中的示例。

除了 computeresolve,您还可以提供 reject 方法,当 Task 遇到错误时,可以执行一些清理工作,例如 unref 一些对象:

lib.rs
rust
struct CountBufferLength {
  data: Ref<JsBufferValue>,
}

impl CountBufferLength {
  pub fn new(data: Ref<JsBufferValue>) -> Self {
    Self { data }
  }
}

impl Task for CountBufferLength {
  type Output = usize;
  type JsValue = JsNumber;

  fn compute(&mut self) -> Result<Self::Output> {
    if self.data.len() == 10 {
      return Err(Error::from_reason("len can't be 10".to_string()));
    }
    Ok((&self.data).len())
  }

  fn resolve(&mut self, env: Env, output: Self::Output) -> Result<Self::JsValue> {
    self.data.unref(env)?;
    let output = u32::try_from(output)
      .map_err(|_| Error::new(Status::InvalidArg, "buffer length exceeds u32"))?;
    env.create_uint32(output)
  }
  fn reject(&mut self, env: Env, err: Error) -> Result<Self::JsValue> {
    self.data.unref(env)?;
    Err(err)
  }
}

您还可以提供一个 finally 方法,在 Taskresolvedrejected 后执行一些操作:

lib.rs
rust
struct CountBufferLength {
  data: Ref<JsBufferValue>,
}

impl CountBufferLength {
  pub fn new(data: Ref<JsBufferValue>) -> Self {
    Self { data }
  }
}

#[napi]
impl Task for CountBufferLength {
  type Output = usize;
  type JsValue = JsNumber;

  fn compute(&mut self) -> Result<Self::Output> {
    if self.data.len() == 10 {
      return Err(Error::from_reason("len can't be 5".to_string()));
    }
    Ok((&self.data).len())
  }

  fn resolve(&mut self, env: Env, output: Self::Output) -> Result<Self::JsValue> {
    let output = u32::try_from(output)
      .map_err(|_| Error::new(Status::InvalidArg, "buffer length exceeds u32"))?;
    env.create_uint32(output)
  } 

  fn finally(&mut self, env: Env) -> Result<()> {
    self.data.unref(env)?;
    Ok(())
  }
}

TIP

impl Task for AsyncFib 上面的 #[napi] 宏只是为了生成 .d.ts 文件, 如果这里没有定义 #[napi],生成的 TypeScript 类型里, AsyncTask 的返回值类型将是 Promise<unknown>

AsyncTask

你定义的 Task 不能直接返回给 JavaScript,JavaScript 引擎不知道如何运行和解析你的 struct 的值, AsyncTask 是可以返回给 JavaScript 引擎的 Task 的包装, 可以使用 Task 和可选的 AbortSignal 来创建它。

lib.rs
rust
#[napi]
fn async_fib(input: u32) -> AsyncTask<AsyncFib> {
  AsyncTask::new(AsyncFib { input })
}

⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️

index.d.ts
ts
export function asyncFib(input: number): Promise<number>

结合 AbortSignal 创建 AsyncTask

您可以给 AsyncTask 传入 AbortSignal ,这样如果 AsyncTask 还没有启动,您就可以中止它。

lib.rs
rust
use napi::bindgen_prelude::AbortSignal;

#[napi]
fn async_fib(input: u32, signal: AbortSignal) -> AsyncTask<AsyncFib> { 
  AsyncTask::with_signal(AsyncFib { input }, signal)
}

⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️

index.d.ts
ts
export function asyncFib(input: number, signal: AbortSignal): Promise<number>

如果在 libuv 开始执行 AsyncTask 之前调用 AbortController.abort, Node-API 可以取消队列中的工作,Promise 会以 nameAbortError 的错误 reject。

test.mjs
js
import { asyncFib } from './index.js'

const controller = new AbortController()

asyncFib(20, controller.signal).catch((e) => {
  console.error(e) // Error: AbortError
})

controller.abort()

如果您不知道 AsyncTask 是否需要中止, 您还可以给 AsyncTask 传入 Option<AbortSignal>

lib.rs
rust
use napi::bindgen_prelude::AbortSignal;

#[napi]
fn async_fib(input: u32, signal: Option<AbortSignal>) -> AsyncTask<AsyncFib> {
  AsyncTask::with_optional_signal(AsyncFib { input }, signal)
}

⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️

index.d.ts
ts
export function asyncFib(
  input: number,
  signal?: AbortSignal | undefined | null,
): Promise<number>

TIP

如果 AsyncTask 已经开始,Node-API 无法取消正在运行的 compute; 如果已经完成,Promise 结果也已经确定。即使取消时机太晚, Rust 中注册的 AbortSignal::on_abort 回调仍会在 JavaScript signal 触发时运行。 当前 adapter 在转换参数时安装处理函数,不会检查 signal.aborted, 因此请传入尚未 abort 的 signal。它还会给 signal.onabort 赋值,覆盖该属性中 已有的处理函数;独立的 JavaScript 监听器请使用 signal.addEventListener('abort', ...)