深入了解Javascript的事件循環(huán)機(jī)制
單線(xiàn)程的Javascript
JavaScript是一種單線(xiàn)程語(yǔ)言,它主要用來(lái)與用戶(hù)互動(dòng),以及操作DOM。多線(xiàn)程需要共享資源、且有可能修改彼此的運(yùn)行結(jié)果,且存在上下文切換。
在 JS 運(yùn)行的時(shí)候可能會(huì)阻止 UI 渲染,這說(shuō)明兩個(gè)線(xiàn)程是互斥的。這是因?yàn)?JS 可以修改 DOM,如果在 JS 執(zhí)行的時(shí)候 UI 線(xiàn)程還在工作,就可能導(dǎo)致不能安全的渲染 UI。
JS 是單線(xiàn)程運(yùn)行的,可以達(dá)到節(jié)省內(nèi)存,節(jié)約上下文切換時(shí)間。
為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個(gè)線(xiàn)程,但是子線(xiàn)程完全受主線(xiàn)程控制,且不得操作DOM。
單線(xiàn)程的同步等待極大影響效率,任務(wù)不得不一個(gè)一個(gè)等待執(zhí)行,對(duì)于網(wǎng)頁(yè)應(yīng)用是無(wú)法接受的。所以Javascript使用事件循環(huán)機(jī)制來(lái)解決異步任務(wù)的問(wèn)題。
同步 vs 異步 宏任務(wù) vs 微任務(wù)
首先了解下同步和異步的區(qū)別:
- 同步:在一個(gè)函數(shù)返回的時(shí)候,調(diào)用者就能夠得到預(yù)期結(jié)果。
- 同步任務(wù):在主線(xiàn)程上排隊(duì)執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù)。
- 異步:在函數(shù)返回的時(shí)候,調(diào)用者還不能夠得到預(yù)期結(jié)果,而是需要在將來(lái)通過(guò)一定的手段得到。
- 異步任務(wù):不進(jìn)入主線(xiàn)程、而放在"任務(wù)隊(duì)列"中的任務(wù),若有多個(gè)異步任務(wù),則需排隊(duì)等待進(jìn)入主線(xiàn)程執(zhí)行棧中被執(zhí)行。

任務(wù)隊(duì)列其實(shí)不止一種,根據(jù)任務(wù)種類(lèi)的不同,可以分為微任務(wù)(micro task)隊(duì)列和宏任務(wù)(macro task)隊(duì)列。常見(jiàn)的任務(wù)如下:
- 宏任務(wù):script(整體代碼)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate;需要特定的異步線(xiàn)程去執(zhí)行,有明確的異步任務(wù)去執(zhí)行,有回調(diào)。
- 微任務(wù):Promise、MutaionObserver、process.nextTick(Node.js 環(huán)境,會(huì)先于其他微任務(wù)執(zhí)行);不需要特定的異步線(xiàn)程去執(zhí)行,沒(méi)有明確的異步任務(wù)去執(zhí)行,只有回調(diào)。
一次 Eventloop 循環(huán)會(huì)處理一個(gè)宏任務(wù)和所有這次循環(huán)中產(chǎn)生的微任務(wù)。 執(zhí)行順序如下圖:

