JavaScript事件循環和任務隊列

引言

首先需要知道的是JavaScript是門\color{red}{單線程,非阻塞}的語言。之所以如此設計,是因為JavaScript主要應用于瀏覽器的互動,即操作DOM。所以一次只能完成一件任務。如果有多個任務,就必須排隊,前面一個任務完成,再執行后面一個任務,以此類推。我們假設JavaScript是多線程的,那么多個線程同時進行,兩個線程同時操作一個DOM,那么以誰的操作為準呢?
\color{red}{非阻塞}是當代碼需要進行一項異步任務(無法立刻返回結果,需要花一定時間才能返回的任務,如I/O事件)的時候,主線程會掛起(pending)這個任務,然后在異步任務返回結果的時候再根據一定規則去執行相應的回調。
單線程雖然實現起來比較簡單,執行環境相對單純;但是只要有一個任務耗時很長,后面的任務都必須排隊等著,會拖延整個程序的執行。因此為了解決這個問題Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和異步(Asynchronous)。

同步模式

就是后一個任務等待前一個任務結束,然后再執行,程序的執行順序與任務的排列順序是一致的、同步的。

異步模式

每一個任務有一個或多個回調函數(callback),前一個任務結束后,不是執行隊列上的后一個任務,而是執行回調函數;后一個任務則是不等前一個任務的回調函數的執行而執行,所以程序的執行順序與任務的排列順序是不一致的、異步的。
"異步模式"非常重要。在瀏覽器端,耗時很長的操作都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操作。在服務器端,"異步模式"甚至是唯一的模式,因為執行環境是單線程的,如果允許同步執行所有http請求,服務器性能會急劇下降,很快就會失去響應。

JavaScript為何能執行異步任務

Javascript是單線程的,但是卻能執行異步任務,這主要是因為 JS 中存在\color{red}{事件循環}(Event Loop)和\color{red}{任務隊列}(Task Queue)。
示例

setTimeout(function(){
    console.log(2);
},0);
 
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});
 
console.log(6);
 
setTimeout(function(){
    console.log(7);
},0);
異步代碼測試結果.png

事件循環(Event Loop)

JS 會創建一個類似于 while (true) 的循環,每執行一次循環體的過程稱之為Tick。每次Tick的過程就是查看是否有待處理事件,如果有則取出相關事件及回調函數放入執行棧中由主線程執行。待處理的事件會存儲在一個任務隊列中,也就是每次Tick會查看任務隊列中是否有需要執行的任務。

示例

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

事件循環會按照上圖所示的模式進行操作,queue.waitForMessage() 會同步地等待消息到達(如果當前沒有任何消息等待被處理)。

任務隊列

和事件循環聯系在一起的是任務隊列,
-所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
-主線程之外,還存在一個”任務隊列”(task queue)。只要異步任務有了運行結果,就在”任務隊列”之中放置一個事件。
-一旦”執行棧”中的所有同步任務執行完畢,系統就會讀取”任務隊列”,看看里面有哪些事件。那些對應的異步任務,于是結束等待狀態,進入執行棧,開始執行。
-主線程不斷重復上面的第三步。

異步任務

異步操作會將相關回調添加到任務隊列中。而不同的異步操作添加到任務隊列的時機也不同,如onclick, setTimeout,ajax 處理的方式都不同,這些異步操作是由瀏覽器內核的webcore來執行的,webcore包含下圖中的3種 webAPI,分別是DOM Binding、network、timer模塊。
-DOM Binding 模塊處理一些DOM綁定事件,如onclick事件觸發時,回調函數會立即被-webcore添加到任務隊列中。
-network 模塊處理Ajax請求,在網絡請求返回時,才會將對應的回調函數添加到任務隊列中。
-timer 模塊會對setTimeout等計時器進行延時處理,當時間到達的時候,才會將回調函數添加到任務隊列中。


webApi.png

事件循環和任務隊列之間的關系

事件循環規范.png

規范中中提到,一個瀏覽器環境,只能有一個事件循環,而一個事件循環可以多個任務隊列,每個任務都有一個任務源(Task source)。相同任務源的任務,只能放到一個任務隊列中。
不同任務源的任務,可以放到不同任務隊列中。
簡單來說:一個事件循環可以有多個任務隊列,隊列之間可有不同的優先級,同一隊列中的任務按先進先出的順序執行,但是不保證多個任務隊列中的任務優先級,具體實現可能會交叉執行。

不同任務隊列的優先級

在異步代碼測試結果的圖中看到,代碼的執行順序并不是按著,代碼書寫順序依次執行的,這是因為不同的異步任務之間也有優先級的區別。異步任務分為兩類,macrotask(宏任務)和 microtask(微任務)

宏任務(macro task)

script(你的全部JS代碼,“同步代碼”), setTimeout, setInterval, setImmediate, I/O,UI rendering

微任務(micro task)

process.nextTick,Promises(這里指瀏覽器原生實現的 Promise), Object.observe, MutationObserver

執行順序

瀏覽器為了能夠使得JS內部task與DOM任務能夠有序的執行,會在一個task執行結束后,在下一個 task 執行開始前,對頁面進行重新渲染 (task->渲染->task->...),鼠標點擊會觸發一個事件回調,需要執行一個宏任務,然后解析HTMl。微任務通常來說就是需要在當前 task 執行結束后立即執行的任務,比如對一系列動作做出反饋,或或者是需要異步的執行任務而又不需要分配一個新的 task,這樣便可以減小一點性能的開銷。所有微任務總會在下一個宏任務之前全部執行完畢。
所以,瀏覽器環境中,js執行任務的流程是這樣的:
1.第一個事件循環,先執行script中的所有同步代碼(即 macrotask 中的第一項任務)
2.再取出 microtask 中的全部任務執行(先清空process.nextTick隊列,再清空promise.then隊列)
3.下一個事件循環,再回到 macrotask 取其中的下一項任務
4.再重復2
5.反復執行事件循環…

一些常見的異步任務

setTimeout()

將事件插入到了事件隊列,必須等到當前代碼(執行棧)執行完,主線程才會去執行它指定的回調函數。
當主線程時間執行過長,無法保證回調會在事件指定的時間執行。
瀏覽器端每次setTimeout會有4ms的延遲,當連續執行多個setTimeout,有可能會阻塞進程,造成性能問題。

setImmediate()

事件插入到事件隊列尾部,主線程和事件隊列的函數執行完成之后立即執行。和setTimeout(fn,0)的效果差不多。
服務端node提供的方法。瀏覽器端最新的api也有類似實現:window.setImmediate,但支持的瀏覽器很少。

process.nextTick()

插入到事件隊列尾部,但在下次事件隊列之前會執行。也就是說,它指定的任務總是發生在所有異步任務之前,當前主線程的末尾。
大致流程:當前”執行棧”的尾部–>下一次Event Loop(主線程讀取”任務隊列”)之前–>觸發process指定的回調函數。
服務器端node提供的辦法。用此方法可以用于處于異步延遲的問題。
可以理解為:此次不行,預約下次優先執行。

Promise

Promise本身是同步的立即執行函數, 當在 executor 中執行 resolve 或者 reject 的時候, 此時是異步操作, 會先執行 then/catch 等,當主棧完成后,才會去調用 resolve/reject 中存放的方法執行,打印 p 的時候,是打印的返回結果,一個 Promise 實例。

async await

Async/Await就是一個自執行的generate函數。利用generate函數的特性把異步的代碼寫成“同步”的形式。
async 函數返回一個 Promise 對象,當函數執行的時候,一旦遇到 await 就會先返回,等到觸發的異步操作完成,再執行函數體內后面的語句。可以理解為,是讓出了線程,跳出了 async 函數體。

參考資料

https://blog.csdn.net/happyqyt/article/details/90644667
https://blog.csdn.net/github_35549695/article/details/82390345
https://www.cnblogs.com/nayek/p/11729923.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop#%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370