1.單執行緒的js
js區別於其他語言即在於它的單執行緒特性,考慮到它的主要執行環境(瀏覽器),這樣設計也是情理之中:
- js在執行時頻繁操作DOM是很常見的業務需求,試想如果同時有多個事件同時在操作同一個DOM節點,瀏覽器怕是分分鐘要崩掉。
- 雖然HTML5中提出
Web Worker
允許多個執行緒同時執行js程式碼,但其實質也只是在主執行緒中分出幾個子執行緒,並沒有改變js單執行緒的本質。並且在多個子執行緒中執行的js程式碼是不允許操作DOM的。
2.為什麼要有事件迴圈
事件驅動的js程式難免在程式設計時出現諸多事件,而依靠單執行緒來執行這些事件,若只是按部就班當一個事件執行完成後再執行另一個事件,那當遇到費時很長的比如ajax請求時,程式豈不是要死掉,依據網際網路行業的八秒原則,怕是使用者要跑的差不多了。遇到這種情況,如果是別的語言,還可以利用多執行緒將事件分發,避免造成阻塞。而js的解決方案則是非同步處理事件,提到非同步則是依賴於我們要談的事件迴圈(Event Loop)。下面我們會從瀏覽器和node環境來談各自的事件迴圈具體實現。
3.瀏覽器端
先從一張圖看一下瀏覽器事件迴圈的執行機制(圖片稍有不完整,會在後面解釋中補充)
- 執行棧:
- js引擎在執行js程式碼時,會維護一個執行棧,這個棧用來存放要執行的事件。
- 執行棧中只會存在一個事件,所謂js的執行過程不過就是各種事件不斷入棧->執行->出棧的過程罷了。
- 同步/非同步事件
- 記得前面有提到js對於事件的非同步處理方法,在這裡事件被分為同步事件和非同步事件。
- 同步事件比如一些立馬執行的語句,比如很常見的
console.log("我是同步的")
,而非同步事件則比如一些ajax
請求的回撥函式,一些滑鼠,鍵盤事件等。 - 執行過程
- js在執行時(從上到下,逐條執行)碰到同步事件則會壓入執行棧,然後被執行,出棧。而當遇到非同步事件則會被註冊進
Event-Table
中,待事件有結果(比如ajax請求完成,setTimeout
到了延時的時間)時就會被推入事件佇列中,就是上圖中的callback queue
。當同步任務執行完成後(即主執行緒中執行棧為空時),event loop
就會去檢查我們這裡的是事件佇列,若存在事件則依次進入執行棧被執行。 - 說了這麼多,舉個栗子吧
setTimeout(()=>{ console.log('我是延時為3s的定時器') },3000) setTimeout(()=>{ console.log('我是延時為0的定時器') },0) console.log('我是同步的')複製程式碼
- 先來看執行效果
我是同步的 我是延時為0的定時器 我是延時為3s的定時器複製程式碼
- 執行過程:
- 先碰到
setTimeout(3s)
,非同步事件,將他註冊進event-table
中。 - 接下來是
setTimeout(0s)
,同樣是非同步事件,註冊進event-table
中。 - 接著碰到
console.log('我是同步的')
識別為同步事件,進入執行棧,執行,之後出棧。 - 此時我們的執行棧已經為空,
event loop
就會去事件佇列中去找是不是有待執行的非同步事件。此時延時為0s的定時器肯定已經在佇列中了,進入執行棧執行對應的回撥函式。接著3s時延時為3s的定時器也會進入事件佇列中,和上面的一樣被執行。(這裡我們看到延時的時間只是它被註冊進事件佇列的時間,而實際被執行的時間或許會因為事件過多而有延遲,畢竟執行棧中始終只會有一個事件被執行,資源有限,先到先得) - 其實所謂事件迴圈可以理解成一個死迴圈,始終在執行棧於事件佇列之間交替檢查。
- 在上面,我們將事件分為同步事件和非同步事件,而實際中對非同步事件(也不能完全說是對非同步事件的劃分,後面解釋)有更細的劃分。
- microTask(微事件)和macroTask(巨集事件)
- 巨集事件:
script
(這裡經常指的是<script>
標籤,作為程式入口,所以我們前面說是對非同步事件的劃分有點不嚴謹),setTimeout
,setInterval
等 - 微事件:
promise
等 - 這裡主要是為了解決
event loop
在拉取事件時像下面這種情況的尷尬//event-table註冊了以下事件 setTimeout(()=>{},0); new Promise(function (resolve,reject){ resolve() }).then(()=>{ //statement }) //到底誰應該被先入隊複製程式碼
- 所以在事件迴圈中將事件佇列分為巨集事件佇列和微事件佇列,非同步事件將在有結果時會被分發進各自的佇列。
- 對於巨集事件和微事件的執行原則
- 將以
script
為第一個巨集事件去開始第一輪迴圈。 - 一次只執行一個巨集事件。
- 執行完一個巨集事件後會清空此時微事件佇列中的所有事件,一次迴圈完成。
- 去執行巨集事件佇列中的下一個事件...
- 一張圖去理解(引用地址:https://juejin.im/post/59e85eebf265da430d571f89)
- 到這我們的瀏覽器端的事件迴圈就結束了,接下來是node環境中的事件迴圈,先來張圖過渡一下。(圖片來自李鍇的《新時期的node.js入門》)
4.node端
- 作為服務端的js執行環境,node在處理高併發的服務端需求時表現極為出色,而這一特性也於我們的事件迴圈息息相關。
- 從前面的圖來看,node的事件迴圈是按階段來的,下面我們來分階段介紹
- timers:這裡維護著一個專門針對
setTimeout
,setInterval
的事件佇列。定時器會在有結果(到達延時時間時)被註冊進這個佇列中後面稱timer queue
。 - IO callbacks:這個階段處理一些上一輪迴圈少數未執行的I/O回撥。
- idle,prepare:內部實現,與程式碼中事件迴圈無關。
- poll:這個階段可以看成整個事件迴圈的主導者,絕大部分事件迴圈在這個部分完成。具體過程後面專門介紹。
- check:本階段主要維護針對
setImmediate(在當前事件迴圈的結尾執行)
的事件佇列(後面稱check queue
)。 - close callback:主要處理一些關閉連線的回撥。
- 主要來說poll階段
- poll:這裡我們把他解釋為輪詢
- poll主要幹兩件事:
- 檢查timer維護的事件的事件佇列中是否有事件被分發進佇列
- 執行poll階段自己維護的事件佇列
- 進入poll階段時:
- 檢查並執行poll事件佇列(後面稱
poll queue
)中的事件 - 當
poll queue
為空時檢查check queue,若不為空進入check階段,執行。 - 檢查
timer queue
是否為空,若不為空進入timer階段,執行。 - 這裡有一個特例
process.nextTick
- 用來定義一個非同步事件,這個事件將在當前事件迴圈階段結束後執行,注意與
setImmediate
的區別,所以說如果這兩個同時存在於一輪迴圈中,process.nextTick總是在setImmediate
之前執行 - 這個方法定義的事件將被分發到
nextTick queue
中。 - 再來提起我們很熟悉的巨集事件和微事件。
- node中也將非同步事件分為巨集事件和微事件來管理執行順序,執行原則在最新的node標準中於瀏覽器一致,這裡有一張圖很清晰的解釋執行順序(圖片來源:https://juejin.im/post/5c337ae06fb9a049bc4cd218)
5.後記
根據個人理解以及整理得,有錯誤或者偏差敬請原諒,歡迎指正。