JavaScript 语言的一大特点就是单线程,也就是说同一个时间只能处理一个任务。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,(事件循环)Event Loop的方案应用而生。
JavaScript 处理任务是在等待任务、执行任务 、休眠等待新任务中不断循环中,也称这种机制为事件循环。
主线程中的任务执行完后,才执行任务队列中的任务
有新任务到来时会将其放入队列,采取先进先执行的策略执行队列中的任务
比如多个 setTimeout 同时到时间了,就要依次执行
任务包括 script(整体代码)、 setTimeout、setInterval、DOM渲染、DOM事件、Promise、XMLHTTPREQUEST等
原理分析
下面通过一个例子来详细分析宏任务与微任务
console.log("哈喽");
setTimeout(function() {
console.log("定时器");
}, 0);
Promise.resolve()
.then(function() {
console.log("promise1");
})
.then(function() {
console.log("promise2");
});
console.log("houdunren.com");
#输出结果为
哈喽
houdunren.com
promise1
promise2
定时器
- 先执最前面的宏任务 script,然后输出
script start
-
然后执行到setTimeout 异步宏任务,并将其放入宏任务队列,等待执行
-
之后执行到 Promise.then 微任务,并将其放入微任务队列,等待执行
-
然后执行到主代码输出
script end
-
主线程所有任务处理完成
-
通过事件循环遍历微任务队列,将刚才放入的Promise.then微任务读取到主线程执行,然后输出
promise1
-
之后又执行 promse.then 产生新的微任务,并放入微任务队列
-
主线程任务执行完毕
-
现次事件循环遍历微任务队列,读取到promise2微任务放入主线程执行,然后输出
promise2
-
主线程任务执行完毕
-
此时微任务队列已经无任务,然后从宏任务队列中读取到 setTimeout任务并加入主线程,然后输出
setTimeout
image.png
脚本加载
引擎在执行任务时不会进行DOM渲染,所以如果把script 定义在前面,要先执行完任务后再渲染DOM,建议将script 放在 BODY 结束标签前。
定时器
定时器会放入异步任务队列,也需要等待同步任务执行完成后执行。
下面设置了 6 毫秒执行,如果主线程代码执行10毫秒,定时器要等主线程执行完才执行。
HTML标准规定最小时间不能低于4毫秒,有些异步操作如DOM操作最低是16毫秒,总之把时间设置大些对性能更好。
setTimeout(func,6);
下面的代码会先输出 houdunren.com 之后输出 哈喽
setTimeout(() => {
console.log("哈喽");
}, 0);
console.log("houdunren.com");
这是对定时器的说明,其他的异步操作如事件、XMLHTTPREQUEST 等逻辑是一样的
微任务
微任务一般由用户代码产生,微任务较宏任务执行优先级更高,Promise.then 是典型的微任务,实例化 Promise 时执行的代码是同步的,便then注册的回调函数是异步微任务的。
js 编译代码是先执行同步任务,遇到 settimeout、setInterval 等宏任务时会进入到宏任务队列中等待同步任务执行完毕后,在去宏任务队列中获取任务来执行。
setTimeout(() => {
console.log("宏任务");
}, 0);
Promise.resolve().then((res) => {
console.log("微任务");
});
console.log("同步任务");
// 返回的打印顺序为: 同步任务 微任务 宏任务
实例操作
定时器的任务编排
下面代码执行结果为:等待 10000 循环完毕后同时打印出: 倒计时,倒计时 2,倒计时 1
// 浏览器在正常解析式遇到定时器会把定时器放在定时器的队列中去开始计时
// 等待倒计时结束后会把任务放在宏任务队列中
// 等待同步任务执行完毕后直接调用宏任务队列中的方法执行
// 如果有多个倒计时,则那个倒计时先计时完毕会先优先进入宏任务队列中
setTimeout(() => {
console.log("倒计时1");
}, 2000);
setTimeout(() => {
console.log("倒计时2");
}, 1000);
// 倒计时会有一个最短计时,即使设置为0毫秒后执行,js也会默认给上一个4毫秒的延迟在执行
// 所以0毫秒的倒计时不是真正的0毫秒倒计时
setTimeout(() => {
console.log("倒计时");
}, 0);
for (let a = 0; a < 10000; a++) {
console.log("");
}
// 执行后可以看到等待10000次循环完毕后不是再等待2秒打印结果,而是同时打印出倒计时2和倒计时1
// 并且倒计时2是在倒计时1之前先出来
// 这是因为js在解析代码过程中遇到定时器后会把定时器放进定时器模块中开始倒计时
// 这是代码会继续往下执行同步任务,等待同步任务执行完毕后去宏任务队列中找可以执行的代码
// 如果宏任务队列中有等待执行的代码就拿过来执行,否则就进行休眠等待下一次解析
promise 微任务处理
运行结果:promise –> 同步任务 –> then –> settimeout
setTimeout(() => {
console.log("settimeout");
}, 0);
new Promise((resolve, reject) => {
// new promise 方法中属于同步任务
console.log("promise");
resolve();
}).then((res) => {
console.log("then");
});
console.log("同步任务");
// 分析:代码解析式遇到settimeout则开始倒计时,倒计时完毕后吧任务放在宏任务队列中
// promise中属于同步任务,所以先打印出promise
// 然后遇到then,promise返回状态后进入微任务队列,同样等待同步任务执行
// 紧接着遇到同步任务打印出同步任务
// 同步任务执行完毕后执行微任务打印then
// 微任务执行完毕后执行宏任务打印settimeout
DOM 渲染任务
一般要把 js 放在页面底部处理,这样不会因为等待 js 执行而导致页面白屏等待
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<!-- 如果js放在DOM前面,则会等待js业务逻辑处理完毕后再去渲染DOM
会导致页面在js处理期间显示白屏,造成体验不友好,所以通常把js模块放在页面底部
-->
<!-- <script src="./js/1.js"></script> -->
<body>
<h2>hello,world</h2>
</body>
<script src="./js/1.js"></script>
</html>
任务共享内存
运行结果:经过一秒后打印出 1 和 2
let i = 0;
setTimeout(() => {
console.log(++i);
}, 1000);
setTimeout(() => {
console.log(++i);
}, 1000);
// 解析:js解析到第一个计时器后会把第一个计时器放在宏任务队列中,然后接着把第二个计时器放在
// 宏任务队列中,同步任务执行完毕后去宏任务队列获取任务
// 第一个任务执行完毕后改变了i的值,此时i的值变成了1
// 然后执行第二个任务,经过上个任务的处理,i的值已经是1,在经过++变成2
// 所以打印结果为倒计时1秒后打印出 1 2
进度条体验任务轮询
.progressBar {
height: 30px;
background-color: aquamarine;
position: absolute;
text-align: center;
}
<div class="progressBar"></div>
function progre() {
let i = 0;
let div = document.querySelector(".progressBar");
(function run() {
if (++i <= 100) {
div.innerHTML = i;
div.style.width = i + "%";
setTimeout(run, 10);
}
})();
}
progre();
// 代码解析:先执行progre方法,方法内部存在自执行函数run,run方法会产生一个计时器放在
// 定时器会生成宏任务进入到宏任务队列中,计时器调用run方法重复生成新的宏任务
// 循环往复会产生多个宏任务,if判断i的值小于等于100时不再产生新的宏任务
任务拆分成多个子任务
.box {
width: 300px;
height: 20px;
position: relative;
border: 1px solid;
}
.progre {
position: absolute;
height: 100%;
width: 0%;
background-color: #09c;
}
<div class="box">
<div class="progre"></div>
</div>
<div class="progretext">0</div>
<button onclick="hd()">开始下载</button>
let num = 984755554;
let nums = 984755554;
let count = 0;
let progrediv = document.querySelector(".progre");
let progretext = document.querySelector(".progretext");
function hd() {
// 每次先循环一部分
for (let i = 0; i < 1000000; i++) {
if (num <= 0) break;
count = num--;
}
// 循环完后判断是否还有值未参与循环
// 如果大于0表示该有值未参与循环,则放到宏任务中执行后续循环
// 不影响下面的同步任务继续执行
if (num > 0) {
setTimeout(hd);
}
let progrs = (100 - (num / nums) * 100).toFixed(2) + "%";
progrediv.style.width = progrs;
progretext.innerHTML = progrs;
if (num === 0) {
progrediv.style.background = "#5edc63";
}
console.log(progrs);
}
promisec 微任务处理复杂任务
// 利用异步执行先执行完同步任务后再去执行微任务
async function hd(num) {
let res = await Promise.resolve().then((_) => {
let count = 0;
for (let i = 0; i < num; i++) {
count += num--;
}
return count;
});
console.log(res);
}
hd(456845789);
console.log("同步任务");
// 打印结果
// 同步任务 78265528430191060
本文借鉴于:
https://szxio.gitee.io/hexoblog/JavaScript/MacroTask/
https://doc.houdunren.com/js/16%20%E4%BB%BB%E5%8A%A1%E7%AE%A1%E7%90%86.html#%E5%BE%AE%E4%BB%BB%E5%8A%A1
网友评论