js
中的事件循環javascript
是一種基於事件的單線程、異步、非阻塞編程語言,我常常在看書或者瀏覽別人博客的時候看到這種說法,但是以前一直沒有深刻理解過,只知道javascript
中常用各類回調函數,好比瀏覽器
端的各類事件回調(點擊事件、滾動事件等)、ajax
請求回調、setTimeout
回調以及react16
的內核fiber
中用到的requestAnimationFrame
、promise
回調、node
端fs
模塊異步讀取文件內容回調、process
模塊的nextTick
等等。最近有時間瀏覽了各類資料(後面有各類相關資料的連接),終於明白全部的這些內容其實都離不開javascript事件循環
,而瀏覽器
端的事件循環又與node
端的事件循環有較大區別,下面分別介紹下。javascript
自從有了node
,javascript
既能夠運行在瀏覽器端又能夠運行在服務端,以chrome
瀏覽器爲例,相同點是都基於v8
引擎,不一樣的是瀏覽器端實現了頁面渲染、而node端則提供了一些服務端會用到的特性,好比fs
、process
等模塊,同時node端爲了實現跨平臺,底層使用libuv
來兼容linux
、windows
、 macOS
三大操做系統。所以雖然都實現了javascript
的異步、非阻塞特性,可是卻有有很多不一樣之處。java
不管是在瀏覽器
端仍是node
端,線程入口都是一個javascript
腳本文件,整個腳本文件從第一行開始到最後運行完成能夠看做是一個entry task
,即初始化任務,下圖task
中第一項即爲該過程。初始化過程當中確定會註冊很多異步事件
,好比常見的setTimeout
、onClick
、promise
等,這些異步事件執行中又有可能註冊更多異步事件。全部的這些異步任務都是在事件循環
一次次的循環中獲得執行,而這些異步任務又能夠分爲兩大類,即microtask
和task(或macrotask)
。那麼一次事件循環中會執行多少個異步任務?microtask
和task
的執行前後順序是什麼呢?看下圖。 node
先忽略圖中的紅色部分(渲染過程,後面再介紹),順時針方向即爲事件循環方向,能夠看出每次循環會前後執行兩類任務,task
和microtask
,每一類任務都由一個隊列組成,其中task
主要包括以下幾類任務:react
setTimeout
setInterval
而microtask
主要包括:linux
promise
MutationObserver
所以microtask
的執行事件結點是在兩次task
執行間隙。前面說了,每類任務都由一個隊列組成,這實際上是一種生產者-消費者
模型,事件的註冊過程即爲任務生產
過程,任務的執行過程即爲事件的消費
過程。那麼每次輪到一類任務執行各個隊列會出隊多少個任務來執行呢?圖中我已經標明,task
隊列每次出隊一項任務來執行,執行完成以後開始執行microtask
;而microtask
則每次都把全部(包括當前microtask
執行過程當中新添加的任務)任務執行完成,而後纔會繼續執行task
。也就是說,即使microtask
是異步任務,也不能無節制的註冊,不然會阻塞task
和頁面渲染
的執行。好比,下面的這段代碼中的setTimeout
回調任務將永遠得不到執行(注意,謹慎運行這段代碼,瀏覽器可能卡死):web
setTimeout(() => {
console.log('run setTimout callback');
}, 0);
function promiseLoop() {
console.log('run promise callback');
return Promise.resolve().then(() => {
return promiseLoop();
});
}
promiseLoop();
複製代碼
如今回過頭來再看上圖中的粉紅色虛線部分,該過程表示的是瀏覽器渲染過程,好比dom
元素的style
、layout
以及position
這些渲染,那爲何用虛線表示呢?是由於該部分的調度是由瀏覽器控制,並且是以60HZ
的頻率調度,之因此是60HZ
是爲了能知足人眼視覺效果的同時儘可能低頻的調度,若是瀏覽器一刻不停的頻繁渲染,那麼不只人眼觀察不到界面的變化效果(就如同夏天電扇轉太快人眼分辨不出來),並且耗費計算資源。所以上圖中渲染過程用虛線表示不必定每次事件循環都會執行渲染過程。ajax
仔細看虛線框起來的渲染過程,能夠看到在執行渲染以前能夠執行一個回調函數requestAnimationFrame
,執行渲染以後能夠執行一個回調函數requestIdleCallback
。使用這兩個鉤子函數註冊的回調函數同task
回調和microtask
回調同樣,會進入專屬的事件隊列,可是這兩個鉤子函數與setTimeout
不同,不是爲了在4ms,16ms或1s
以後再執行,而是在下一次頁面渲染
階段去執行,具體來講是requestAnimationFrame
在style
和layout
計算以前執行,requestIdleCallback
則是在變動真正渲染到頁面後執行。算法
requestAnimationFrame
比setTimeout
更適合作動畫,這裏有個例子能夠參考:jsfiddle.net/H7EEE/245/。效果以下圖所示,能夠看出requestAnimationFrame
比setTimeout
動畫效果更加流暢。 chrome
requestIdleCallback
則是在每一渲染貞後的空閒時間去完成回調任務,所以通常用於一些低優先級的
任務調度
,好比
react16
則使用了該鉤子函數實現異步
reconcilation
算法以保證頁面性能,固然因爲
requestIdleCallback
是比較新的
API
,
react
團隊實現了
pollyfill
,注意是目前是使用
requestAnimationFrame
實現的哦。
如今總結一下瀏覽器端的事件隊列,共包括四個事件隊列:task
隊列、requestAnimationFrame
隊列、requestIdleCallback
隊列以及microtask
隊列,javascript
腳本加載完成後首先執行第一個task
隊列任務,即初始化任務
,而後執行全部microtask
隊列任務,接着再次執行第二個task
隊列任務,以此類推,這其中穿插着60HZ
的渲染過程
。先執行誰後執行誰如今瞭解清楚了,但是到每一個事件隊列執行的輪次時,分別會有多少個事件出隊執行呢?答案見下圖(截圖自Jake Archibald
大神的JSConf
演講視頻): shell
task
每次出隊一項回調函數去執行,requestAnimationFrame
每次出隊全部當前隊列的回調函數去執行(requestIdleCallback
同樣),microtask
每次出隊全部當前隊列的回調函數以及本身輪次執行過程當中又新增到隊尾的回調函數。這三種不一樣的調度方式正好覆蓋了全部場景。
index.js
、promise
、setTimeout
的執行前後順序console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
new Promise(function (resolve) {
console.log('promise1.1');
resolve();
}).then(function () {
console.log('promise1.2');
}).then(function () {
console.log('promise1.3');
}).then(function () {
console.log('promise1.4');
});
new Promise(function (resolve) {
console.log('promise2.1');
resolve();
}).then(function () {
console.log('promise2.2');
}).then(function () {
console.log('promise2.3');
}).then(function () {
console.log('promise2.4');
});
console.log('script end');
複製代碼
這段代碼的輸入以下:
script start
promise1.1
promise2.1
script end
promise1.2
promise2.2
promise1.3
promise2.3
promise1.4
promise2.4
setTimeout
複製代碼
按照前面的事件循環示例圖,按照以下順序執行:
task
(index.js); 這裏包括四項輸出:script start
、promise1.1
、promise2.1
、script end
。其中須要留意promise1.1
和promise2.1
,由於new Promise
中resolve()
調用以前也是同步代碼,所以也會同步執行。microtask
; 這裏須要留意microtask
會邊執行邊生成新的添加到事件隊列
隊尾,所以執行完全部microtask
才從新進入事件循環開始下一項。task
(setTimeout); 根據前面的示例圖,這裏又輪到了task
的執行,只不過此次是setTimout
。前面介紹了下瀏覽器端的事件循環,涉及到task
和microtask
,其實node端的異步任務也包括這些,只不過node
端的task
劃分的更細,以下圖所示,node
端的task
能夠分爲4類任務隊列:
setTimeout
、setInterval
fs(disk)
、child_process
setImmediate
而microtask
包括:
process.nextTick
promise
開始後會首先執行註冊過的全部microtask
,而後會依次執行該4類task
隊列。而每執行完一個task
隊列就會接着執行microtask
隊列,而後再接着執行下一個task
隊列。所以microtask
隊列的執行是穿插在各個類形的task
之間的,固然也能夠。 node
端與瀏覽器
端事件循環的一個很重要的不一樣點是,瀏覽器
端task
隊列每輪事件循環僅出隊一個回調函數去執行接着去執行microtask
,而node
端只要輪到執行task
,則會跟執行完隊列中的全部當前任務,可是當前輪次新添加到隊尾的任務則會等到下一輪次纔會執行,該機制與瀏覽器端的requestAnimationFrame
的調度機制時同樣的。 總結一下node
端的事件循環,共包括4類task
事件隊列與2類microtask
事件隊列,microtask
穿插在task
之間執行。task
每次輪到執行會將當前隊列中的全部回調函數出隊執行,而microtask
的調度機制則與瀏覽器
端同樣,每次輪到執行都會出隊全部當前隊列的回調函數以及本身輪次執行過程當中又新增到隊尾的回調函數去執行。與瀏覽器
端不同的是node
端的microtask
包括process.nextTick
和promise
兩類。console.log('main');
setTimeout(function () {
console.log('execute in first timeout');
Promise.resolve(3).then(res => {
console.log('execute in third promise');
});
}, 0);
setTimeout(function () {
console.log('execute in second timeout');
Promise.resolve(4).then(res => {
console.log('execute in fourth promise');
});
}, 0);
Promise.resolve(1).then(res => {
console.log('execute in first promise');
});
Promise.resolve(2).then(res => {
console.log('execute in second promise');
});
複製代碼
前面這段代碼的輸出結果以下:
main
execute in first promise
execute in second promise
execute in first timeout
execute in second timeout
execute in third promise
execute in fourth promise
複製代碼
執行順序以下:
這個執行順序與以前畫的圖徹底對應。
index.js
、promise
、async await
、setTimeout
的執行前後順序console.log('script start');
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('entry async2');
return Promise.resolve();
}
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
}).then(function () {
console.log('promise3');
}).then(function () {
console.log('promise4');
}).then(function () {
console.log('promise5');
}).then(function () {
console.log('promise6');
});
console.log('script end');
複製代碼
這段代碼在node10
環境的執行結果以下:
script start
async1 start
entry async2
promise1
script end
promise2
promise3
promise4
async1 end
promise5
promise6
setTimeout
複製代碼
注意我這裏強調了是node10
環境,是由於node8
和node9
下面async await
有bug
,而node10
中獲得了修復,詳情能夠參考這篇文章:Faster async functions and promises。下面按照前面的事件循環
示例圖分析下前面這段代碼的執行結果:
task
(index.js); 這裏包括5項輸出:script start
、async1 start
、entry async2
、promise1
、script end
。這裏要注意async
函數中第一個await
以前執行的代碼也是同步代碼,所以會打印出scync1 start
以及entry async2
。microtask
; 這裏打印了全部剩下的promise
以及一個位於await
後的語句async1 end
。打印這個集合確定是沒問題的,可是問題是爲何async1 end
會比promise
延遲3個呢? 這個問題是這段代碼最難懂的地方,答案在剛剛提到的那篇文章中:每一個await
須要至少3個microtask queue ticks
,所以這裏async1 end
的打印相對於promise
晚打印了3個tick
。其實經過這裏例子咱們也應該的出一個結論,就是最要不要把promise
和async await
混用,不然容易時序混亂。task
(setTimeout)。 根據前面的示例圖,這裏又輪到了task的執行,只不過此次是setTimout。 從demo2
能夠看出,雖然async await
本質上也是microtask
,可是每一個await
會耗費至少3個microtask queue ticks
,這點須要注意。本篇總結主要參考了以下資源,強烈推薦瀏覽閱讀: