取消Promise、Fetch和XHR请求的高级技巧

279 阅读5分钟

JavaScript的异步编程中,Promise对象扮演着核心角色,它允许我们以更优雅的方式处理延迟和异步操作。但是,一个常见的问题是,一旦创建了Promise,就没有一个直接的方式来取消它。这不仅限制了我们的控制能力,也可能导致资源浪费。本文将探讨如何在JavaScript中优雅地“取消”Promise,以及如何有效地管理FetchXMLHttpRequest请求的取消。我们将介绍一些实用的技巧和最新的API,帮助你更好地控制异步流程。跟随本文,你将学会如何通过Promise.withResolvers()AbortController来实现取消操作,以及如何处理取消后的清理工作。让我们一起揭开取消Promise的神秘面纱,掌握这些实用的编程技巧。

如何取消 Promise

目前,JavaScript 的 Promise 并没有提供一个 API 来取消一个普通的 Promise。所以,我们接下来要讨论的是如何丢弃/忽略 Promise 的结果。

现在可以使用的一个新 API 是 Promise.withResolvers()。它返回一个包含一个新的 Promise 对象和两个用于解决或拒绝它的函数的对象。

代码看起来是这样的:

let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

现在我们可以这样做:

const { promise, resolve, reject } = Promise.withResolvers();

所以我们可以利用这个来暴露一个 cancel 方法:

const buildCancelableTask = <T>(asyncFn: () => Promise<T>) => {
  let rejected = false;
  const { promise, resolve, reject } = Promise.withResolvers<T>();

  return {
    run: () => {
      if (!rejected) {
        asyncFn().then(resolve, reject);
      }

      return promise;
    },

    cancel: () => {
      rejected = true;
      reject(new Error('CanceledError'));
    },
  };
};

然后我们可以使用以下测试代码:

const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));

const ret = buildCancelableTask(async () => {
  await sleep(1000);
  return 'Hello';
});

(async () => {
  try {
    const val = await ret.run();
    console.log('val: ', val);
  } catch (err) {
    console.log('err: ', err);
  }
})();

setTimeout(() => {
  ret.cancel();
}, 500);

在这里,我们预设任务至少需要 1000ms,但我们在接下来的 500ms 内取消它,所以你将看到:

media.beehiiv.com/cdn-cgi/ima…

请注意,这不是真正的取消,而是一个早期拒绝。原始的 asyncFn() 将继续执行直到它解决或拒绝,但这并不重要,因为我们用 Promise.withResolvers<T>() 创建的 Promise 已经被拒绝了。

如何取消 Fetch

AbortController 是 Fetch API 的一部分,它提供了一种机制来取消一个或多个 Web 请求。AbortController 允许你通过一个信号(AbortSignal)来控制一个或多个请求的取消行为。当 AbortSignalabort 方法被调用时,所有关联到这个信号的请求都会收到取消的指令。

以下是如何使用 AbortController 来取消一个 Fetch 请求的步骤:

1. 创建一个 AbortController 实例

const controller = new AbortController();

2. 获取 AbortSignal 对象

AbortController 实例有一个 signal 属性,它是一个 AbortSignal 对象,你可以将它传递给 Fetch 请求。

const { signal } = controller;

3. 将 AbortSignal 传递给 Fetch 请求

当你发起一个 Fetch 请求时,将 signal 对象作为选项传递给请求。

fetch(url, { signal })
  .then(response => {
    // 处理响应
  })
  .catch(error => {
    // 处理错误,包括取消错误
    if (error.name === 'AbortError') {
      console.log('Fetch aborted');
    }
  });

4. 取消请求

当你想要取消请求时,调用 AbortControllerabort 方法。这会触发所有关联到这个 AbortControllerAbortSignal 的请求取消。

// 稍后取消请求
setTimeout(() => {
  controller.abort();
}, 3000); // 3秒后取消请求

完整示例

const controller = new AbortController();
const { signal } = controller;

fetch('https://example.com/data', { signal })
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => {
    if (error.name === 'AbortError') {
      // 处理取消逻辑
      console.log('Fetch aborted');
    } else {
      // 处理其他错误
      console.error('Fetch error:', error);
    }
  });

// 3秒后取消请求
setTimeout(() => {
  controller.abort();
}, 3000);

使用 AbortController 的好处是,你可以对多个请求使用同一个 AbortController 来控制它们的取消行为。这对于同时取消多个相关请求非常有用,比如在用户离开页面或取消某个操作时。

请注意,AbortController 只能用于支持 AbortSignal 的 API,比如 Fetch API。对于 XMLHttpRequest 或其他不支持 AbortSignal 的 API,你不能直接使用 AbortController 来取消请求。

如何取消 XHR 请求

对于取消 XMLHttpRequest (XHR) 请求,abort() 方法是最直接的方式,也是唯一的标准方法来取消正在进行的 HTTP 请求。没有其他内置的 JavaScript 方法可以取消 XHR 请求。然而,你可以采取一些策略来管理和控制请求,尽管这些不是直接取消请求的方法:

1. 控制请求触发

在发送请求之前,你可以设置一个标志来控制是否发送请求:

var xhr;
var isRequestActive = true;

function sendRequest() {
  if (!isRequestActive) return;

  xhr = new XMLHttpRequest();
  xhr.open('GET', 'your-url-here', true);
  xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
      console.log('Request successful:', xhr.responseText);
    } else {
      console.error('Request failed:', xhr.status);
    }
  };
  xhr.onerror = function() {
    console.error('Request failed');
  };
  xhr.send();
}

function cancelRequest() {
  isRequestActive = false;
  if (xhr) {
    xhr.abort();
  }
}

// 发送请求
sendRequest();

// 取消请求
cancelRequest();

2. 超时控制

你可以设置一个超时来自动取消请求,如果请求在一定时间内没有完成:

var xhr;
var timeoutId;

function sendRequest() {
  xhr = new XMLHttpRequest();
  xhr.open('GET', 'your-url-here', true);
  xhr.onload = function() {
    clearTimeout(timeoutId);
    if (xhr.status >= 200 && xhr.status < 300) {
      console.log('Request successful:', xhr.responseText);
    } else {
      console.error('Request failed:', xhr.status);
    }
  };
  xhr.onerror = function() {
    clearTimeout(timeoutId);
    console.error('Request failed');
  };
  xhr.send();

  // 设置超时
  timeoutId = setTimeout(function() {
    xhr.abort();
    console.log('Request timed out and was aborted');
  }, 5000); // 5秒后超时
}

// 发送请求
sendRequest();

3. 资源清理

在某些情况下,你可能想要在请求被取消后执行一些清理工作,比如释放资源或更新 UI:

var xhr;

function sendRequest() {
  xhr = new XMLHttpRequest();
  xhr.open('GET', 'your-url-here', true);
  xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
      console.log('Request successful:', xhr.responseText);
    } else {
      console.error('Request failed:', xhr.status);
    }
  };
  xhr.onerror = function() {
    console.error('Request failed');
  };
  xhr.onabort = function() {
    console.log('Request was aborted');
  };
  xhr.send();
}

function cancelRequest() {
  if (xhr) {
    xhr.abort();
    // 执行清理工作
    cleanupResources();
  }
}

function cleanupResources() {
  // 清理资源
  console.log('Resources cleaned up');
}

// 发送请求
sendRequest();

// 取消请求
cancelRequest();

这些方法提供了额外的控制和灵活性,但它们并不是取消请求的替代方法。abort() 仍然是唯一直接取消 XHR 请求的标准方法。