JS `AbortController` 与 `AbortSignal`:统一取消异步操作

各位观众老爷们,大家好!我是你们的老朋友,今天咱们来聊聊 JavaScript 里一对好基友:AbortControllerAbortSignal。 这俩哥们儿,专门负责取消异步操作,让你的代码更优雅,更可控。

开场:异步操作的烦恼

在 JavaScript 的世界里,异步操作简直是家常便饭。 比如,从服务器请求数据,处理用户输入,执行定时任务等等。 这些操作往往需要一段时间才能完成,而且,有时候我们可能需要提前取消它们。

想想看,你发起了个网络请求,结果用户手抖点了一下取消按钮,或者页面已经跳转了,你还在傻乎乎地等着服务器返回数据,这多浪费资源啊!

更糟糕的是,如果你的代码没有妥善处理取消的情况,可能还会导致一些奇怪的 bug,比如内存泄漏,或者页面崩溃。

AbortControllerAbortSignal 闪亮登场

为了解决这些问题,JavaScript 引入了 AbortControllerAbortSignal 这两个 API。

  • AbortController: 就像一个遥控器,用来控制异步操作的取消。 你可以通过它创建一个 AbortSignal 对象,然后把这个 AbortSignal 传递给你的异步操作。
  • AbortSignal: 就像一个开关,用来告诉异步操作是否需要取消。 异步操作会监听这个 AbortSignal 的状态,一旦 AbortSignal 被设置为取消状态,异步操作就会停止执行。

简单示例:取消 fetch 请求

咱们先来看一个最常见的例子:取消 fetch 请求。

const controller = new AbortController();
const signal = controller.signal;

fetch('https://example.com/data', { signal })
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  })
  .then(data => {
    console.log('Data:', data);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Fetch aborted');
    } else {
      console.error('Fetch error:', error);
    }
  });

// 在某个时刻,取消请求
setTimeout(() => {
  controller.abort();
}, 5000); // 5秒后取消请求

在这个例子中,我们首先创建了一个 AbortController 对象,然后通过 controller.signal 获取了一个 AbortSignal 对象。

接着,我们将 signal 对象传递给 fetch 函数的 options 参数。

fetch 函数会监听 signal 的状态。 如果 signal 被设置为取消状态,fetch 函数就会抛出一个 AbortError 异常。

catch 块中,我们可以判断 error.name 是否为 AbortError,如果是,就说明请求被取消了。

最后,我们使用 setTimeout 函数在 5 秒后调用 controller.abort() 方法,取消请求。

AbortControllerAbortSignal 的原理

其实,AbortControllerAbortSignal 的原理非常简单。

  • AbortController 内部维护着一个 AbortSignal 对象。
  • AbortSignal 对象有一个 aborted 属性,表示是否被取消。
  • AbortController.abort() 方法会将 AbortSignal 对象的 aborted 属性设置为 true
  • 异步操作会监听 AbortSignal 对象的 aborted 属性,一旦该属性变为 true,异步操作就会停止执行。

更详细的介绍

下面,咱们来更详细地介绍一下 AbortControllerAbortSignal 的各个属性和方法。

AbortController

属性/方法 描述
AbortController() 构造函数,创建一个 AbortController 对象。
signal 只读属性,返回一个与 AbortController 关联的 AbortSignal 对象。
abort(reason?) 方法,取消与 AbortController 关联的异步操作。 可以传递一个 reason 参数,表示取消的原因。 这个 reason 会被传递给 AbortSignal 对象的 reason 属性。

AbortSignal

属性/方法 描述
aborted 只读属性,返回一个布尔值,表示 AbortSignal 是否被取消。 如果被取消,返回 true,否则返回 false
reason 只读属性,返回一个值,表示取消的原因。 只有在 aborted 属性为 true 时,该属性才有效。 如果没有指定取消原因,则返回 undefined
onabort 事件处理程序属性,用于指定当 AbortSignal 被取消时要执行的函数。
throwIfAborted() 方法,如果 AbortSignal 已经被取消,则抛出一个 AbortError 异常。 这个方法可以用来在异步操作开始之前检查 AbortSignal 的状态,如果已经被取消,则直接抛出异常,避免执行不必要的代码。
addEventListener() 方法,允许你监听 abort 事件。 当 AbortSignal 被取消时,会触发 abort 事件。 例如:signal.addEventListener('abort', () => { console.log('Abort signal received!'); });
removeEventListener() 方法,移除之前添加的 abort 事件监听器。

使用 AbortSignal 取消其他异步操作

除了 fetch 请求,AbortSignal 还可以用来取消其他类型的异步操作,比如 setTimeoutXMLHttpRequest,甚至是你自己编写的异步函数。

示例1:取消 setTimeout

const controller = new AbortController();
const signal = controller.signal;

const timeoutId = setTimeout(() => {
  console.log('Timeout completed');
}, 5000);

