文章目录
背景&需求
开发过程中,碰到了一个批量下载需求,众所周知,一般碰到批量下载都是下载一个压缩包,但因为此次设计原因,要做成分批次下载多张图片,数据结构是一个List<"http链接">,于是我想到了创建a标签,模拟用户点击行为去逐次下载,但在这个过程中遇到了一些问题,在此分析一下。
一、使用a标签循环模拟点击事件实现
示例:
如上图所示,需要点击下载时,将此list中的内容通过浏览器下载到用户本地,于是我使用创建a标签元素的方式,循环模拟点击。代码如下:
// 批量下载
funBatchDownload(list) {
list.map(item => {
this.funDownload(item)
})
},
funDownload(url) {
let link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', '附件')
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
但此段代码在运行过程中,发现无法在浏览器下载的同时去创建多个a元素,需要在执行
funDownload 函数时设置定时器,来解决问题,此时大多数人可能会将代码修改成:
funBatchDownload(list) {
list.map(item => {
setTimeout(function () {
this.funDownload(item)
}, 2000)
})
}
但运行时将会发现,这个定时器写了跟没写一样,下载函数还是同时执行的,这是为什么呢?
二、JS的事件循环机制
JavaScript的事件运行机制是基于浏览器内部的事件循环(Event Loop)。简单来说,事件循环是一个处理所有异步代码(如事件、定时器、网络请求等)的机制。
事件循环(Event Loop):主线程从"任务队列"中读取事件这个过程是循环不断的。
1、同步任务在主线程中执行,形成“执行栈”
2、另外还有一个异步任务队列,异步任务有了结果后,在异步任务队列放一个事件,等待执行
3、同步任务和异步任务进入不同的执行场所,同步任务进入执行栈,异步任务进入eventTable并注册事件
4、异步任务有了结果后,将事件推入异步任务队列
5、所有的同步任务执行完后,去异步任务队列上读取可执行的异步任务,如果有就推入主线程执行
6、主线程不断重复上面的步骤
简单来说,事件循环机制会将此页面栈的同步和异步任务拆分开成两个执行队列,异步任务在同步任务执行完毕后执行。常见的异步任务有:定时器、网络请求、Promise等等。
观察如下代码:
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log('我想在五秒内连续打印这句话')
}, 1000);
}
console.log('a');
经典面试题,这段代码的输出结果是:
先输出a。再同时打印出这句话。原因:当js代码运行到有定时器的地方的时候,会把定时器操作放在任务队列的尾部,然后跟它说:“你先排队吧,还没有轮到你,因为同步代码还没有执行完。” 也就是说,js认为setTimeout是一个异步操作,必须让它排队,它只能在同步代码执行结束后才能执行。那么要如何实现文章开头所述需求呢?
三、如何实现for循环中添加定时器
可以使用闭包和立即执行函数改变作用域,在每次循环中存储 i ,实现每隔一秒执行一次for循环中内容,如下
// 批量下载
funBatchDownload(list) {
let vm = this
for (let i = 0; i < list.length; i++) {
(function (i) {
setTimeout(function () {
vm.funDownload(list[i])
}, i * 1000)
})(i)
}
},
funDownload(url) {
let link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', '附件')
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},