第一個(gè)例子:
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();
//等同于
var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};上面代碼中的req.send方法是Ajax操作向服務(wù)器發(fā)送數(shù)據(jù),它是一個(gè)異步任務(wù),意味著只有當(dāng)前腳本的所有代碼執(zhí)行完,系統(tǒng)才會(huì)去讀取"任務(wù)隊(duì)列"。指定回調(diào)函數(shù)的部分(onload和onerror),在send()方法的前面或后面無(wú)關(guān)緊要,因?yàn)樗鼈儗儆趫?zhí)行棧的一部分,系統(tǒng)總是執(zhí)行完它們,才會(huì)去讀取"任務(wù)隊(duì)列"。
第二個(gè)例子:
console.log('1 第一次循環(huán) 開(kāi)始執(zhí)行');
setTimeout(function () {
console.log('2 第二次循環(huán) 開(kāi)始執(zhí)行');
new Promise(function (resolve) {
console.log('3 第二次循環(huán) 宏任務(wù)結(jié)束');
resolve();
}).then(function () {
console.log('4 第二次循環(huán) 微任務(wù)執(zhí)行')
})
}, 0)
new Promise(function (resolve) {
console.log('5 第一次循環(huán) 宏任務(wù)結(jié)束');
resolve();
}).then(function () {
console.log('6 第一次循環(huán) 微任務(wù)執(zhí)行')
})
setTimeout(function () {
console.log('7 第三次循環(huán) 開(kāi)始執(zhí)行');
new Promise(function (resolve) {
console.log('8 第三次循環(huán) 宏任務(wù)結(jié)束');
resolve();
}).then(function () {
console.log('9 第三次循環(huán) 微任務(wù)執(zhí)行')
})
}, 0)
/*
結(jié)果
1 第一次循環(huán) 開(kāi)始執(zhí)行
5 第一次循環(huán) 宏任務(wù)結(jié)束
6 第一次循環(huán) 微任務(wù)執(zhí)行
2 第二次循環(huán) 開(kāi)始執(zhí)行
3 第二次循環(huán) 宏任務(wù)結(jié)束
4 第二次循環(huán) 微任務(wù)執(zhí)行
7 第三次循環(huán) 開(kāi)始執(zhí)行
8 第三次循環(huán) 宏任務(wù)結(jié)束
9 第三次循環(huán) 微任務(wù)執(zhí)行
*/定時(shí)器
定時(shí)器功能主要由setTimeout()和setInterval()這兩個(gè)函數(shù)來(lái)完成,它們的內(nèi)部運(yùn)行機(jī)制完全一樣,區(qū)別在于前者指定的代碼是一次性執(zhí)行,后者則為反復(fù)執(zhí)行。
如果將setTimeout()的第二個(gè)參數(shù)設(shè)為0,就表示當(dāng)前代碼執(zhí)行完(執(zhí)行棧清空)以后,立即執(zhí)行(0毫秒間隔)指定的回調(diào)函數(shù)。主線(xiàn)程盡可能早得執(zhí)行,但是沒(méi)有辦法保證回調(diào)函數(shù)一定會(huì)在setTimeout()指定的時(shí)間執(zhí)行,因?yàn)楸仨毜鹊疆?dāng)前代碼(執(zhí)行棧)執(zhí)行完,主線(xiàn)程才會(huì)去執(zhí)行它指定的回調(diào)函數(shù)。所以單線(xiàn)程無(wú)法實(shí)現(xiàn)真正的異步,因?yàn)檫€是存在阻塞。
HTML5標(biāo)準(zhǔn)規(guī)定了setTimeout()的第二個(gè)參數(shù)的最小值(最短間隔),不得低于4毫秒,如果低于這個(gè)值,就會(huì)自動(dòng)增加。
在此之前,老版本的瀏覽器都將最短間隔設(shè)為10毫秒。
另外,對(duì)于那些DOM的變動(dòng)(尤其是涉及頁(yè)面重新渲染的部分),通常不會(huì)立即執(zhí)行,而是每16毫秒執(zhí)行一次。
這時(shí)使用requestAnimationFrame()的效果要好于setTimeout()。
在Node.js環(huán)境下,還提供了另外兩個(gè)方法:
- process.nextTick方法可以在當(dāng)前"執(zhí)行棧"的尾部,下一次Event Loop之前,觸發(fā)回調(diào)函數(shù)。也就是說(shuō),它指定的任務(wù)總是在本次"事件循環(huán)"觸發(fā),發(fā)生在所有異步任務(wù)之前,同時(shí)也是在所有微任務(wù)之前執(zhí)行。
- setImmediate方法則是在當(dāng)前"任務(wù)隊(duì)列"的尾部添加事件,也就是說(shuō),它指定的任務(wù)總是在之后的Event Loop執(zhí)行,這與setTimeout(fn, 0)很像。
多個(gè)process.nextTick?語(yǔ)句總是在當(dāng)前"執(zhí)行棧"一次執(zhí)行完,多個(gè)setImmediate則可能需要多次loop才能執(zhí)行完。
To Be Continued
Node.js使用V8作為js的解析引擎,而I/O處理方面使用了自己設(shè)計(jì)的libuv,libuv是一個(gè)基于事件驅(qū)動(dòng)的跨平臺(tái)抽象層,封裝了不同操作系統(tǒng)一些底層特性,對(duì)外提供統(tǒng)一的API,事件循環(huán)機(jī)制也是它里面的實(shí)現(xiàn)的。(在Python中,uvloop,一個(gè)完整的asyncio事件循環(huán)的替代品,也是建立在libuv基礎(chǔ)之上,是由Cython編寫(xiě)而成。)這個(gè)機(jī)制和瀏覽器中Javascript的事件循環(huán)機(jī)制是不太一樣的。
以上就是深入了解Javascript的事件循環(huán)機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于Javascript事件循環(huán)機(jī)制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
分享JavaScript監(jiān)聽(tīng)全部Ajax請(qǐng)求事件的方法
最近在做一個(gè)小項(xiàng)目,引入了第三方j(luò)s文件,這個(gè)文件會(huì)調(diào)用XMLHttpRequest向服務(wù)器發(fā)送 Ajax請(qǐng)求,但是我有需要監(jiān)聽(tīng)其Ajax請(qǐng)求的某些事件,以便額外地執(zhí)行其他腳本。于是稍微看了看監(jiān)聽(tīng) Ajax請(qǐng)求的事件方法,在這里分享給大家。有需要的朋友們可以參考借鑒。2016-08-08
JavaScript中實(shí)現(xiàn)鍵值對(duì)應(yīng)的字典與哈希表結(jié)構(gòu)的示例
字典或者哈希表這樣的鍵值對(duì)應(yīng)結(jié)構(gòu)在其他很多語(yǔ)言中都有內(nèi)置,非常好用,這里我們來(lái)看一下JavaScript中實(shí)現(xiàn)鍵值對(duì)應(yīng)的字典與哈希表結(jié)構(gòu)的示例:2016-06-06
Javascript簡(jiǎn)寫(xiě)條件語(yǔ)句(推薦)
下面小編就為大家?guī)?lái)一篇Javascript簡(jiǎn)寫(xiě)條件語(yǔ)句(推薦)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-06-06
js實(shí)現(xiàn)數(shù)組轉(zhuǎn)換成json
本文給大家分享的是使用javascript實(shí)現(xiàn)的數(shù)組轉(zhuǎn)換json的代碼,非常簡(jiǎn)單實(shí)用,相當(dāng)于JSON.stringify(array);,有需要的小伙伴可以參考下。2015-06-06
JavaScript實(shí)現(xiàn)隨機(jī)五位數(shù)驗(yàn)證碼
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)隨機(jī)五位數(shù)驗(yàn)證碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09

