包阅导读总结
1.
关键词:JavaScript、Promise、取消、AbortController、React Hook
2.
总结:本文介绍了在 JavaScript 中取消 Promise 的方法,包括使用 `Promise.withResolvers()` 和 `AbortController`,还探讨了构建可取消任务、顺序请求的 React Hook 及优化,同时给出了相关代码示例和使用场景。
3.
主要内容:
– JavaScript 中取消 Promise 的需求,原生 Promise 不支持取消操作
– 使用 `Promise.withResolvers()` 方法创建可取消任务,并给出代码示例及测试
– 使用 `AbortController` 实现类似效果,可用于构建可取消的获取操作
– 构建简单的顺序请求 React Hook
– 封装基础的顺序请求 React 钩子
– 优化顺序请求 React Hook,处理竞争条件等问题
– 给出使用优化后的 React Hook 的代码示例及效果展示
思维导图:
文章地址:https://mp.weixin.qq.com/s/-KZmFC3IJO9LzrStStuqqw
文章来源:mp.weixin.qq.com
作者:飘飘
发布时间:2024/7/12 0:01
语言:中文
总字数:1956字
预计阅读时间:8分钟
评分:86分
标签:JavaScript,Promise,React Hook,AbortController,前端开发
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
前言
介绍了在 JavaScript 中取消 Promise 的几种方法。由于原生 Promise 不支持取消操作,提供了两种主要技术:使用Promise.withResolvers()
和 AbortController 来创建可取消的任务。此外,还探讨了如何构建一个简单的 React Hook 来处理顺序请求,并优化以支持参数传递和避免不必要的资源消耗。今日前端早读课文章由 @飘飘翻译分享。
正文从这开始~~
使用Promise.withResolvers
和AbortController
在 JavaScript 中创建可取消任务
【图书】一天理解JavaScript Promise
在 JavaScript 中,你可能已经知道如何取消请求:对于 XHR 可以使用xhr.abort()
,对于 fetch 可以使用 signal 。但是你如何取消一个普通的 Promise 呢?
目前,JavaScript 的 Promise 本身并不提供取消常规 Promise 的 API。因此,我们接下来要讨论的是如何丢弃 / 忽略 Promise 的结果。
方法 1:使用新的Promise.withResolvers()
有一个新的 API 可以使用了,即Promise.withResolvers()
。它会返回一个对象,其中包含一个新的 Promise 对象和两个用于解析或拒绝该对象的函数。
【第3276期】深入剖析 Promise.withResolver
代码如下:
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);
在这里,将任务预设为至少耗时 1000 毫秒,但我们在接下来的 500 毫秒内取消了任务,所以你会看到:
请注意,这并不是真正的取消,而是提前拒绝。原来的asyncFn()
将继续执行,直到解析或拒绝为止,但这并不重要,因为用Promise.withResolvers<T>()
创建的 Promise 已经被拒绝了。
方法 2:使用 AbortController
就像我们取消获取请求一样,我们也可以实现一个监听器来实现提前拒绝。它看起来是这样的:
const buildCancelableTask = <T>(asyncFn: () => Promise<T>) => {
const abortController = new AbortController();
return {
run: () =>
new Promise<T>((resolve, reject) => {
const cancelTask = () => reject(new Error('CanceledError'));
if (abortController.signal.aborted) {
cancelTask();
return;
}
asyncFn().then(resolve, reject);
abortController.signal.addEventListener('abort', cancelTask);
}),
cancel: () => {
abortController.abort();
},
};
};
它具有与上述相同的效果,但使用的是 AbortController。你可以在此处使用其他监听器,但 AbortController 提供了额外好处,即如果你多次调用 cancel ,它不会多次触发 ‘abort’ 事件。
基于此代码,我们可以进一步构建可取消的获取操作。这在需要连续请求的场景下非常有用,例如您可能希望丢弃之前的请求结果并使用最新的请求结果。
const buildCancelableFetch = <T>(
requestFn: (signal: AbortSignal) => Promise<T>,
) => {
const abortController = new AbortController();
return {
run: () =>
new Promise<T>((resolve, reject) => {
if (abortController.signal.aborted) {
reject(new Error('CanceledError'));
return;
}
requestFn(abortController.signal).then(resolve, reject);
}),
cancel: () => {
abortController.abort();
},
};
};
const ret = buildCancelableFetch(async signal => {
return fetch('http://localhost:5000', { signal }).then(res =>
res.text(),
);
});
(async () => {
try {
const val = await ret.run();
console.log('val: ', val);
} catch (err) {
console.log('err: ', err);
}
})();
setTimeout(() => {
ret.cancel();
}, 500);
请注意,这不会影响服务器处理逻辑;它只会导致浏览器拒绝 / 取消请求,换句话说,如果你发送 POST 请求以更新用户信息,它仍然可能生效。因此,这种方法更常用于在获取新数据时发送 GET 请求的场景。
构建简单的顺序请求 React Hook
我们可以进一步封装一个简单的顺序请求 React 钩子:
import { useCallback, useRef } from 'react';
const buildCancelableFetch = <T>(
requestFn: (signal: AbortSignal) => Promise<T>,
) => {
const abortController = new AbortController();
return {
run: () =>
new Promise<T>((resolve, reject) => {
if (abortController.signal.aborted) {
reject(new Error('CanceledError'));
return;
}
requestFn(abortController.signal).then(resolve, reject);
}),
cancel: () => {
abortController.abort();
},
};
};
function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}
export function useSequentialRequest<T>(
requestFn: (signal: AbortSignal) => Promise<T>,
) {
const requestFnRef = useLatest(requestFn);
const currentRequest = useRef<{ cancel: () => void } | null>(null);
return useCallback(async () => {
if (currentRequest.current) {
currentRequest.current.cancel();
}
const { run, cancel } = buildCancelableFetch(requestFnRef.current);
currentRequest.current = { cancel };
return run().finally(() => {
if (currentRequest.current?.cancel === cancel) {
currentRequest.current = null;
}
});
}, [requestFnRef]);
}
然后,我们就可以简单地使用它了:
import { useSequentialRequest } from './useSequentialRequest';
export function App() {
const run = useSequentialRequest((signal: AbortSignal) =>
fetch('http://localhost:5000', { signal }).then((res) => res.text()),
);
return <button onClick={run}>Run</button>;
}
这样,当您快速点击按钮多次时,您只会收到最新的请求数据,丢弃以前的请求。
构建优化序列请求 React Hook
如果我们需要一个更全面的顺序请求 React Hook,上面提供的示例仍有改进的空间。例如
代码如下:
import { useCallback, useRef } from 'react';
function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}
export function useSequentialRequest<Args extends unknown[], Data>(
requestFn: (signal: AbortSignal, ...args: Args) => Promise<Data>,
) {
const requestFnRef = useLatest(requestFn);
const running = useRef(false);
const abortController = useRef<AbortController | null>(null);
return useCallback(
async (...args: Args) => {
if (running.current) {
abortController.current?.abort();
abortController.current = null;
}
running.current = true;
const controller = abortController.current ?? new AbortController();
abortController.current = controller;
return requestFnRef.current(controller.signal, ...args).finally(() => {
if (controller === abortController.current) {
running.current = false;
}
});
},
[requestFnRef],
);
}
值得注意的是,在 finally 块中,我们需要检查当前的 controller 是否等于abortController.current
,以防止出现竞争条件:这可以确保我们只在当前活动请求完成时更新状态。反之,如果它们不相等,则意味着 finally 块属于已取消的请求,不应修改running.current
的状态。
下面介绍如何使用它:
import { useState } from 'react';
import { useSequentialRequest } from './useSequentialRequest';
export default function Home() {
const [data, setData] = useState('');
const run = useSequentialRequest(async (signal: AbortSignal, query: string) =>
fetch(`/api/hello?query=${query}`, { signal }).then((res) => res.text()),
);
const handleInput = async (queryStr: string) => {
try {
const res = await run(queryStr);
setData(res);
} catch {
// ignore
}
};
return (
<>
<input
placeholder="Please input"
onChange={(e) => {
handleInput(e.target.value);
}}
/>
<div>Response Data: {data}</div>
</>
);
}
您可以在线体验:https://stackblitz.com/edit/stackblitz-starters-wzfsfn?file=app%2Fpage.tsx
尝试快速输入,它会取消之前的请求,同时始终保留最新的回复。
关于本文
译者:@飘飘
作者:@Zachary Lee
原文:https://webdeveloper.beehiiv.com/p/cancel-promises-javascript
这期前端早读课
对你有帮助,帮”赞“一下,
期待下一期,帮”在看” 一下 。