signal.addEventListener('abort', () => {
  clearTimeout(timeoutId);
  console.log('Timeout aborted');
});

// 在某个时刻,取消 timeout
setTimeout(() => {
  controller.abort();
}, 2000); // 2秒后取消 timeout

在这个例子中,我们使用 addEventListener 方法监听 AbortSignalabort 事件。 当 AbortSignal 被取消时,我们会调用 clearTimeout 函数来取消 setTimeout

示例2:取消自定义的异步函数

function myAsyncFunction(signal) {
  return new Promise((resolve, reject) => {
    // 检查 AbortSignal 是否已经被取消
    if (signal.aborted) {
      reject(new Error('Operation aborted'));
      return;
    }

    // 监听 AbortSignal 的 abort 事件
    signal.addEventListener('abort', () => {
      reject(new Error('Operation aborted'));
    });

    // 模拟异步操作
    setTimeout(() => {
      resolve('Operation completed');
    }, 3000);
  });
}

const controller = new AbortController();
const signal = controller.signal;

myAsyncFunction(signal)
  .then(result => {
    console.log('Result:', result);
  })
  .catch(error => {
    console.error('Error:', error.message);
  });

// 在某个时刻,取消异步操作
setTimeout(() => {
  controller.abort();
}, 2000); // 2秒后取消异步操作

在这个例子中,我们在自定义的异步函数 myAsyncFunction 中,首先检查 AbortSignal 是否已经被取消。 如果已经被取消,则直接 reject Promise。

然后,我们使用 addEventListener 方法监听 AbortSignalabort 事件。 当 AbortSignal 被取消时,我们会 reject Promise。

这样,我们就可以使用 AbortControllerAbortSignal 来取消自定义的异步函数了。

传递取消原因

AbortController.abort() 方法可以接受一个可选的 reason 参数,用于指定取消的原因。 这个 reason 会被传递给 AbortSignal 对象的 reason 属性。

const controller = new AbortController();
const signal = controller.signal;

fetch('https://example.com/data', { signal })
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  })
  .then(data => {
    console.log('Data:', data);
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Fetch aborted. Reason:', signal.reason);
    } else {
      console.error('Fetch error:', error);
    }
  });

// 在某个时刻,取消请求
setTimeout(() => {
  controller.abort('Request timed out'); // 指定取消原因
}, 5000); // 5秒后取消请求

在这个例子中,我们在调用 controller.abort() 方法时,传递了一个字符串 'Request timed out' 作为取消原因。

catch 块中,我们可以通过 signal.reason 属性来获取取消原因。

throwIfAborted() 方法

AbortSignal 对象提供了一个 throwIfAborted() 方法,用于在异步操作开始之前检查 AbortSignal 的状态。 如果 AbortSignal 已经被取消,则直接抛出一个 AbortError 异常,避免执行不必要的代码。

function myAsyncFunction(signal) {
  try {
    signal.throwIfAborted(); // 检查 AbortSignal 是否已经被取消
  } catch (error) {
    console.log('Operation already aborted');
    return Promise.reject(error);
  }

  return new Promise((resolve, reject) => {
    // 监听 AbortSignal 的 abort 事件
    signal.addEventListener('abort', () => {
      reject(new Error('Operation aborted'));
    });

    // 模拟异步操作
    setTimeout(() => {
      resolve('Operation completed');
    }, 3000);
  });
}

const controller = new AbortController();
const signal = controller.signal;

// 在某个时刻,取消异步操作
setTimeout(() => {
  controller.abort();
}, 1000); // 1秒后取消异步操作

myAsyncFunction(signal)
  .then(result => {
    console.log('Result:', result);
  })
  .catch(error => {
    console.error('Error:', error.message);
  });

在这个例子中,我们在 myAsyncFunction 函数的开头调用了 signal.throwIfAborted() 方法。 如果在调用 myAsyncFunction 之前,AbortSignal 已经被取消,那么 signal.throwIfAborted() 方法会抛出一个 AbortError 异常,导致 myAsyncFunction 函数直接返回一个 rejected 的 Promise。

最佳实践

  • 在发起异步操作之前,总是创建一个 AbortController 对象,并获取其 AbortSignal 对象。
  • AbortSignal 对象传递给异步操作。
  • 在异步操作中,监听 AbortSignalabort 事件,并在事件处理程序中停止异步操作。
  • 在合适的时候,调用 AbortController.abort() 方法取消异步操作。
  • 使用 signal.throwIfAborted() 方法在异步操作开始之前检查 AbortSignal 的状态。
  • 合理使用 reason 参数,传递取消原因,方便调试。

总结

AbortControllerAbortSignal 是一对非常实用的 API,可以帮助我们更好地控制异步操作的取消。 掌握它们的使用方法,可以使我们的代码更健壮,更易于维护。

好了,今天的讲座就到这里。 希望大家有所收获! 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注