最近在写一个Node.js程序,功能是下载页面上的资源,首先拿到页面资源链接列表,如:
[
'https://xxx.com/img/logo.jpg',
'https://xxx.com/img/bg.jpg',
'https://xxx.com/css/main.css',
'https://xxx.com/css/animate.css',
'https://xxx.com/js/jquery.js',
'https://xxx.com/js/form.js',
...
]
要求是资源并行下载,所有资源下载结束后通知,收集错误的下载链接。
如果是传统做法是遍历数组发送请求,声明一个变量记录请求数,不管成功或失败,结束后都给这个变量+1,并且调用一个函数,这个函数判断当前变量是否等于数组长度,相等则表示所有请求已经完成。
// pseudo code
var count = 0
var errs = []
var data = [...]
function request(url) {
ajax({url: url})
.success(function () {
count++
callback()
})
.fail(function () {
count++
errs.push(...)
callback()
})
}
function callback() {
if (count === data.length) {
console.log('done!')
}
}
data.forEach(request)
因为请求是异步的,我们也无法确定每个请求花费的时间,所以只能在回调里处理。现在我们有了Promise
,async-await
,支持同步的写法,那可以怎么写呢?
我们用setTimeout
来模拟请求,数据data = [500, 400, 300, 200, 100]
既是每个请求返回的数据也是每个请求所需的时间。
如果是继发请求(一个请求结束后再请求后一个),那么应该是按顺序打印,理论上所有请求的总时间等于每个请求所花时间之和,约等于1500ms
;如果是并发请求(假设请求数不会太多,不超过限制),顺序是按时间从小到大打印,理论上所有请求的总时间等于最长的那个时间,约等于500ms
。
首先先看下怎么并行请求和请求结束确定
// 模拟请求
function request(param) {
return new Promise(resolve => {
setTimeout(() => {
console.log(param)
resolve()
}, param)
})
}
const items = [500, 400, 300, 200, 100]
✘ 直接for循环
(() => {
for (let item of items) {
request(item)
}
console.log('end')
})()
// 输出:end, 100, 200, 300, 400, 500
上面的输出可以看出,请求是并行的,但是无法确定什么结束
✘ for循环,使用async-await
(async () => {
for (let item of items) {
await request(item)
}
console.log('end')
})()
// 输出:500, 400, 300, 200, 100, end
上面的代码可以看出,虽然确定了结束,但请求是继发的
✔ 使用Promise.all
(() => {
Promise.all(items.map(request)).then(res => {
console.log('end')
})
})()
// 输出:100, 200, 300, 400, 500, end
上面的代码可以看出,请求是并发的,并且在所有请求结束后打印end
,满足条件
我们不能保证所有的请求都是正常的,接下来看看当有请求出错时怎么处理,假设200
的请求出错
function request(param) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (param === 200) {
// console.log(param, ' failed')
return reject({
status: 'error',
data: param
})
}
// console.log(param, ' success')
resolve({
status: 'success',
data: param
})
}, param)
})
}
const items = [500, 400, 300, 200, 100]
Promise
有catch
方法捕获错误,最近新增的finally
方法能在最后执行
(() => {
Promise.all(items.map(request))
.then(res => {
console.log(res)
})
.catch (err => {
console.log(err)
})
.finally(res => {
console.log('end', res)
})
})()
// 输出 {status: "error", data: 200}, end, undefined
上面的输出可以看出,如果有错误,则不会进入then
,而是进入catch
,然后进入finally
,但是finally
不接受参数,只告诉你结束了。如果把上面模拟请求的console.log(...)
注释去掉,还会发现finally
是在catch
结束后就执行了,而200
后面的请求还未结束。
接下来我们改造下模拟请求,在请求出错后就catch
错误
function request(param) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (param === 200) {
// console.log(param, ' failed')
return reject({
status: 'error',
data: param
})
}
// console.log(param, ' success')
resolve({
status: 'success',
data: param
})
}, param)
}).catch(err => err)
}
(() => {
Promise.all(items.map(request))
.then(res => {
console.log(res, 'end')
})
})()
// 输出 [{…}, {…}, {…}, {stauts: 'error', data: 200}, {…}], end
这样就可以在then
中拿到全部的结果了,如果要用for
循环的话也是可以的
(async () => {
const temp = []
// 这个for循环的作用和上面的map类似
for (let item of items) {
temp.push(request(item))
}
const result = []
for (let t of temp) {
result.push(await t)
}
console.log(result, 'end')
})()
// 输出与上面一致
第一个for
循环保证并发请求,保存了Promise
,第二个循环加入await
保证按顺序执行。
好了,以上就是全部内容,你有更好的写法吗?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。