在閱讀mqtt.js源碼的時候,遇到一段很使人疑惑的代碼。
nextTickWork中調用process.nextTick(work)
,其中函數work又調用了nextTickWork。
這怎麼這想遞歸呢?又有點像死循環?
究竟是怎麼回事啊,下面咱們來系統性學習一下process.nextTick()
。html
writable._write = function (buf, enc, done) { completeParse = done parser.parse(buf) work() // 開始nextTick } function work () { var packet = packets.shift() if (packet) { that._handlePacket(packet, nextTickWork) // 注意這裏 } else { var done = completeParse completeParse = null if (done) done() } } function nextTickWork () { if (packets.length) { process.nextTick(work) // 注意這裏 } else { var done = completeParse completeParse = null done() } }
初識process.nextTick()
前端
process.nextTick()
知識點process.nextTick()
使用示例node
process.nextTick()
可用於控制代碼執行順序process.nextTick()
可徹底異步化API爲何說process.nextTick()是更增強大的異步專家?git
爲何要用process.nextTick()?github
process.nextTick()
process.nextTick(callback[, ...args])
process.nextTick()
知識點process.nextTick()
會將callback添加到」next tick queue「process.nextTick()
可能會致使一個無限循環,須要去適時終止遞歸。process.nextTick()
可用於控制代碼執行順序。保證方法在對象完成constructor後可是在I/O發生前調用。process.nextTick()
可徹底異步化API。API要麼100%同步要麼100%異步是很重要的,能夠經過process.nextTick()
去達到這種保證process.nextTick()
使用示例process.nextTick()
對於API的開發很重要console.log('start'); process.nextTick(() => { console.log('nextTick callback'); }); console.log('scheduled'); // start // scheduled // nextTick callback
process.nextTick()
可用於控制代碼執行順序process.nextTick()
可用於賦予用戶一種能力,去保證方法在對象完成constructor後可是在I/O發生前調用。web
function MyThing(options) { this.setupOptions(options); process.nextTick(() => { this.startDoingStuff(); }); } const thing = new MyThing(); thing.getReadyForStuff(); // thing.startDoingStuff() 在準備好以後再調用,而不是在初始化就調用
API要麼100%同步要麼100%異步是很重要的,能夠經過process.nextTick()
去使得一個API徹底異步化達到這種保證。segmentfault
// 多是同步,多是異步的API function maybeSync(arg, cb) { if (arg) { cb(); return; } fs.stat('file', cb); }
// maybeTrue可能爲false可能爲true,因此foo(),bar()的執行順序沒法保證。 const maybeTrue = Math.random() > 0.5; maybeSync(maybeTrue, () => { foo(); }); bar();
如何使得API徹底是一個async的API呢?或者說如何保證foo()在bar()以後調用呢?
經過process.nextTick()徹底異步化。api
// 徹底是異步的API function definitelyAsync(arg, cb) { if (arg) { process.nextTick(cb); return; } fs.stat('file', cb); }
你也許會發現process.nextTick()
不會在代碼中出現,即便它是異步API的一部分。這是爲何呢?由於process.nextTick()
不是event loop的技術部分。取而代之的是,nextTickQueue
會在當前的操做完成後執行,不考慮event loop的當前階段。在這裏,operation
的定義是指從底層的C/C++處理程序處處理須要執行的JavaScript的轉換。瀏覽器
回過頭來看咱們的程序,任何階段你調用process.nextTick()
,全部傳遞進process.nextTick()
的callback會在event loop繼續前完成解析。這會形成一些糟糕的狀況,經過創建一個遞歸的process.nextTick()調用,它容許你「starve」你的I/O。,這樣可使得event loop不到達poll階段。微信
爲何說「process.nextTick()比setTimeout()更精準的延遲調用」呢?
不要着急,帶着疑問去看下文便可。看懂就能找到答案。
爲何Node.js要設計這種遞歸的process.nextTick()
呢 ?這是由於Node.js的設計哲學的一部分是API必須是async的,即便它沒有必要。 看下下面的例子:
function apiCall(arg, callback) { if(typeof arg !== 'string'){ return process.nextTick(callback, new TypeError('argument should be string')); } }
代碼片斷作了argument的檢查,若是它不是string類型的話,它會將一個error傳遞進callback中。這個API最近進行了更新,容許將參數傳遞到process.nextTick()
,從而容許在callback以後傳遞的任何參數做爲回調的參數進行傳遞,這樣就不用嵌套函數了。
咱們如今作的是將一個error傳遞到user,可是必須在咱們容許執行的代碼執行完以後。經過使用process.nextTick()
,咱們能夠保證apiCall
老是在用戶代碼的其他部分和容許事件循環繼續以前運行它的callback。爲了實現這一點,JS call stack能夠被展開,而後immediately執行提供的回調,從而容許一我的遞歸調用process.nextTick()
而不至於拋出RangeError: Maximum call stack size exceeded from v8.
)
一句話歸納的話就是:process.nextTick()
能夠保證咱們要執行的代碼會正常執行,最後再拋出這個error。這個操做是setTimeout()沒法作到的,由於咱們並不知道執行那些代碼須要多長時間。
是怎麼作到process.nextTick(callback)比setTimeout()更嚴格的延遲調用的呢?
process.nextTick(callback)能夠保證在這一次事件循環的call stack 解除(unwound)後,在下一次事件循環前,調用callback。
能夠把緣由再講得詳細一點嗎?
process.nextTick()會在這一次event loop的call stack清空後(下一次event loop開始前)再調用callback。而setTimeout()是並不知道何時call stack清空的。咱們setTimeout(cb, 1000),可能1s後,因爲種種緣由call 棧中還留存了幾個函數沒有調用,調大到10秒又很不合適,由於它可能1.1秒就執行完了。
相信有必定開發經驗的同窗一看就懂,一看就知道process.nextTick()的強大了。
內心默唸:「終於不用調坑爹的setTimeout延遲參數了!」
這個哲學會致使一些潛在問題。下面來看下這段代碼:
let bar; // 它是異步,可是同步調用callback function someAsyncApiCall(callback) { callback(); } // callback在someAsyncApiCall完成前調用 someAsyncApiCall(() => { // 由於someAsyncApiCall尚未完成,bar還未賦值 console.log('bar', bar); // undefined }); bar = 1;
用戶定義了有一個異步簽名的someAsyncApiCall()
,可是它實際上同步執行了。當someAsyncApiCall()調用的時候,內部的callback在異步操做還沒完成前就調用了,callback嘗試得到bar的引用,可是做用域內是沒有這個變量的,由於script尚未執行到bar = 1
這一步。
有什麼辦法能夠保證在賦值以後再調用這個函數呢?
經過將callback
傳遞進process.nextTick()
,script能夠成功執行,而且能夠訪問到全部變量和函數等等,而且在callback調用以前已經初始化好。 它擁有容許不容許事件循環繼續的優勢。對於用戶在event loop想要繼續運行以前alert一個error是頗有用的。
下面是經過process.nextTick()
改進的上面的代碼:
let bar; function someAsyncApiCall(callback) { process.nextTick(callback); } someAsyncApiCall(() => { console.log('bar', bar); // 1 }); bar = 1;
還有一個真實世界的例子:
const server = net.createServer(() => {}).listen(8080); server.on('listening', () => {});
當咱們傳遞一個端口號進去時,端口號會被馬上綁定。因此'listening' callback能夠被當即調用。問題是.on('listening');
這個callback可能還沒設置呢?這要怎麼辦?
爲了作到在精準無誤的監聽到listen的動做,將對‘listening’事件的監聽操做,隊列到nextTick(),從而能夠容許代碼徹底運行完畢。 這可使得用戶設置任何他們想要的事件。
這裏有一個匹配用戶指望的例子。
const server = net.createServer(); server.on('connection', (conn) => { }); server.listen(8080); server.on('listening', () => { });
listen()
在event.loop循環的開始運行,可是listening callback被放置在setImmediate()
中。除非傳入hostname,不然當即綁定端口。event loop在處理的時候,它必須在poll階段,這也就是意味着沒有機會接收到鏈接,從而容許在偵聽listen事件前觸發connection事件。
再來看一個例子:
運行一個繼承了EventEmitter的function constructor,它想在constructor內部發出一個'event'事件。
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!'); // nothing happens });
沒法在constructor內理解emit一個event,由於script不會運行到用戶監聽event響應callback的位置。因此在constructor內部,可使用process.nextTick
設置一個callback在constructor完成以後emit這個event,因此最終的代碼以下:
const EventEmitter = require('events'); const util = require('util'); function MyEmitter() { EventEmitter.call(this); // 一旦分配了handler處理程序,就使用process.nextTick()發出這個事件 process.nextTick(() => { this.emit('event'); }); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); // an event occurred!' });
回過頭來看下mqtt.js用於接收消息的message event源碼中的process.nextTick()
process.nextTick()確保work函數準確在這一次call stack清空後,下一次event loop開始前調用。
writable._write = function (buf, enc, done) { completeParse = done parser.parse(buf) work() // 開始nextTick } function work () { var packet = packets.shift() if (packet) { that._handlePacket(packet, nextTickWork) // 注意這裏 } else { // 停止process.nextTick()的遞歸 var done = completeParse completeParse = null if (done) done() } } function nextTickWork () { if (packets.length) { process.nextTick(work) // 注意這裏 } else { // 停止process.nextTick()的遞歸 var done = completeParse completeParse = null done() } }
經過對process.nextTick()的學習以及對源碼的理解,咱們得出:
流寫入本地執行work(),若接收到有效的數據包,開始process.nextTick()遞歸。
function nextTickWork () { if (packets.length) { work() // 注意這裏 } }
會形成當前的event loop永遠不會停止,一直處於阻塞狀態,形成一個無限循環。
正是由於有了process.nextTick(),才能確保work函數準確在這一次call stack清空後,下一次event loop開始前調用。
參考連接:
期待和你們交流,共同進步,歡迎你們加入我建立的與前端開發密切相關的技術討論小組:
- SegmentFault技術圈:ES新規範語法糖
- SegmentFault專欄:趁你還年輕,作個優秀的前端工程師
- 知乎專欄:趁你還年輕,作個優秀的前端工程師
- Github博客: 趁你還年輕233的我的博客
- 前端開發QQ羣:660634678
- 微信公衆號: 生活在瀏覽器裏的咱們 / excellent_developers
努力成爲優秀前端工程師!