---
title: '异步任务'
description: 在 libuv 线程池中运行任务并使用 AbortSignal 中止它
---

# 异步任务

在讨论 `AsyncTask` 之前，我们需要先讨论 `Task`。

## `Task`

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

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

**lib.rs**

```rust {11-13}
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 Output` 和 `type JsValue`，
`Output` 是 `compute` 方法的返回类型，`JsValue` 是 `resolve` 方法的返回类型。

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

:::

你可以使用底层 API `Env::spawn` 在 libuv 线程池中生成一个定义的 `Task`，参见[引用](/cn/docs/concepts/reference)中的示例。

除了 `compute` 和 `resolve`，您还可以提供 `reject` 方法，当 `Task` 遇到错误时，可以执行一些清理工作，例如 `unref` 一些对象：

**lib.rs**

```rust {28}
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` 方法，在 `Task` 被 `resolved` 或 `rejected` 后执行一些操作：

**lib.rs**

```rust {27}
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`](https://developer.mozilla.org/en-US/docs/Web/API/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 {4}
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 会以 `name` 为 `AbortError` 的错误 reject。

**test.mjs**

```js {6}
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', ...)`。

:::
