強大的異步專家process.nextTick()

在閱讀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()
    • 語法(callback和可選args)
    • process.nextTick()知識點
    • process.nextTick()使用示例
      • 最簡單的例子
      • process.nextTick()可用於控制代碼執行順序
      • process.nextTick()可徹底異步化API
  • 如何理解process.nextTick()?
  • 爲何說process.nextTick()是更增強大的異步專家?
    • process.nextTick()比setTimeout()更嚴格的延遲調用
    • process.nextTick()解決的實際問題
  • 爲何要用process.nextTick()?
    • 容許用戶處理error,清除不須要的資源,或者在事件循環前再次嘗試請求
    • 有時確保callback在call stack unwound(解除)後,event loop繼續循環前 調用
  • 回顧一下

初識process.nextTick()

語法(callback和可選args)

process.nextTick(callback[, ...args])
複製代碼
  • callback 回調函數
  • args 調用callback時額外傳的參數

process.nextTick()知識點

  • process.nextTick()會將callback添加到」next tick queue「
  • 」next tick queue「會在當前JavaScript stack執行完成後,下一次event loop開始執行前按照FIFO出隊
  • 若是遞歸調用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發生前調用。前端

function MyThing(options) {
  this.setupOptions(options);
  process.nextTick(() => {
    this.startDoingStuff();
  });
}
const thing = new MyThing();
thing.getReadyForStuff(); // thing.startDoingStuff() 在準備好以後再調用,而不是在初始化就調用
複製代碼
API要麼100%同步要麼100%異步時很重要的

API要麼100%同步要麼100%異步是很重要的,能夠經過process.nextTick()去使得一個API徹底異步化達到這種保證。node

// 多是同步,多是異步的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()徹底異步化。git

// 徹底是異步的API
function definitelyAsync(arg, cb) {
  if (arg) {
    process.nextTick(cb);
    return;
  }
  fs.stat('file', cb);
}
複製代碼

如何理解process.nextTick()

你也許會發現process.nextTick()不會在代碼中出現,即便它是異步API的一部分。這是爲何呢?由於process.nextTick()不是event loop的技術部分。取而代之的是,nextTickQueue會在當前的操做完成後執行,不考慮event loop的當前階段。在這裏,operation的定義是指從底層的C/C++處理程序處處理須要執行的JavaScript的轉換。github

回過頭來看咱們的程序,任何階段你調用process.nextTick(),全部傳遞進process.nextTick()的callback會在event loop繼續前完成解析。這會形成一些糟糕的狀況,經過創建一個遞歸的process.nextTick()調用,它容許你「starve」你的I/O。,這樣可使得event loop不到達poll階段。web

爲何說process.nextTick()是更增強大的異步專家?

process.nextTick()比setTimeout()更精準的延遲調用

爲何說「process.nextTick()比setTimeout()更精準的延遲調用」呢? 不要着急,帶着疑問去看下文便可。看懂就能找到答案。segmentfault

爲何Node.js要設計這種遞歸的process.nextTick()呢 ?這是由於Node.js的設計哲學的一部分是API必須是async的,即便它沒有必要。 看下下面的例子:api

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延遲參數了!」

強大的process.nextTick()解決的實際問題

這個哲學會致使一些潛在問題。下面來看下這段代碼:

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(),從而能夠容許代碼徹底運行完畢。 這可使得用戶設置任何他們想要的事件。

爲何要用process.nextTick()?

  • 容許用戶處理error,清除不須要的資源,或者在事件循環前再次嘗試請求
  • 有時確保callback在call stack unwound(解除)後,event loop繼續循環前 調用

容許用戶處理error,清除不須要的資源,或者在事件循環前再次嘗試請求

這裏有一個匹配用戶指望的例子。

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事件。

有時確保callback在call stack unwound(解除)後,event loop繼續循環前 調用

再來看一個例子: 運行一個繼承了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()遞歸。

  • 開始nextTick的條件:if(packet)/if (packets.length) 也就是說有接收到websocket包時開始。
  • 遞歸nextTick的過程:work()->nextTickWork()->process.nextTick(work)。
  • 結束nextTick的條件:packet爲空或者packets爲空,經過completeParse=null,done()結束遞歸。
  • 若是對work不加process.nextTick會怎樣?
function nextTickWork () {
  if (packets.length) {
    work() // 注意這裏
  }
}
複製代碼

會形成當前的event loop永遠不會停止,一直處於阻塞狀態,形成一個無限循環。 正是由於有了process.nextTick(),才能確保work函數準確在這一次call stack清空後,下一次event loop開始前調用。

參考連接:

期待和你們交流,共同進步,歡迎你們加入我建立的與前端開發密切相關的技術討論小組:

努力成爲優秀前端工程師!

相關文章
相關標籤/搜索