如何写一个预先不知道长度的数组读取的for循环进度条呢?
首先把进度条的ui样式准备好,代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>progress</title>
<style>
body,
html {
margin: 0;
width: 100%;
}
#container {
width: 90%;
height: 20px;
margin: 20px 5%;
box-sizing: border-box;
border: 1px solid #aaa;
border-radius: 3px;
position: relative;
}
#content {
width: 0;
height: 20px;
background: #16b777;
}
#text {
width: 18px;
height: 20px;
position: absolute;
left: calc(50% - 10px);
right: 0;
top: 0;
font-size: 14px;
line-height: 18px;
bottom: 0;
}
</style>
</head>
<body>
<div id="container">
<div id="content"></div>
<div id="text"></div>
</div>
<script>
const container = document.getElementById("container");
const content = document.getElementById("content");
const text = document.getElementById("text");
const width = container.clientWidth;
</script>
</body>
</html>
container是进度条的外层容器,content是进度条,text用来显示当前进度。进度条的实现就是通过改变content的长度就可以实现了,那如何改变呢?可能第一个想到的就是:
for(let i = 0; i < arr.length; i ++) {
content.style.width = i * width / arr.length + 'px';
text.innerText = (i * 100) / arr.length + "%";
}
但是这样写了之后,发现并没有达到预期的效果,进度条不是递增的,而是直接从0蹦到100%;为什么会这样呢?这要从js语言执行的事件循环机制说起,什么是事件循环呢?先看一张图:
所谓事件循环,就是图片最下层的消息队列,也被称为js的宏任务,js产生宏任务的方式有
- script(整体代码)
- setTimeout
- setInterval
- I/O
- UI交互事件
- postMessage
- MessageChannel
- setImmediate
- UI rendering
宏任务队列会挨个执行,每个宏任务队列都会有自己的执行环境栈,在执行环境栈中每个函数帧执行的时候,有可能会产生微任务,微任务都会添加到当前宏任务的最后,在这个宏任务执行完成后,立即执行微任务,当所有微任务执行完成之后,才会执行下一个宏任务。
js中会产生微任务的方式有:
- Promise.then
- Object.observe
- MutaionObserver
- process.nextTick
而浏览器会在每个宏任务执行完成(包括当前宏任务所有的微任务)之后,下一个宏任务开始执行之前,对页面进行渲染。
因此,使用上面的方法,没有办法达到进度条的效果,因为对dom元素的修改都是在同一个宏任务中进行的,当浏览器对页面进行渲染时,只能拿到最后一次给dom元素赋的值,所以就会从0直接蹦到100%。因此,如果要产生进度条效果,就需要生成多个宏任务,在每个宏任务中修改dom元素,这样才会有进度条的效果。
有了对这些概念的理解,就可以对代码做一些修改:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>progress</title>
<style>
body,
html {
margin: 0;
width: 100%;
}
#container {
width: 90%;
height: 20px;
margin: 20px 5%;
box-sizing: border-box;
border: 1px solid #aaa;
border-radius: 3px;
position: relative;
}
#content {
width: 0;
height: 20px;
background: #16b777;
}
#text {
width: 18px;
height: 20px;
position: absolute;
left: calc(50% - 10px);
right: 0;
top: 0;
font-size: 14px;
line-height: 18px;
bottom: 0;
}
</style>
</head>
<body onload="progress()">
<div id="container">
<div id="content"></div>
<div id="text"></div>
</div>
<script>
const container = document.getElementById("container");
const content = document.getElementById("content");
const text = document.getElementById("text");
const count = 10000;
const width = container.clientWidth;
function render(i) {
setTimeout(() => {
text.innerText = (i * 100) / count + "%";
content.style.width = (width * i) / count + "px";
}, 0);
}
function progress() {
for (let i = 0; i <= count; i++) {
render(i);
}
}
</script>
</body>
</html>
通过setTimeout产生宏任务,在宏任务中对dom元素做修改,这样就会有动态变化的效果。但是在实际运行过程中会发现,如果循环次数很多的时候,界面会先卡住一段时间,然后才出现进度条的变化。为什么呢?
因为js是单线程执行的,for循环开始执行,就会一直到全部循环完成,这个宏任务才算结束,然后后续的宏任务才会执行。怎样解决单线程执行造成的堵塞呢?可以使用async函数来解决。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>progress</title>
<style>
body,
html {
margin: 0;
width: 100%;
}
#container {
width: 90%;
height: 20px;
margin: 20px 5%;
box-sizing: border-box;
border: 1px solid #aaa;
border-radius: 3px;
position: relative;
}
#content {
width: 0;
height: 20px;
background: #16b777;
}
#text {
width: 18px;
height: 20px;
position: absolute;
left: calc(50% - 10px);
right: 0;
top: 0;
font-size: 14px;
line-height: 18px;
bottom: 0;
}
</style>
</head>
<body onload="progress()">
<div id="container">
<div id="content"></div>
<div id="text"></div>
</div>
<script>
const container = document.getElementById("container");
const content = document.getElementById("content");
const text = document.getElementById("text");
const count = 10000;
const width = container.clientWidth;
function render(i) {
return new Promise((resolve, reject) => {
setTimeout(() => {
text.innerText = (i * 100) / count + "%";
content.style.width = (width * i) / count + "px";
resolve();
}, 0);
});
}
async function progress() {
for (let i = 0; i <= count; i++) {
await render(i);
}
}
</script>
</body>
</html>
这样,就实现了一个效果还可以的进度条。