JavaScript 语言的一大特点就是单线程,也就是说同一个时间只能处理一个任务。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,(事件循环)Event Loop的方案应用而生。
JavaScript 处理任务是在等待任务、执行任务 、休眠等待新任务中不断循环中,也称这种机制为事件循环。
主线程中的任务执行完后,才执行任务队列中的任务
有新任务到来时会将其放入队列,采取先进先执行的策略执行队列中的任务
比如多个 setTimeout 同时到时间了,就要依次执行
任务包括 script(整体代码)、 setTimeout、setInterval、DOM渲染、DOM事件、Promise、XMLHTTPREQUEST等
原理分析
下面通过一个例子来详细分析宏任务与微任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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,然后输出
然后执行到setTimeout 异步宏任务,并将其放入宏任务队列,等待执行
之后执行到 Promise.then 微任务,并将其放入微任务队列,等待执行
然后执行到主代码输出
主线程所有任务处理完成
通过事件循环遍历微任务队列,将刚才放入的Promise.then微任务读取到主线程执行,然后输出
之后又执行 promse.then 产生新的微任务,并放入微任务队列
主线程任务执行完毕
现次事件循环遍历微任务队列,读取到promise2微任务放入主线程执行,然后输出
主线程任务执行完毕
此时微任务队列已经无任务,然后从宏任务队列中读取到 setTimeout任务并加入主线程,然后输出
脚本加载
引擎在执行任务时不会进行DOM渲染,所以如果把script 定义在前面,要先执行完任务后再渲染DOM,建议将script 放在 BODY 结束标签前。
定时器
定时器会放入异步任务队列,也需要等待同步任务执行完成后执行。
下面设置了 6 毫秒执行,如果主线程代码执行10毫秒,定时器要等主线程执行完才执行。
HTML标准规定最小时间不能低于4毫秒,有些异步操作如DOM操作最低是16毫秒,总之把时间设置大些对性能更好。
下面的代码会先输出 houdunren.com 之后输出 哈喽
1 2 3 4
| setTimeout(() => { console.log("哈喽"); }, 0); console.log("houdunren.com");
|
这是对定时器的说明,其他的异步操作如事件、XMLHTTPREQUEST 等逻辑是一样的
微任务
微任务一般由用户代码产生,微任务较宏任务执行优先级更高,Promise.then 是典型的微任务,实例化 Promise 时执行的代码是同步的,便then注册的回调函数是异步微任务的。
js 编译代码是先执行同步任务,遇到 settimeout、setInterval 等宏任务时会进入到宏任务队列中等待同步任务执行完毕后,在去宏任务队列中获取任务来执行。
1 2 3 4 5 6 7 8
| setTimeout(() => { console.log("宏任务"); }, 0); Promise.resolve().then((res) => { console.log("微任务"); }); console.log("同步任务");
|
实例操作
定时器的任务编排
下面代码执行结果为:等待 10000 循环完毕后同时打印出: 倒计时,倒计时 2,倒计时 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
setTimeout(() => { console.log("倒计时1"); }, 2000); setTimeout(() => { console.log("倒计时2"); }, 1000);
setTimeout(() => { console.log("倒计时"); }, 0); for (let a = 0; a < 10000; a++) { console.log(""); }
|
promise 微任务处理
运行结果:promise –> 同步任务 –> then –> settimeout
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| setTimeout(() => { console.log("settimeout"); }, 0); new Promise((resolve, reject) => { console.log("promise"); resolve(); }).then((res) => { console.log("then"); }); console.log("同步任务");
|
DOM 渲染任务
一般要把 js 放在页面底部处理,这样不会因为等待 js 执行而导致页面白屏等待
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head>
<body> <h2>hello,world</h2> </body> <script src="./js/1.js"></script> </html>
|
任务共享内存
运行结果:经过一秒后打印出 1 和 2
1 2 3 4 5 6 7 8 9 10 11 12
| let i = 0; setTimeout(() => { console.log(++i); }, 1000); setTimeout(() => { console.log(++i); }, 1000);
|
进度条体验任务轮询
1 2 3 4 5 6
| .progressBar { height: 30px; background-color: aquamarine; position: absolute; text-align: center; }
|
1
| <div class="progressBar"></div>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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();
|
任务拆分成多个子任务
1 2 3 4 5 6 7 8 9 10 11 12 13
| .box { width: 300px; height: 20px; position: relative; border: 1px solid; }
.progre { position: absolute; height: 100%; width: 0%; background-color: #09c; }
|
1 2 3 4 5
| <div class="box"> <div class="progre"></div> </div> <div class="progretext">0</div> <button onclick="hd()">开始下载</button>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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--; } 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 微任务处理复杂任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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("同步任务");
|
本文借鉴于:
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