最近對Event loop比較感興趣,因此瞭解了一下。可是發現整個Event loop儘管有不少篇文章,可是沒有一篇能夠看完就對它全部內容都瞭解的文章。大部分的文章都只闡述了瀏覽器或者Node兩者之一,沒有對比的去看的話,認識老是淺一點。因此纔有了這篇整理了百家之長的文章。
Event loop:爲了協調事件(event),用戶交互(user interaction),腳本(script),渲染(rendering),網絡(networking)等,用戶代理(user agent)必須使用事件循環(event loops)。(3月29修訂)
那什麼是事件?html
事件:事件就是因爲某種外在或內在的信息狀態發生的變化,從而致使出現了對應的反應。好比說用戶點擊了一個按鈕,就是一個事件;HTML頁面完成加載,也是一個事件。一個事件中會包含多個任務。
咱們在以前的文章中提到過,JavaScript引擎又稱爲JavaScript解釋器,是JavaScript解釋爲機器碼的工具,分別運行在瀏覽器和Node中。而根據上下文的不一樣,Event loop也有不一樣的實現:其中Node使用了libuv庫來實現Event loop; 而在瀏覽器中,html規範定義了Event loop,具體的實現則交給不一樣的廠商去完成。node
因此,瀏覽器的Event loop和Node的Event loop是兩個概念,下面分別來看一下。git
在實際工做中,瞭解Event loop的意義能幫助你分析一些異步次序的問題(固然,隨着es7 async和await的流行,這樣的機會愈來愈少了)。除此之外,它還對你瞭解瀏覽器和Node的內部機制有積極的做用;對於參加面試,被問到一堆異步操做的執行順序時,也不至於兩眼抓瞎。github
在JavaScript中,任務被分爲Task(又稱爲MacroTask,宏任務)和MicroTask(微任務)兩種。它們分別包含如下內容:面試
MacroTask: script(總體代碼), setTimeout, setInterval, setImmediate(node獨有), I/O, UI rendering
MicroTask: process.nextTick(node獨有), Promises, Object.observe(廢棄), MutationObserver
須要注意的一點是:在同一個上下文中,總的執行順序爲同步代碼—>microTask—>macroTask[6]。這一塊咱們在下文中會講。segmentfault
瀏覽器中,一個事件循環裏有不少個來自不一樣任務源的任務隊列(task queues),每個任務隊列裏的任務是嚴格按照先進先出的順序執行的。可是,由於瀏覽器本身調度的關係,不一樣任務隊列的任務的執行順序是不肯定的。promise
具體來講,瀏覽器會不斷從task隊列中按順序取task執行,每執行完一個task都會檢查microtask隊列是否爲空(執行完一個task的具體標誌是函數執行棧爲空),若是不爲空則會一次性執行完全部microtask。而後再進入下一個循環去task隊列中取下一個task執行,以此類推。瀏覽器
注意:圖中橙色的MacroTask任務隊列也應該是在不斷被切換着的。網絡
本段大批量引用了《什麼是瀏覽器的事件循環(Event Loop)》的相關內容,想看更加詳細的描述能夠自行取用。異步
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任務同理。
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) 瀏覽器輸出: time1 promise1 time2 promise2 Node輸出: time1 time2 promise1 promise2
在這個例子中,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 timer2 promise1 或者 promise2 timer2 timer1 promise2 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 定時器詳解》。
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 2 1
由於咱們上文說過microTask會優於macroTask運行,因此先輸出下面兩個,而在Node中process.nextTick比Promise更加優先[3],因此4在3前。而根據咱們以前所說的Node沒有絕對意義上的0ms,因此1,2的順序不固定。
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 3 5 1 6
這個例子來源於《JavaScript中的執行機制》。Promise的代碼是同步代碼,then和catch纔是異步的,因此4要同步輸出,而後Promise的then位於microTask中,優於其餘位於macroTask隊列中的任務,因此5會優於1,6輸出,而Timer優於Check階段,因此1,6。
綜上,關於最關鍵的順序,咱們要依據如下幾條規則:
process.tick()
會優於Promise
Event loop仍是比較深奧的,深刻進去會有不少有意思的東西,有任何問題還望不吝指出。