JS事件循環(huán)-微任務(wù)-宏任務(wù)(原理講解+面試題分析)
前言
JS代碼在運(yùn)行時(shí),有兩種運(yùn)行環(huán)境。
一是在瀏覽器中,二是在node中。
由于JS線程是單線程,在運(yùn)行JS代碼時(shí),可能會(huì)遇到比較耗時(shí)的操作,比如setTimeout,或者是發(fā)送網(wǎng)絡(luò)請(qǐng)求等,又由于JS線程是單線程,如果在解析耗時(shí)的代碼時(shí)候,停在了這里,那執(zhí)行代碼的性能將是比較低的。
為了解決此問題,在瀏覽器、node環(huán)境下,其實(shí)是有事件循環(huán)機(jī)制的。
瀏覽器的事件循環(huán)
瀏覽器的事件循環(huán)
JS線程執(zhí)行代碼時(shí)候,遇到比較耗時(shí)的操作時(shí),將這些操作交給瀏覽器去處理,然后這些操作會(huì)根據(jù)不同的種類放進(jìn)微任務(wù)隊(duì)列或者宏任務(wù)隊(duì)列,宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列都不為空的時(shí)候,只有等微任務(wù)隊(duì)列為空,即微任務(wù)隊(duì)列里面的事件全部都執(zhí)行完之后,才會(huì)再去讓宏任務(wù)隊(duì)列中的事件出棧,之后交由JS線程去處理,執(zhí)行代碼。
事件循環(huán)大概就是如圖所示的流程:
瀏覽器的宏任務(wù)、微任務(wù)
其實(shí),在瀏覽器拿到那些有些不能同步處理的事件的時(shí)候,有的會(huì)加入宏任務(wù)隊(duì)列,有的會(huì)加入微任務(wù)隊(duì)列,那么一般我們?nèi)绾螀^(qū)分呢?
一般情況下:加入宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列的事件如下:
宏任務(wù)隊(duì)列(macrotask queue):ajax、setTimeout、setInterval、DOM監(jiān)聽、UI Rendering等
微任務(wù)隊(duì)列(microtask queue):Promise的then回調(diào)、 Mutation Observer API、queueMicrotask()。
那么這些事件的執(zhí)行順序是怎么樣子的呢?
首先,有一個(gè)原則,宏任務(wù)隊(duì)列里面的事件,要執(zhí)行的話,一定是在確保微任務(wù)隊(duì)列為空的情況下,即微任務(wù)隊(duì)列里面的事件全部執(zhí)行完的情況。
其次,main script里面的內(nèi)容是最先執(zhí)行的。
由此,可以得到執(zhí)行順序?yàn)椋簃ain script > 微任務(wù)隊(duì)列里面的事件 > 宏任務(wù)里面的事件。
面試題一
題目如下:
setTimeout(function () { console.log("setTimeout1"); new Promise(function (resolve) { resolve(); }).then(function () { new Promise(function (resolve) { resolve(); }).then(function () { console.log("then4"); }); console.log("then2"); }); }); new Promise(function (resolve) { console.log("promise1"); resolve(); }).then(function () { console.log("then1"); }); setTimeout(function () { console.log("setTimeout2"); }); console.log(2); queueMicrotask(() => { console.log("queueMicrotask1") }); new Promise(function (resolve) { resolve(); }).then(function () { console.log("then3"); }); // promise1 // 2 // then1 // queueMicrotask1 // then3 // setTimeout1 // then2 // then4 // setTimeout2
分析如下:
在第一個(gè)事件循環(huán)里面,main script、宏任務(wù)、微任務(wù)里面的事件如下:
在判斷加入宏任務(wù)隊(duì)列還是微任務(wù)隊(duì)列時(shí)候,遵循如下原則:
宏任務(wù)隊(duì)列(macrotask queue):ajax、setTimeout、setInterval、DOM監(jiān)聽、UI Rendering等
微任務(wù)隊(duì)列(microtask queue):Promise的then回調(diào)、 Mutation Observer
API、queueMicrotask()。
按照這個(gè)原則,第一輪事件循環(huán)里面的事件如下:
先執(zhí)行main script、然后微任務(wù)隊(duì)列里面的,最后是宏任務(wù)隊(duì)列里面的
// promise1
// 2
// then1
// queueMicrotask1
// then3
之后執(zhí)行setTimeout1的宏任務(wù),此時(shí)第二輪事件循環(huán)里面的內(nèi)容如下:
第二輪事件循環(huán)執(zhí)行內(nèi)容如下:
// setTimeout1
// then2
// then4
// setTimeout2
綜上:最后執(zhí)行結(jié)果為:
// promise1 // 2 // then1 // queueMicrotask1 // then3 // setTimeout1 // then2 // then4 // setTimeout2
面試題二
題目如下:
// async function bar() { // console.log("22222") // return new Promise((resolve) => { // resolve() // }) // } // async function foo() { // console.log("111111") // await bar() // console.log("33333") // } // foo() // console.log("444444") async function async1 () { console.log('async1 start') await async2(); console.log('async1 end') } async function async2 () { console.log('async2') } console.log('script start') setTimeout(function () { console.log('setTimeout') }, 0) async1(); new Promise (function (resolve) { console.log('promise1') resolve(); }).then (function () { console.log('promise2') }) console.log('script end') // script start // async1 start // async2 // promise1 // script end // async1 end // promise2 // setTimeout
第一輪事件循環(huán)里面的事件如下:
然后按照順序執(zhí)行,最后結(jié)果如下:
// script start // async1 start // async2 // promise1 // script end // async1 end // promise2 // setTimeout
node的事件循環(huán)
node的事件循環(huán)
瀏覽器的事件循環(huán)是是根據(jù)HTML5定義的規(guī)范來實(shí)現(xiàn)的,不同的瀏覽器可能會(huì)有不同的實(shí)現(xiàn),而Node中是由libuv實(shí)現(xiàn)的。
首先我們看一下node的架構(gòu)圖:
我們可以從圖中大致看出,事件循環(huán)是在libuv中實(shí)現(xiàn)的,libuv主要維護(hù)的是一個(gè)事件循環(huán)(Event Loop)和 線程池(worker threads)。
libuv是一個(gè)多平臺(tái)的專注于異步IO的庫,它最初是為Node開發(fā)的,但是現(xiàn)在也被使用到Luvit、Julia、pyuv等其他地方;
EventLoop負(fù)責(zé)調(diào)用系統(tǒng)的一些其他操作:文件的IO、Network、child-processes等
由圖可以看出,事件循環(huán)就像是一個(gè)橋梁,是連接著應(yīng)用程序的JavaScript(左邊部分)和系統(tǒng)調(diào)用(右邊線程池部分)之間的通道:
無論是我們的文件IO、數(shù)據(jù)庫、網(wǎng)絡(luò)IO、定時(shí)器、子進(jìn)程,在完成對(duì)應(yīng)的操作后,都會(huì)將對(duì)應(yīng)的結(jié)果和回調(diào)函數(shù)放到事件循環(huán)(任務(wù)隊(duì)列)中;
事件循環(huán)會(huì)不斷的從任務(wù)隊(duì)列中取出對(duì)應(yīng)的事件(回調(diào)函數(shù))來執(zhí)行;
但是一次完整的事件循環(huán)Tick分成很多個(gè)階段:
- 定時(shí)器(Timers):本階段執(zhí)行已經(jīng)被 setTimeout() 和 setInterval() 的調(diào)度回調(diào)函數(shù)。
- 待定回調(diào)(Pending Callback):對(duì)某些系統(tǒng)操作(如TCP錯(cuò)誤類型)執(zhí)行回調(diào),比如TCP連接時(shí)接收到ECONNREFUSED。idle, prepare:僅系統(tǒng)內(nèi)部使用。
- 輪詢(Poll):檢索新的 I/O 事件;執(zhí)行與 I/O 相關(guān)的回調(diào);
- 檢測(check):setImmediate() 回調(diào)函數(shù)在這里執(zhí)行。
- 關(guān)閉的回調(diào)函數(shù):一些關(guān)閉的回調(diào)函數(shù),如:socket.on(‘close’, …)
node的宏任務(wù)、微任務(wù)
node中也有微任務(wù)和宏任務(wù),執(zhí)行的原則和在瀏覽器中一樣,是先執(zhí)行微任務(wù),然后再執(zhí)行宏任務(wù),但是對(duì)于宏任務(wù)來說,是按照上圖從上到下的順序執(zhí)行的。
具體對(duì)應(yīng)的常見事件的執(zhí)行順序如下;
在微任務(wù)隊(duì)列中:
- next tick queue:process.nextTick;
- other queue:Promise的then回調(diào)、queueMicrotask;
(是按照從上往下的事件順序執(zhí)行)
在宏任務(wù)隊(duì)列:
- timer queue:setTimeout、setInterval;
- poll queue:IO事件;
- check queue:setImmediate;
- close queue:close事件
(同樣是按照從上往下的事件順序執(zhí)行)
所以,綜上所述,在每一次事件循環(huán)的tick中,會(huì)按照如下順序來執(zhí)行代碼:
next tick microtask queue;
other microtask queue;
timer queue;
poll queue;
check queue;
close queue
當(dāng)然,main script 依舊是最先執(zhí)行的,只有main script執(zhí)行結(jié)束后,才會(huì)按照上述順序來執(zhí)行代碼。
面試題一
async function async1() { console.log('async1 start') await async2() console.log('async1 end') } async function async2() { console.log('async2') } console.log('script start') setTimeout(function () { console.log('setTimeout0') }, 0) setTimeout(function () { console.log('setTimeout2') }, 300) setImmediate(() => console.log('setImmediate')); process.nextTick(() => console.log('nextTick1')); async1(); process.nextTick(() => console.log('nextTick2')); new Promise(function (resolve) { console.log('promise1') resolve(); console.log('promise2') }).then(function () { console.log('promise3') }) console.log('script end') // script start // async1 start // async2 // promise1 // promise2 // script end // nexttick1 // nexttick2 // async1 end // promise3 // settimetout0 // setImmediate // setTimeout2
第一輪事件循環(huán)里面的事件如下:
按照順序自左向右執(zhí)行,3s后執(zhí)行setTimeout2,
最后的結(jié)果是:
// script start
// async1 start
// async2
// promise1
// promise2
// script end
// nexttick1
// nexttick2
// async1 end
// promise3
// settimetout0
// setImmediate
// setTimeout2
到此這篇關(guān)于JS事件循環(huán)-微任務(wù)-宏任務(wù)(原理講解+面試題分析)的文章就介紹到這了,更多相關(guān)循環(huán)-微任務(wù)-宏任務(wù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Js類的靜態(tài)方法與實(shí)例方法區(qū)分及jQuery拓展的兩種方法
這篇文章主要介紹了Js類的靜態(tài)方法與實(shí)例方法區(qū)分及jQuery拓展的兩種方法 的相關(guān)資料,對(duì)靜態(tài)方法(Static)和實(shí)例方法(非Static)不太理解的朋友可以一起學(xué)習(xí)下2016-06-06Javascript中設(shè)置默認(rèn)參數(shù)值示例
這篇文章主要介紹了Javascript中默認(rèn)參數(shù)值的設(shè)置,很簡單,但很實(shí)用,需要的朋友可以參考下2014-09-09js實(shí)現(xiàn)網(wǎng)頁右上角滑出會(huì)自動(dòng)消失大幅廣告的方法
這篇文章主要介紹了js實(shí)現(xiàn)網(wǎng)頁右上角滑出會(huì)自動(dòng)消失大幅廣告的方法,是javascript廣告特效的典型應(yīng)用,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-02-02微信小程序?qū)崿F(xiàn)動(dòng)態(tài)顯示和隱藏某個(gè)控件功能示例
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)動(dòng)態(tài)顯示和隱藏某個(gè)控件功能,涉及微信小程序事件響應(yīng)及樣式動(dòng)態(tài)操作相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2018-12-12Js參數(shù)RSA加密傳輸之jsencrypt.js的使用
這篇文章主要介紹了Js參數(shù)RSA加密傳輸之jsencrypt.js的使用,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02JavaScript遍歷json對(duì)象數(shù)據(jù)的方法
這篇文章介紹了JavaScript遍歷json對(duì)象數(shù)據(jù)的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04