Event Loop 是一個很重要的概念,指的是計算機系統的一種運行機制。前端
JavaScript語言就採用這種機制,來解決單線程運行帶來的一些問題。node
想要理解Event Loop,就要從程序的運行模式講起。運行之後的程序叫作「進程」(process),通常狀況下,一個進程一次只能執行一個任務。面試
若是有不少任務須要執行,不外乎三種解決方法。promise
(1)排隊。由於一個進程一次只能執行一個任務,只好等前面的任務執行完了,再執行後面的任務。瀏覽器
(2)新建進程。使用fork命令,爲每一個任務新建一個進程。多線程
(3)新建線程。由於進程太耗費資源,因此現在的程序每每容許一個進程包含多個線程,由線程去完成任務。(進程和線程的詳細解釋,請看這裏。)異步
以JavaScript語言爲例,它是一種單線程語言,全部任務都在一個線程上完成,即採用上面的第一種方法。一旦遇到大量任務或者遇到一個耗時的任務,網頁就會出現」假死」,由於JavaScript停不下來,也就沒法響應用戶的行爲。socket
你也許會問,JavaScript爲何是單線程,難道不能實現爲多線程嗎?async
這跟歷史有關係。JavaScript從誕生起就是單線程。緣由大概是不想讓瀏覽器變得太複雜,由於多線程須要共享資源、且有可能修改彼此的運行結果,對於一種網頁腳本語言來講,這就太複雜了。後來就約定俗成,JavaScript爲一種單線程語言。(Worker API能夠實現多線程,可是JavaScript自己始終是單線程的。)函數
若是某個任務很耗時,好比涉及不少I/O(輸入/輸出)操做,那麼線程的運行大概是下面的樣子。
上圖的綠色部分是程序的運行時間,紅色部分是等待時間。能夠看到,因爲I/O操做很慢,因此這個線程的大部分運行時間都在空等I/O操做的返回結果。這種運行方式稱爲」同步模式」(synchronous I/O)或」堵塞模式」(blocking I/O)。
若是採用多線程,同時運行多個任務,那極可能就是下面這樣。
上圖代表,多線程不只佔用多倍的系統資源,也閒置多倍的資源,這顯然不合理。
Event Loop就是爲了解決這個問題而提出的。Wikipedia這樣定義:
「Event Loop是一個程序結構,用於等待和發送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)」
簡單說,就是在程序中設置兩個線程:一個負責程序自己的運行,稱爲」主線程」;另外一個負責主線程與其餘進程(主要是各類I/O操做)的通訊,被稱爲」Event Loop線程」(能夠譯爲」消息線程」)。
上圖主線程的綠色部分,仍是表示運行時間,而橙色部分表示空閒時間。每當遇到I/O的時候,主線程就讓Event Loop線程去通知相應的I/O程序,而後接着日後運行,因此不存在紅色的等待時間。等到I/O程序完成操做,Event Loop線程再把結果返回主線程。主線程就調用事先設定的回調函數,完成整個任務。
能夠看到,因爲多出了橙色的空閒時間,因此主線程得以運行更多的任務,這就提升了效率。這種運行方式稱爲」異步模式「(asynchronous I/O)或」非堵塞模式」(non-blocking mode)。
這正是JavaScript語言的運行方式。單線程模型雖然對JavaScript構成了很大的限制,但也所以使它具有了其餘語言不具有的優點。若是部署得好,JavaScript程序是不會出現堵塞的,這就是爲何node.js平臺能夠用不多的資源,應付大流量訪問的緣由。
在實際工做中,瞭解Event loop的意義能幫助你分析一些異步次序的問題(固然,隨着es7 async和await的流行,這樣的機會愈來愈少了)。除此之外,它還對你瞭解瀏覽器和Node的內部機制有積極的做用;對於參加面試,被問到一堆異步操做的執行順序時,也不至於兩眼抓瞎。
3. 瀏覽器上的實現
在JavaScript中,任務被分爲Task(又稱爲MacroTask,宏任務)和MicroTask(微任務)兩種。它們分別包含如下內容:
MacroTask: script(總體代碼), setTimeout, setInterval, setImmediate(node獨有), I/O, UI renderingMicroTask: process.nextTick(node獨有), Promises, Object.observe(廢棄), MutationObserver
須要注意的一點是:在同一個上下文中,總的執行順序爲同步代碼—>microTask—>macroTask[6]。這一塊咱們在下文中會講。
瀏覽器中,一個事件循環裏有不少個來自不一樣任務源的任務隊列(task queues),每個任務隊列裏的任務是嚴格按照先進先出的順序執行的。可是,由於瀏覽器本身調度的關係,不一樣任務隊列的任務的執行順序是不肯定的。
具體來講,瀏覽器會不斷從task隊列中按順序取task執行,每執行完一個task都會檢查microtask隊列是否爲空(執行完一個task的具體標誌是函數執行棧爲空),若是不爲空則會一次性執行完全部microtask。而後再進入下一個循環去task隊列中取下一個task執行,以此類推。
注意:圖中橙色的MacroTask任務隊列也應該是在不斷被切換着的。
本段大批量引用了《什麼是瀏覽器的事件循環(Event Loop)》的相關內容,想看更加詳細的描述能夠自行取用。
4. Node上的實現
nodejs的event loop分爲6個階段,它們會按照順序反覆運行,分別以下:
timers:執行setTimeout() 和 setInterval()中到期的callback。
I/O callbacks:上一輪循環中有少數的I/Ocallback會被延遲到這一輪的這一階段執行
idle, prepare:隊列的移動,僅內部使用
poll:最爲重要的階段,執行I/O callback,在適當的條件下會阻塞在這個階段
check:執行setImmediate的callback
close callbacks:執行close事件的callback,例如socket.on("close",func)
不一樣於瀏覽器的是,在每一個階段完成後,而不是MacroTask任務完成後,microTask隊列就會被執行。這就致使了一樣的代碼在不一樣的上下文環境下會出現不一樣的結果。咱們在下文中會探討。
另外須要注意的是,若是在timers階段執行時建立了setImmediate則會在此輪循環的check階段執行,若是在timers階段建立了setTimeout,因爲timers已取出完畢,則會進入下輪循環,check階段建立timers任務同理。
5. 示例
5.1 瀏覽器與Node執行順序的區別
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') })}, 0)setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') })}, 0)瀏覽器輸出:time1promise1time2promise2Node輸出:time1time2promise1promise2
在這個例子中,Node的邏輯以下:
最初timer1和timer2就在timers階段中。開始時首先進入timers階段,執行timer1的回調函數,打印timer1,並將promise1.then回調放入microtask隊列,一樣的步驟執行timer2,打印timer2;
至此,timer階段執行結束,event loop進入下一個階段以前,執行microtask隊列的全部任務,依次打印promise一、promise2。
而瀏覽器則由於兩個setTimeout做爲兩個MacroTask, 因此先輸出timer1, promise1,再輸出timer2,promise2。
更加詳細的信息能夠查閱《深刻理解js事件循環機制(Node.js篇)》
爲了證實咱們的理論,把代碼改爲下面的樣子:
setImmediate(() => { console.log('timer1') Promise.resolve().then(function () { console.log('promise1') })})setTimeout(() => { console.log('timer2') Promise.resolve().then(function () { console.log('promise2') })}, 0)Node輸出:timer1 timer2promise1 或者 promise2timer2 timer1promise2 promise1
按理說
setTimeout(fn,0)
應該比
setImmediate(fn)
快,應該只有第二種結果,爲何會出現兩種結果呢?
這是由於Node 作不到0毫秒,最少也須要1毫秒。實際執行的時候,進入事件循環之後,有可能到了1毫秒,也可能還沒到1毫秒,取決於系統當時的情況。若是沒到1毫秒,那麼 timers 階段就會跳過,進入 check 階段,先執行setImmediate的回調函數。
另外,若是已通過了Timer階段,那麼setImmediate會比setTimeout更快,例如:
const fs = require('fs');fs.readFile('test.js', () => { setTimeout(() => console.log(1)); setImmediate(() => console.log(2));});
上面代碼會先進入 I/O callbacks 階段,而後是 check 階段,最後纔是 timers 階段。所以,setImmediate纔會早於setTimeout執行。
具體能夠看《Node 定時器詳解》。
5.2 不一樣異步任務執行的快慢
setTimeout(() => console.log(1));setImmediate(() => console.log(2));Promise.resolve().then(() => console.log(3));process.nextTick(() => console.log(4));輸出結果:4 3 1 2或者4 3 1 2
由於咱們上文說過microTask會優於macroTask運行,因此先輸出下面兩個,而在Node中process.nextTick比Promise更加優先[3],因此4在3前。而根據咱們以前所說的Node沒有絕對意義上的0ms,因此1,2的順序不固定。
5.3 MicroTask隊列與MacroTask隊列
setTimeout(function () { console.log(1); },0); console.log(2); process.nextTick(() => { console.log(3); }); new Promise(function (resolve, rejected) { console.log(4); resolve() }).then(res=>{ console.log(5); }) setImmediate(function () { console.log(6) }) console.log('end');Node輸出:2 4 end 5 1 6
這個例子來源於《JavaScript中的執行機制》。Promise的代碼是同步代碼,then和catch纔是異步的,因此4要同步輸出,而後Promise的then位於microTask中,優於其餘位於macroTask隊列中的任務,因此5會優於1,6輸出,而Timer優於Check階段,因此1,6。
綜上,關於最關鍵的順序,咱們要依據如下幾條規則:
同一個上下文下,MicroTask會比MacroTask先運行
而後瀏覽器按照一個MacroTask任務,全部MicroTask的順序運行,Node按照六個階段的順序運行,並在每一個階段後面都會運行MicroTask隊列
同個MicroTask隊列下process.tick()會優於Promise
對前端感興趣的,想要進行學習的朋友能夠加我qq裙:213126486 邀請碼:落葉,一塊兒討論進步~