关于 Promise 实战的几个案例
本文是关于一次面试的记录,从而引发的进一步扩展。
初级:如何同时发起十个请求
一看到这个标题,我们很容易的就想到可以用 Promise.all()
进行实现。
以下是实现的具体代码。
js
// 假设我们有一个包含URLs的数组
const urls = [
'https://api.example.com/data/1',
'https://api.example.com/data/2',
// ... 更多URLs
'https://api.example.com/data/10'
];
// 定义一个异步的fetchData函数来发起请求
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // 假设API返回JSON格式的数据
} catch (error) {
console.error(`Failed to fetch data from ${url}: `, error);
throw error; // 重新抛出错误,以便Promise.all能捕获
}
}
// 使用Promise.all同时发起所有请求
Promise.all(urls.map(fetchData))
.then(dataArrays => {
// dataArrays是一个数组,包含了每个fetchData调用的结果
// 这里可以处理所有请求成功的数据
console.log('All requests completed successfully:', dataArrays);
})
.catch(error => {
// 这里处理如果有任何请求失败的情况
console.error('At least one request failed:', error);
});
这里还可以再看看 Promise
的其他方法和它们之间的区别。
Promise.all()
,接收一个由Promise
组成的数组。 当全部成功时,返回全部的值,当任何一个失败时,只返回首次失败的原因。
js
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// Expected output: Array [3, 42, "foo"]
Promise.allSettled()
,接收一个由Promise
组成的数组。 会返回每一个 Promise 的结果,它的返回结构为{ status: "rejected", reason: "foo" }
或者{ status: 'fulfilled', value: 99 }
,这取决于Promise
的状态。最后把所有的结果合并到一个数组中。
js
Promise.allSettled([
Promise.resolve(33),
new Promise((resolve) => setTimeout(() => resolve(66), 0)),
99,
Promise.reject(new Error("一个错误")),
]).then((values) => console.log(values));
// [
// { status: 'fulfilled', value: 33 },
// { status: 'fulfilled', value: 66 },
// { status: 'fulfilled', value: 99 },
// { status: 'rejected', reason: Error: 一个错误 }
// ]
Promise.race()
,接收一个由Promise
组成的数组。返回最先完成的Promise
的结果。
js
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one');
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two');
});
Promise.race([promise1, promise2]).then((value) => {
console.log(value);
// Both resolve, but promise2 is faster
});
// Expected output: "two"
进阶:如何对发起的请求进行并发控制
控制并发数量
思路:
- 维护一个队列,存储的是 请求的 url
- 几个控制变量
- 并发请求数量
- 当前队列
- 当前请求数量
- 请求开始的执行条件用 while 去做
- 请求结束的标识 用 setInterval 循环去监听
js
function fetchWithLimit(urls, limit = 5) {
const queue = [];
const results = [];
let activeCount = 0;
// 初始化队列
urls.forEach(url => {
queue.push(url)
});
function fakeFetch(url) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(url)
console.log('请求执行完毕', url);
}, 100);
})
}
function fetchURL() {
if (queue.length > 0 && activeCount < limit) {
activeCount++
const url = queue.shift()
console.log('请求开始', url)
fakeFetch(url).then((res) => {
results.push(res)
})
.catch((err) => console.log(err))
.finally(() => {
activeCount--
console.log('请求结束', url)
fetchURL()
})
}
}
// 开始处理队列
while (activeCount < limit) {
fetchURL();
}
return new Promise((resolve) => {
const timer = setInterval(() => {
if (queue.length === 0 && activeCount === 0) {
clearInterval(timer)
resolve(results)
}
}, 100);
})
}
const urls = Array.from({ length: 70 }, (_, i) => `https://api.example.com/data/${i}`);
fetchWithLimit(urls, 5).then((res) => console.log('最终结果', res))
高级:如何取消一个正在进行的请求
这里很容易的可以想到用Promise.race()
去做竞态的控制。当需要取消请求时,想办法触发其中一个 Promise
使其状态变为 fulfilled
或者 Rejected
。那么整个包括 Promise.race()
的请求就完成了。
那么问题来了,如何去触发 Promise
使其改变状态呢。
我们可以思考下,在 axios
中,是如何进行一个请求的取消。
js
const controller = new AbortController()
aiios.get('/foo/bar', {
singal: controller.singal
}).then(fuction(res)
//...
)
// 调用中止请求的方法
controller.abort()
如果对于 AbortController
不熟悉的,我们可以来看一段 MDN 关于它的介绍:
AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。 你可以使用 AbortController.AbortController() 构造函数创建一个新的 AbortController。使用 AbortSignal 对象可以完成与 DOM 请求的通信。
通过上面的代码,我们可以判断。
axios
内部封装AbortController
以支持取消请求的方式主要是通过将用户提供的signal
选项与fetch
请求或者 XMLHttpRequest
的中止机制集成。
- 接收
signal
选项 :当创建axios
请求时,用户可以向配置对象中传递一个从AbortController.signal
获取的AbortSignal
实例。
javascript
const controller = new AbortController();
axios.get('/api/data', {
signal: controller.signal
});
- 集成到请求流程 :在axios的请求处理逻辑中,它会检查是否有提供的
signal
。如果有,它会把这个signal
与实际的网络请求(无论是使用fetch
还是XMLHttpRequest
)关联起来。
-
对于基于
fetch
的请求,直接将signal
作为选项传递给fetch
,因为fetch
原生支持AbortSignal
。javascriptfetch(url, { method: 'GET', signal: config.signal, // 这里的config.signal就是用户传入的AbortSignal })
-
对于基于
XMLHttpRequest
的兼容性处理,axios会监听signal
的abort
事件,当事件触发时手动调用XMLHttpRequest.abort()
来取消请求。
-
处理取消事件 :当
AbortController
的abort()
方法被调用时,其关联的AbortSignal
会触发abort
事件。axios监听这个事件,并在接收到信号时执行相应的清理工作,包括但不限于停止请求、清理定时器以及正确地解析和传递错误信息。 -
错误处理 :当请求因取消而中止时,axios会捕获到一个错误,并在
.catch
处理中可以通过检查错误的类型(通常是DOMException
,错误名为AbortError
)来识别请求是否因取消而失败。
这样,axios通过将用户提供的AbortSignal
与底层网络请求的取消机制绑定,实现了请求的可取消性,同时也保持了错误处理的一致性和易用性。
所以,要手动实现可中断的请求,关键点如下:
- 使用 Promise.race(),包括 提供取消功能的Promise 和 真正请求的Promise。
- 使用
new AbortController()
构造函数,创建出一个新的AbortController
对象实例。 - 调用
AbortController
对象实例上的abort()
方法。 - 监听
AbortController
对象实例上的abort()
事件执行。 - 当监听到第4点事件执行时,调用 提供取消功能的Promise 的 Reject() 回调,从而中止
Promise.race()
然后我们就可以写出如下代码:
js
function cancellableFetch(url, controller) {
// 创建实际的fetch请求Promise
const fetchPromise = fetch(url).then(response => response.text());
// 创建一个与AbortController关联的Promise,用于模拟取消操作
const cancelPromise = new Promise((resolve, reject) => {
controller.signal.addEventListener('abort', () => {
reject(new Error('Fetch cancelled'));
});
});
// 使用Promise.race()来决定是fetch完成还是操作被取消
return Promise.race([fetchPromise, cancelPromise]);
}
// 创建AbortController实例
const controller = new AbortController();
// 尝试发起一个可取消的fetch请求
cancellableFetch('https://api.example.com/data', controller)
.then(data => console.log('Data received:', data))
.catch(error => {
if (error.message === 'Fetch cancelled') {
console.log('Fetch was cancelled');
} else {
console.error('An error occurred:', error);
}
});
// 假设在某个时刻用户决定取消请求
setTimeout(() => {
controller.abort(); // 触发取消操作
}, 6000); // 例如,如果请求超过6秒未完成则取消
结语
以上就是关于 Promise
的一些实战案例了,需要大家有所收获。