1、Javascript中的事件循環機制
首先,因為JavaScript是一門單線程的語言。意味著著同一時間內只能做一件事,那么就會存在阻塞現象(比如一個線程刪除了這個DOM節點,一個線程需要操作這個DMO節點就出現沖突),而實現單線程非阻塞的方法就是事件循環。
在JavaScript中,所有的任務都可以分為
1)同步任務:立即執行的任務,同步任務一般會直接進入到主線程中執行。
2)異步任務:異步執行的任務,比如ajax網絡請求,setTimeout定時函數等。異步任務還可以細分為微任務與宏任務。不同的任務源會被分配到不同的Task隊列中,任務源可以分為 微任務(microtask) 和 宏任務(macrotask)。在ES6規范中,microtask稱為jobs,macrotask稱為task。
常見的宏任務:整體代碼,setTimeout,setInterval,setImmediate,I/O操作,postMessage、MessageChannel,UI rendering/UI事件。
常見的微任務:new Promise().then,MutaionObserver(前端的回溯),Object.observe(已廢棄;Proxy 對象替代),process.nextTick(Node.js)。
為什么進入微任務的概念?只有宏任務可以嗎?由于回調函數的執行順序是遵循先進先出的原則。如果存在高優先級的的任務,回調函數無法立即執行,所以引入了微任務的概念。宏任務執行完一遍后,先去微任務隊列把微任務執行完后再進行下一輪。
1)執行一個宏任務,如果遇到微任務就將它放到微任務的事件隊列中。
2)當前宏任務執行完成后,會查看微任務的事件隊列,然后將里面的所有微任務依次執行完。循環往復,直到兩個 queue 中的任務都取完。
Event loop 順序:
a、執行同步代碼,這屬于宏任務
b、執行棧為空,查詢是否有微任務需要執行
c、執行所有微任務
d、必要的話渲染 UI
e、然后開始下一輪Event loop,執行宏任務中的異步代碼
通過上述的Event loop順序可知,如果宏任務中的異步代碼有大量的計算并且需要操作DOM的話,為了更快的響應界面響應,我們可以把操作DOM放入微任務中。
例題1:
解析:遇到 console.log(1) ,直接打印 1; 遇到定時器,屬于新的宏任務,留著后面執行;遇到 new Promise,這個是直接執行的,打印 'new Promise'; .then 屬于微任務,放入微任務隊列,后面再執行;遇到 console.log(3) 直接打印 3; 好了本輪宏任務執行完畢,現在去微任務列表查看是否有微任務,發現 .then 的回調,執行它,打印 'then';當一次宏任務執行完,再去執行新的宏任務,這里就剩一個定時器的宏任務了,執行它,打印 2;
第一輪:主線程開始執行,遇到setTimeout,將setTimeout的回調函數丟到宏任務隊列中,在往下執行new Promise立即執行,輸出2,then的回調函數丟到微任務隊列中,再繼續執行,遇到process.nextTick,同樣將回調函數扔到為任務隊列,再繼續執行,輸出5,當所有同步任務執行完成后看有沒有可以執行的微任務,發現有then函數和nextTick兩個微任務,先執行哪個呢?process.nextTick指定的異步任務總是發生在所有異步任務之前,因此先執行process.nextTick輸出4然后執行then函數輸出3,第一輪執行結束。
第二輪:從宏任務隊列開始,發現setTimeout回調,輸出1執行完畢,因此結果是25431
面試回答:
首先js 是單線程運行的,在代碼執行的時候,通過將不同函數的執行上下文壓入執行棧中來保證代碼的有序執行
在執行同步代碼的時候,如果遇到了異步事件,js 引擎并不會一直等待其返回結果,而是會將這個事件掛起,繼續執行執行棧中的其他任務
當同步事件執行完畢后,再將異步事件對應的回調加入到與當前執行棧中不同的另一個任務隊列中等待執行
任務隊列可以分為宏任務對列和微任務對列,當當前執行棧中的事件執行完畢后,js 引擎首先會判斷微任務對列中是否有任務可以執行,如果有就將微任務隊首的事件壓入棧中執行
當微任務對列中的任務都執行完成后再去判斷宏任務對列中的任務。
2、Node中 的事件循環機制
Node中的微任務存在2種:process.nextTick() 注冊的回調 (nextTick task queue);promise.then() 注冊的回調 (promise task queue)
Node 在執行微任務時, 會優先執行 nextTick task queue 中的任務,執行完之后會接著執行 promise task queue 中的任務。所以如果 process.nextTick 的回調與 promise.then 的回調都處于主線程或事件循環中的同一階段,process.nextTick 的回調要優先于? promise.then 的回調執行。
Node中的宏任務存在4種:setTimeout、setInterval、setImmediate和I/O。
宏任務在微任務執行之后執行,因此在同一個事件循環周期內,如果既存在微任務隊列又存在宏任務隊列,那么優先將微任務隊列清空,再執行宏任務隊列。
Node中事件循環分為六個階段:
由于Pending callbacks、Idle/Prepare 和 Close callbacks 階段是 Node 內部使用的三個階段,所以這里主要分析與開發者代碼執行更為直接關聯的Timers、Poll 和 Check 三個階段。
1)Timers(計時器階段):初次進入事件循環,會從計時器階段開始。此階段會判斷是否存在過期的計時器回調(包含 setTimeout 和 setInterval),如果存在則會執行所有過期的計時器回調,執行完畢后,如果回調中觸發了相應的微任務,會接著執行所有微任務,執行完微任務后再進入 Pending callbacks 階段。
2)Pending callbacks:執行推遲到下一個循環迭代的I / O回調(系統調用相關的回調)。
3)Idle/Prepare:僅供系統內部使用。
4)Poll(輪詢階段):
a、當回調隊列不為空時:
會執行回調,若回調中觸發了相應的微任務,這里的微任務執行時機和其他地方有所不同,不會等到所有回調執行完畢后才執行,而是針對每一個回調執行完畢后,就執行相應微任務。執行完所有的回掉后,變為下面的情況。
b、當回調隊列為空時(沒有回調或所有回調執行完畢):
但如果存在有計時器(setTimeout、setInterval和setImmediate)沒有執行,會結束輪詢階段,進入 Check 階段。否則會阻塞并等待任何正在執行的I/O操作完成,并馬上執行相應的回調,直到所有回調執行完畢。
5)Check(查詢階段):會檢查是否存在 setImmediate 相關的回調,如果存在則執行所有回調,執行完畢后,如果回調中觸發了相應的微任務,會接著執行所有微任務,執行完微任務后再進入 Close callbacks 階段。
6)Close callbacks:執行一些關閉回調,比如socket.on('close', ...)等。
注意:宏任務和微任務在node中的執行順序。
在Node V10及以前,執行完一個階段的所有任務,再執行process.nextTick 的回調,再執行微任務隊列的內容。如promise.then 的回調執行。
Node V10及以后和瀏覽器一致。
解析:第一個事件循環主線程發起,因此先執行同步代碼,所以先輸出 start,然后輸出 end;
再從上往下分析,遇到微任務,插入微任務隊列,遇到宏任務,插入宏任務隊列,分析完成后,微任務隊列包含:Promise.resolve 和 process.nextTick,宏任務隊列包含:fs.readFile 和 setTimeout;
先執行微任務隊列,但是根據優先級,先執行process.nextTick 再執行 Promise.resolve,所以先輸出nextTick callback再輸出Promise callback;
再執行宏任務隊列,根據宏任務插入先后順序執行 setTimeout 再執行 fs.readFile,這里需要注意,先執行setTimeout由于其回調時間較短,因此回調也先執行,并非是setTimeout先執行所以才先執行回調函數,但是它執行需要時間肯定大于1ms,所以雖然fs.readFile先于setTimeout執行,但是setTimeout執行更快,所以先輸出setTimeout,最后輸出read file success。
輸出結果:startendnextTick callbackPromise callbacksetTimeoutread file success
解析:在上面代碼中,有 2 個宏任務和 1 個微任務,宏任務是setTimeout 和 fs.readFile,微任務是Promise.resolve。
整個過程優先執行主線程的第一個事件循環過程,所以先執行同步邏輯,先輸出 2。
接下來執行微任務,輸出poll callback。
再執行宏任務中的fs.readFile 和 setTimeout,由于fs.readFile優先級高,先執行fs.readFile。但是處理時間長于1ms,因此會先執行setTimeout的回調函數,輸出1。這個階段在執行過程中又會產生新的宏任務fs.readFile,因此又將該fs.readFile 插入宏任務隊列
最后由于只剩下宏任務了fs.readFile,因此執行該宏任務,并等待處理完成后的回調,輸出read file sync success。
輸出結果:2?? poll callback1??? read file success??? read file sync success
3、async與await
async是異步的意思,await則可以理解為async wait。所以可以理解async就是用來聲明一個異步方法,而await是用來等待異步方法執行。
1)async 函數返回一個promise對象
2)await 正常情況下,await命令后面是一個Promise對象,返回該對象的結果(理解為new Promise())。如果不是Promise對象,就直接返回對應的值。不管await后面跟著的是什么,await都會阻塞后面的代碼(理解為new Promise().then()的代碼)。
示例1:
await會阻塞下面的代碼(即加入微任務隊列),先執行async外面的同步代碼,同步代碼執行完,再回到async函數中,再執行之前阻塞的代碼。
輸出結果:1 fn2 3 2
示例2:
解析:首先遇到console.log('script start'),直接打印結果,輸出script start;遇到定時器將其放入宏任務隊列中;遇到async1(),執行它,遇到console.log('async1 start'),輸出async1 start;遇到await async2(), 執行async2() ,然后阻塞下面代碼(即加入微任務列表);遇到console.log('async2'),輸出script async2;跳到new promise(),直接打印promise1;有resolve(),把then()后面放入微任務隊列;打印最后一行console.log('script end')。上一輪宏任務執行結束,依次執行微任務隊列的任務:await阻塞的的代碼console.log('async1 end'); promise().then()的代碼console.log('promise2');完成再執行宏任務隊列setTimeout中的console.log('settimeout')。
輸出結果為:script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout。
示例3:
解析:首先打印start;遇到setTimeout放入宏任務中;遇到new Promise(),執行打印children4;遇到setTimeout,放入宏任務中;由于此promise還沒有返回結果所以then不會執行且不會放入微隊列中;宏任務一輪結束,此時微任務隊列中沒有任務可執行;執行宏任務隊列,打印children2;遇到Promise()且直接返回成功,則將其then放入微隊列中。宏任務執行結束,執行隊列微任務,打印children3;執行下一個宏任務setTimeout,打印children5;Promise()且直接返回成功結果,將then()放入微隊列。此時宏任務執行結束,執行微任務,即剛放的then(),打印children7,遇到setTimeout放入宏任務中; 執行宏任務,執行setTimeout,打印children6;
輸出結果為:start、children4、children2、children3、children5、children7、children6。
示例4:
解析:執行p,返回一個Promise對象,執行promise,定義p1,緊接著執行p1,遇到setTimeout,放入宏任務隊列,返回成功的回調resolve(2),將p1的then()放入微任務隊列,打印3;返回p的成功回調結果resolve(2),將將p的then()放入微任務隊列中;繼續執行代碼,console.log("end"),打印end;一輪宏任務結束,依次執行微任務隊列,執行p1的then()(此時setTimeout中的resolve(1)失效,因為promise的狀態只能改變一次),打印2;執行p的then(),打印4;
輸出結果為:3、end、2、4。
若把代碼resolve(2)注釋掉,輸出結果為:3、end、4、1。