那么如何解决竞态问题呢?在以上这些场景中,我们很容易想到:当发出新的请求时,取消掉上次请求即可。
// 堆代码 duidaima.com const xhr= new XMLHttpRequest(); xhr.open('GET', 'https://xxx'); xhr.send(); xhr.abort(); // 取消请求2.fetch API 取消请求
const controller = new AbortController(); const signal = controller.signal; fetch('/xxx', { signal, }).then(function(response) { //... }); controller.abort(); // 取消请求3. axios 取消请求
const source = axios.CancelToken.source(); axios.get('/xxx', { cancelToken: source.token }).then(function (response) { // ... }); source.cancel() // 取消请求在 cancel 时,axios 会在内部调用 promise.reject() 与 xhr.abort()。
axios.get('/xxx', { cancelToken: source.token }).catch(function(err) { if (axios.isCancel(err)) { console.log('Request canceled', err.message); } else { // 处理错误 } });但 cancelToken 从 v0.22.0 开始已被 axios 弃用。原因是基于实现该 API 的提案 cancelable promises proposal[1] 已被撤销。从 v0.22.0 开始,axios 支持以 fetch API 方式的 AbortController 取消请求。
const controller = new AbortController(); axios.get('/xxx', { signal: controller.signal }).then(function(response) { //... }); controller.abort() // 取消请求同样,在处理请求错误时,也需要判断 error 是否来自 cancel。
import { createImperativePromise } from 'awesome-imperative-promise'; const { resolve, reject, cancel } = createImperativePromise(promise); resolve("some value"); // or reject(new Error()); // or cancel();内部的 cancel 方法其实就是将 resolve,reject 设为 null,让 promise 永远不会 resolve/reject。
function onlyResolvesLast(fn) { // 保存上一个请求的 cancel 方法 let cancelPrevious = null; const wrappedFn = (...args) => { // 当前请求执行前,先 cancel 上一个请求 cancelPrevious && cancelPrevious(); // 执行当前请求 const result = fn.apply(this, args); // 创建指令式的 promise,暴露 cancel 方法并保存 const { promise, cancel } = createImperativePromise(result); cancelPrevious = cancel; return promise; }; return wrappedFn; }以上就是 awesome-only-resolves-last-promise的实现。只需要将 onlyResolvesLast 包装一下请求方法,就能实现自动忽略,减少很多模板代码。
const fn = (duration) => new Promise(r => { setTimeout(r, duration); }); const wrappedFn = onlyResolvesLast(fn); wrappedFn(500).then(() => console.log(1)); wrappedFn(1000).then(() => console.log(2)); wrappedFn(100).then(() => console.log(3)); // 输出 3
let fetchId = 0; // 保存最新的请求 id const getUsers = () => { // 发起请求前,生成新的 id 并保存 const id = fetchId + 1; fetchId = id; await 请求 // 判断是最新的请求 id 再处理回调 if (id === fetchId) { // 请求处理 } }上面的使用方法也会在项目中产生很多模板代码,经过封装后,也能实现一套同样用法的 onlyResolvesLast:
function onlyResolvesLast(fn) { // 利用闭包保存最新的请求 id let id = 0; const wrappedFn = (...args) => { // 发起请求前,生成新的 id 并保存 const fetchId = id + 1; id = fetchId; // 执行请求 const result = fn.apply(this, args); return new Promise((resolve, reject) => { // result 可能不是 promise,需要包装成 promise Promise.resolve(result).then((value) => { // 只处理最新一次请求 if (fetchId === id) { resolve(value); } }, (error) => { // 只处理最新一次请求 if (fetchId === id) { reject(error); } }); }) }; return wrappedFn; }用法也一样,使用 onlyResolvesLast 包装一下请求方法,实现过期请求自动忽略。而且,这样的实现不依赖指令式 promise,也更轻量。
const fn = (duration) => new Promise(r => { setTimeout(r, duration); }); const wrappedFn = onlyResolvesLast(fn); wrappedFn(500).then(() => console.log(1)); wrappedFn(1000).then(() => console.log(2)); wrappedFn(100).then(() => console.log(3)); // 输出 3「取消」和「忽略」的比较