任务轮询 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 20 21 22 23 console.log("Ashuntefannao"); setTimeout(() => { console.log("setTimeout"); }); new Promise((resolve, reject) => { console.log("Promise中的同步代码"); resolve(); }) .then((_) => { console.log("Promise.then1"); }) .then((_) => { console.log("Promise.then2"); }); console.log("阿顺特烦恼"); # 执行结果 Ashuntefannao Promise中的同步代码 阿顺特烦恼 Promise.then1 Promise.then2 setTimeout
立即执行最前面的主线程同步代码,打印结果
执行setTimeout,将其放入异步宏任务队列
立即执行后续同步代码,在Promise中
执行到第一个then,将其放入到异步微任务队列中
执行到第二个then,将其放入到异步微任务队列中
立即执行末尾的主线程同步代码,打印结果
所有主线程同步代码执行完毕,在微任务队列中取出第一个then代码块,放入主线程并执行
主线程同步代码执行完毕,继续轮询微任务队列,取出第二个then代码块并执行。
主线程代码执行完毕,微任务队列为空,轮询异步宏任务队列
取出setTimeout中的代码块,放入主线程,并执行
主线程同步代码执行完毕,微任务队列为空,异步宏任务队列为空
脚本加载 引擎在执行任务时不会进行DOM渲染,所以如果把script
定义在前面,要先执行完任务后再渲染DOM。
解决:
将script
放在 BODY 结束标签前。
为script
标签添加type="module"
,也会延迟解析执行
为script
标签添加defer="defer"
属性
为script
标签添加async="async"
属性
defer/async
属性,只对外部脚本引入生效,多次使用该属性引入其它外部脚本,不能够保证先后顺序,若引入的多个脚本之间存在依赖关系,需要注意。
定时器 定时器会放入异步宏任务队列,需要等待同步任务、异步微任务执行完成后执行。
下面设置了 6 毫秒执行,如果主线程代码执行10毫秒,定时器要等主线程执行完才执行。
HTML标准规定最小时间不能低于4毫秒,有些异步操作如DOM操作最低是16毫秒,总之把时间设置大些对性能更好。
下面的代码会先输出 Ashuntefannao
之后输出 阿顺特烦恼
1 2 3 4 setTimeout(() => { console.log("阿顺特烦恼"); }, 0); console.log("Ashuntefannao");
微任务 微任务一般由用户代码产生,微任务较宏任务执行优先级更高,Promise.then
是典型的微任务,实例化 Promise 时执行的代码是同步的,then注册的回调函数是异步微任务。
任务的执行顺序是同步任务、微任务、宏任务所以下面执行结果是 1、2、3、4
1 2 3 4 5 6 7 8 9 10 setTimeout(() => console.log(4)); new Promise(resolve => { resolve(); console.log(1); }).then(_ => { console.log(3); }); console.log(2);
我们再来看下面稍复杂的任务代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 setTimeout(() => { console.log("定时器"); setTimeout(() => { console.log("timeout timeout"); }, 0); new Promise(resolve => { console.log("settimeout Promise"); resolve(); }).then(() => { console.log("settimeout then"); }); }, 0); new Promise(resolve => { console.log("Promise"); resolve(); }).then(() => { console.log("then"); }); console.log("阿顺特烦恼");
以上代码执行结果为
1 2 3 4 5 6 7 Promise 阿顺特烦恼 then 定时器 settimeout Promise settimeout then timeout timeout
实例操作 进度条 下面的定时器虽然都定时了一秒钟,但任务队列是按先进先出(先进先执行)原则,依次执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let i = 0; setTimeout(() => { console.log(++i); console.log("timeout1"); }, 1000); setTimeout(() => { console.log(++i); console.log("timeout2"); }, 1000); //一秒后打印结果 1 timeout1 2 timeout2
下面是一个进度条的示例,将每个数字放在一个任务中执行
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 26 27 28 29 30 31 <style> body { padding: 30px; } .loading { height: 30px; background: yellowgreen; width: 0; text-align: center; font-weight: bold; } </style> <body> <div class="loading"></div> </body> <script> let laodBox=document.querySelector(".loading"); function view() { let i = 0; (function handle() { laodBox.innerHTML = i + "%"; laodBox.style.width = i + "%"; if (i++ < 100) { setTimeout(handle, 20); } })(); } view(); console.log("定时器开始了..."); </script>
任务分解 一个比较耗时的任务可能造成游览器卡死现象,所以可以将任务拆分为多个异步小任务执行,暂时置于异步任务队列中,当主线程空闲时,在进行任务轮询。下面是一个数字统计的函数,我们会发现运行时间特别长
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { console.time("runtime"); function sub(num) { let count = 0; for (let i = 0; i <= num; i++) { count += i; } console.log(count); console.timeEnd("runtime"); } let num = 987654321; sub(num); console.log("阿顺特烦恼"); //需要等待上面执行完才会执行 }
现在把任务分解成小块放入任务队列,游览器就不会出现卡死的现象了,也不会影响后续代码的执行
执行run时,遇到第一个setTimeout,先将其添加到异步宏任务队列。
后面遇到同步代码,打印结果,又遇到异步宏任务,添加到任务队列中
同步代码执行完毕,取出第一个setTimeout到主线程并执行(运算for循环)
后面遇到同步代码,打印结果,又遇到异步宏任务,添加到任务队列中
同步代码执行完毕,取出第二个setTimeout到主线程并执行(调用run)
以此往复,任务轮询……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { let count = 0; let num = 987654321; function run() { setTimeout(() => { for (let i = 0; i < 100000000; i++) { if (num <= 0) break; count += num--; } }); if (num > 0) { console.log(num); setTimeout(run); } else { console.log(num); console.log(count); } } run(); console.log("阿顺特烦恼"); }
交给微任务处理是更好的选择
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { async function run(num) { let res = await Promise.resolve().then((_) => { let count = 0; for (let i = 0; i < num; i++) { count += num--; } return count; }); console.log(res); } run(987654321); console.log("阿顺特烦恼"); }