1. 程式人生 > >極簡 Node.js 入門 - 2.4 定時器

極簡 Node.js 入門 - 2.4 定時器

> 極簡 Node.js 入門系列教程:[https://www.yuque.com/sunluyong/node](https://www.yuque.com/sunluyong/node) > > 本文更佳閱讀體驗:[https://www.yuque.com/sunluyong/node/timer](https://www.yuque.com/sunluyong/node/timer) timer 用於安排函式在未來某個時間點被呼叫,Node.js 中的定時器函式實現了與 Web 瀏覽器提供的定時器 API 類似的 API,但是使用了事件迴圈實現,Node.js 中有四個相關的方法 1. setTimeout(callback, delay[, ...args]) 1. setInterval(callback[, ...args]) 1. setImmediate(callback[, ...args]) 1. process.nextTick(callback[, ...args])
前兩個含義和 web 上的是一致的,後兩個是 Node.js 獨有的,效果看起來就是 setTimeout(callback, 0),在 Node.js 程式設計中使用的最多

Node.js 不保證回撥被觸發的確切時間,也不保證它們的順序,回撥會在儘可能接近指定的時間被呼叫。setTimeout 當 delay 大於 2147483647 或小於 1 時,則 delay 將會被設定為 1, 非整數的 delay 會被截斷為整數 ## 奇怪的執行順序 看一個示例,用幾種方法分別非同步列印一個數字 ```javascript setImmediate(console.log, 1); setTimeout(console.log, 1, 2); Promise.resolve(3).then(console.log); process.nextTick(console.log, 4); console.log(5); ``` 會列印 5 4 3 2 1 或者 5 4 3 1 2 ## 同步 & 非同步 第五行是同步執行,其它都是非同步的 ```javascript setImmediate(console.log, 1); setTimeout(console.log, 1, 2); Promise.resolve(3).then(console.log); process.nextTick(console.log, 4); /****************** 同步任務和非同步任務的分割線 ********************/ console.log(5); ``` 所以先列印 5,這個很好理解,剩下的都是非同步操作,Node.js 按照什麼順序執行呢? ## event loop Node.js 啟動後會初始化事件輪詢,過程中可能處理非同步呼叫、定時器排程和 process.nextTick(),然後開始處理event loop。[官網]()中有這樣一張圖用來介紹 event loop 操作順序 ``` ┌───────────────────────────┐ ┌─>
│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘ ``` event loop 的每個階段都有一個任務佇列,當 event loop 進入給定的階段時,將執行該階段的任務佇列,直到佇列清空或執行的回撥達到系統上限後,才會轉入下一個階段,當所有階段被順序執行一次後,稱 event loop 完成了一個 tick

非同步操作都被放到了下一個 event loop tick 中,process.nextTick 在進入下一次 event loop tick 之前執行,所以肯定在其它非同步操作之前 ```javascript setImmediate(console.log, 1); setTimeout(console.log, 1, 2); Promise.resolve(3).then(console.log); /****************** 下次 event loop tick 分割線 ********************/ process.nextTick(console.log, 4); /****************** 同步任務和非同步任務的分割線 ********************/ console.log(5); ``` 各個階段主要任務 1. **timers**:執行 setTimeout、setInterval 回撥 1. **pending callbacks**:執行 I/O(檔案、網路等) 回撥 1. **idle, prepare**:僅供系統內部呼叫 1. **poll**:獲取新的 I/O 事件,執行相關回調,在適當條件下把阻塞 node 1. **check**:setImmediate 回撥在此階段執行 1. **close callbacks**:執行 socket 等的 close 事件回撥 日常開發中絕大部分非同步任務都是在 timers、poll、check 階段處理的 ### timers
Node.js 會在 timers 階段檢查是否有過期的 timer,如果存在則把回撥放到 timer 佇列中等待執行,Node.js 使用單執行緒,受限於主執行緒空閒情況和機器其它程序影響,並不能保證 timer 按照精確時間執行
定時器主要有兩種 1. Immediate 1. Timeout Immediate 型別的計時器回撥會在 **check** 階段被呼叫,Timeout 計時器會在設定的時間過期後儘快的呼叫回撥,但 ```javascript setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); ``` 多次執行會發現列印的順序不一樣 ### poll poll 階段主要有兩個任務 1. 計算應該阻塞和輪詢 I/O 的時間 1. 然後,處理 poll 佇列裡的事件 當event loop進入 poll 階段且沒有被排程的計時器時 - 如果 poll 佇列不是空的 ,event loop 將迴圈訪問回撥佇列並同步執行,直到佇列已用盡或者達到了系統或達到最大回調數 - 如果 poll 佇列是空的 - 如果有 setImmediate() 任務,event loop 會在結束 **poll** 階段後進入 **check** 階段 - 如果沒有 setImmediate()任務,event loop 阻塞在 **poll** 階段等待回撥被新增到佇列中,然後立即執行
一旦 **poll** 佇列為空,event loop 將檢查 timer 佇列是否為空,如果非空則進入下一輪 event loop

上面提到了如果在不同的 I/O 裡,不能確定 setTimeout 和 setImmediate 的執行順序,但如果 setTimeout 和 setImmediate 在一個 I/O 回撥裡,肯定是 setImmediate 先執行,因為在 poll 階段檢查到有 setImmediate() 任務,event loop 直接進入 check 階段執行 setImmediate 回撥 ```javascript const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); }); ``` ### check 在該階段執行 setImmediate 回撥
### 為什麼 Promise.then 比 setTimeout 早一些 前端同學肯定都聽說過 micoTask 和 macroTask,Promise.then 屬於 microTask,在瀏覽器環境下 microTask 任務會在每個 macroTask 執行最末端呼叫

在 Node.js 環境下 microTask 會在每個階段完成之間呼叫,也就是每個階段執行最後都會執行一下 microTask 佇列 ```javascript setImmediate(console.log, 1); setTimeout(console.log, 1, 2); /****************** microTask 分割線 ********************/ Promise.resolve(3).then(console.log); // microTask 分割線 /****************** 下次 event loop tick 分割線 ********************/ process.nextTick(console.log, 4); /****************** 同步任務和非同步任務的分割線 ********************/ console.log(5); ``` ## setImmediate VS process.nextTick setImmediate 聽起來是立即執行,process.nextTick 聽起來是下一個時鐘執行,為什麼效果是反過來的?這就要從那段不堪回首的歷史講起

