JS面試之對事件循環(huán)的理解
一、是什么
JavaScript
在設(shè)計之初便是單線程,即指程序運行時,只有一個線程存在,同一時間只能做一件事
為什么要這么設(shè)計,跟JavaScript
的應(yīng)用場景有關(guān)
JavaScript
初期作為一門瀏覽器腳本語言,通常用于操作 DOM
,如果是多線程,一個線程進(jìn)行了刪除 DOM
,另一個添加 DOM
,此時瀏覽器該如何處理?
為了解決單線程運行阻塞問題,JavaScript
用到了計算機(jī)系統(tǒng)的一種運行機(jī)制,這種機(jī)制就叫做事件循環(huán)(Event Loop)
事件循環(huán)(Event Loop)
在JavaScript
中,所有的任務(wù)都可以分為
- 同步任務(wù):立即執(zhí)行的任務(wù),同步任務(wù)一般會直接進(jìn)入到主線程中執(zhí)行
- 異步任務(wù):異步執(zhí)行的任務(wù),比如
ajax
網(wǎng)絡(luò)請求,setTimeout
定時函數(shù)等
同步任務(wù)與異步任務(wù)的運行流程圖如下:
從上面我們可以看到,同步任務(wù)進(jìn)入主線程,即主執(zhí)行棧,異步任務(wù)進(jìn)入任務(wù)隊列,主線程內(nèi)的任務(wù)執(zhí)行完畢為空,會去任務(wù)隊列讀取對應(yīng)的任務(wù),推入主線程執(zhí)行。上述過程的不斷重復(fù)就是事件循環(huán)
二、宏任務(wù)與微任務(wù)
如果將任務(wù)劃分為同步任務(wù)和異步任務(wù)并不是那么的準(zhǔn)確,舉個例子:
console.log(1) setTimeout(()=>{ console.log(2) }, 0) new Promise((resolve, reject)=>{ console.log('new Promise') resolve() }).then(()=>{ console.log('then') }) console.log(3)
如果按照上面流程圖來分析代碼,我們會得到下面的執(zhí)行步驟:
console.log(1)
,同步任務(wù),主線程中執(zhí)行setTimeout()
,異步任務(wù),放到Event Table
,0 毫秒后console.log(2)
回調(diào)推入Event Queue
中new Promise
,同步任務(wù),主線程直接執(zhí)行.then
,異步任務(wù),放到Event Table
console.log(3)
,同步任務(wù),主線程執(zhí)行
所以按照分析,它的結(jié)果應(yīng)該是 1
=> 'new Promise'
=> 3
=> 2
=> 'then'
但是實際結(jié)果是:1
=>'new Promise'
=> 3
=> 'then'
=> 2
出現(xiàn)分歧的原因在于異步任務(wù)執(zhí)行順序,事件隊列其實是一個“先進(jìn)先出”的數(shù)據(jù)結(jié)構(gòu),排在前面的事件會優(yōu)先被主線程讀取
例子中 setTimeout
回調(diào)事件是先進(jìn)入隊列中的,按理說應(yīng)該先于 .then
中的執(zhí)行,但是結(jié)果卻偏偏相反
原因在于異步任務(wù)還可以細(xì)分為微任務(wù)與宏任務(wù)
微任務(wù)
一個需要異步執(zhí)行的函數(shù),執(zhí)行時機(jī)是在主函數(shù)執(zhí)行結(jié)束之后、當(dāng)前宏任務(wù)結(jié)束之前
常見的微任務(wù)有:
- Promise.then
- MutaionObserver
- Object.observe(已廢棄;Proxy 對象替代)
- process.nextTick(Node.js)
宏任務(wù)
宏任務(wù)的時間粒度比較大,執(zhí)行的時間間隔是不能精確控制的,對一些高實時性的需求就不太符合
常見的宏任務(wù)有:
- script (可以理解為外層同步代碼)
- setTimeout/setInterval
- UI rendering/UI事件
- postMessage、MessageChannel
- setImmediate、I/O(Node.js)
這時候,事件循環(huán),宏任務(wù),微任務(wù)的關(guān)系如圖所示
按照這個流程,它的執(zhí)行機(jī)制是:
- 執(zhí)行一個宏任務(wù),如果遇到微任務(wù)就將它放到微任務(wù)的事件隊列中
- 當(dāng)前宏任務(wù)執(zhí)行完成后,會查看微任務(wù)的事件隊列,然后將里面的所有微任務(wù)依次執(zhí)行完
回到上面的題目
console.log(1) setTimeout(()=>{ console.log(2) }, 0) new Promise((resolve, reject)=>{ console.log('new Promise') resolve() }).then(()=>{ console.log('then') }) console.log(3)
流程如下
// 遇到 console.log(1) ,直接打印 1
// 遇到定時器,屬于新的宏任務(wù),留著后面執(zhí)行
// 遇到 new Promise,這個是直接執(zhí)行的,打印 'new Promise'
// .then 屬于微任務(wù),放入微任務(wù)隊列,后面再執(zhí)行
// 遇到 console.log(3) 直接打印 3
// 好了本輪宏任務(wù)執(zhí)行完畢,現(xiàn)在去微任務(wù)列表查看是否有微任務(wù),發(fā)現(xiàn) .then 的回調(diào),執(zhí)行它,打印 'then'
// 當(dāng)一次宏任務(wù)執(zhí)行完,再去執(zhí)行新的宏任務(wù),這里就剩一個定時器的宏任務(wù)了,執(zhí)行它,打印 2
三、async與await
async
是異步的意思,await
則可以理解為等待
放到一起可以理解async
就是用來聲明一個異步方法,而 await
是用來等待異步方法執(zhí)行
async
async
函數(shù)返回一個promise
對象,下面兩種方法是等效的
function f() { return Promise.resolve('TEST'); } // asyncF is equivalent to f! async function asyncF() { return 'TEST'; }
await
正常情況下,await
命令后面是一個 Promise
對象,返回該對象的結(jié)果。如果不是 Promise
對象,就直接返回對應(yīng)的值
async function f(){ // 等同于 // return 123 return await 123 } f().then(v => console.log(v)) // 123
不管await
后面跟著的是什么,await
都會阻塞后面的代碼
async function fn1 (){ console.log(1) await fn2() console.log(2) // 阻塞 } async function fn2 (){ console.log('fn2') } fn1() console.log(3)
上面的例子中,await
會阻塞下面的代碼(即加入微任務(wù)隊列),先執(zhí)行 async
外面的同步代碼,同步代碼執(zhí)行完,再回到 async
函數(shù)中,再執(zhí)行之前阻塞的代碼
所以上述輸出結(jié)果為:1
,fn2
,3
,2
四、流程分析
通過對上面的了解,我們對JavaScript
對各種場景的執(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('settimeout') }) async1() new Promise(function (resolve) { console.log('promise1') resolve() }).then(function () { console.log('promise2') }) console.log('script end')
分析過程:
- 執(zhí)行整段代碼,遇到
console.log('script start')
直接打印結(jié)果,輸出script start
- 遇到定時器了,它是宏任務(wù),先放著不執(zhí)行
- 遇到
async1()
,執(zhí)行async1
函數(shù),先打印async1 start
,下面遇到await
怎么辦?先執(zhí)行async2
,打印async2
,然后阻塞下面代碼(即加入微任務(wù)列表),跳出去執(zhí)行同步代碼 - 跳到
new Promise
這里,直接執(zhí)行,打印promise1
,下面遇到.then()
,它是微任務(wù),放到微任務(wù)列表等待執(zhí)行 - 最后一行直接打印
script end
,現(xiàn)在同步代碼執(zhí)行完了,開始執(zhí)行微任務(wù),即await
下面的代碼,打印async1 end
- 繼續(xù)執(zhí)行下一個微任務(wù),即執(zhí)行
then
的回調(diào),打印promise2
- 上一個宏任務(wù)所有事都做完了,開始下一個宏任務(wù),就是定時器,打印
settimeout
所以最后的結(jié)果是:script start
、async1 start
、async2
、promise1
、script end
、async1 end
、promise2
、settimeout
以上就是JS面試之對事件循環(huán)的理解的詳細(xì)內(nèi)容,更多關(guān)于JS面試事件循環(huán)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
addeventlistener監(jiān)聽scroll跟touch(實例講解)
下面小編就為大家?guī)硪黄猘ddeventlistener監(jiān)聽scroll跟touch(實例講解)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08用javascript連接access數(shù)據(jù)庫的方法
用javascript連接access數(shù)據(jù)庫的方法...2006-11-11