最開始的時候只有 process.nextTick 方法,沒有 setImmediate 方法,通過上面的分析可以看出來任何時候呼叫 process.nextTick(),nextTick 會在 event loop 之前執行,直到 nextTick 佇列被清空才會進入到下一 event loop,如果出現 process.nextTick 的遞迴呼叫程式沒有被正確結束,那麼 IO 的回撥將沒有機會被執行 ```javascript const fs = require('fs'); fs.readFile('a.txt', (err, data) => { console.log('read file task done!'); }); let i = 0; function test(){ if(i++ < 999999) { console.log(`process.nextTick ${i}`); process.nextTick(test); } } test(); ``` 執行程式將返回 ``` nextTick 1 nextTick 2 ... ... nextTick 999999 read file task done! ``` 於是乎需要一個不這麼 bug 的呼叫,setImmediate 方法出現了,比較令人費解的是在 process.nextTick 起錯名字的情況下,setImmediate 也用了一個錯誤的名字以示區分。。。 那麼是不是程式設計中應該杜絕使用  process.nextTick 呢?官方推薦大部分時候應該使用 setImmediate,同時對 process.nextTick 的最大呼叫堆疊做了限制,但 process.nextTick 的呼叫機制確實也能為我們解決一些棘手的問題 1. 允許使用者在 even tloop 開始之前 處理異常、執行清理任務 1. 允許回撥在呼叫棧 unwind 之後,下次 event loop 開始之前執行 一個類繼承了 EventEmitter,而且想在例項化的時候觸發一個事件 ```javascript const EventEmitter = require('events'); const util = require('util'); function MyEmitter() { EventEmitter.call(this); this.emit('event'); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); }); ``` 在建構函式執行 `this.emit('event')` 會導致事件觸發比事件回撥函式繫結早,使用 process.nextTick 可以輕鬆實現預期效果 ```javascript const EventEmitter = require('events'); const util = require('util'); function MyEmitter() { EventEmitter.call(this); // use nextTick to emit the event once a handler is assigned process.nextTick(() => { this.emit('event'); }); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); }); ``` ## 參考 1. [Node.js 事件迴圈,定時器和 process.nextTick()](https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/) 1. [深入理解js事件迴圈機制(Node.js篇)](http://lynnelv.github.io/js-event-loop-nodejs)