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

在閱讀mqtt.js源碼的時(shí)候,遇到一段很令人疑惑的代碼彩届。
nextTickWork中調(diào)用process.nextTick(work),其中函數(shù)work又調(diào)用了nextTickWork誓酒。
這怎么這想遞歸呢樟蠕?又有點(diǎn)像死循環(huán)?
到底是怎么回事啊靠柑,下面我們來系統(tǒng)性學(xué)習(xí)一下process.nextTick()寨辩。

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()知識點(diǎn)
    • process.nextTick()使用示例
      • 最簡單的例子
      • process.nextTick()可用于控制代碼執(zhí)行順序
      • process.nextTick()可完全異步化API
  • 如何理解process.nextTick()?
  • 為什么說process.nextTick()是更加強(qiáng)大的異步專家歼冰?
    • process.nextTick()比setTimeout()更嚴(yán)格的延遲調(diào)用
    • process.nextTick()解決的實(shí)際問題
  • 為什么要用process.nextTick()靡狞?
    • 允許用戶處理error,清除不需要的資源隔嫡,或者在事件循環(huán)前再次嘗試請求
    • 有時(shí)確保callback在call stack unwound(解除)后甸怕,event loop繼續(xù)循環(huán)前 調(diào)用
  • 回顧一下

初識process.nextTick()

語法(callback和可選args)

process.nextTick(callback[, ...args])
  • callback 回調(diào)函數(shù)
  • args 調(diào)用callback時(shí)額外傳的參數(shù)

process.nextTick()知識點(diǎn)

  • process.nextTick()會將callback添加到”next tick queue“
  • ”next tick queue“會在當(dāng)前JavaScript stack執(zhí)行完成后,下一次event loop開始執(zhí)行前按照FIFO出隊(duì)
  • 如果遞歸調(diào)用process.nextTick()可能會導(dǎo)致一個(gè)無限循環(huán)腮恩,需要去適時(shí)終止遞歸梢杭。
  • process.nextTick()可用于控制代碼執(zhí)行順序。保證方法在對象完成constructor后但是在I/O發(fā)生前調(diào)用秸滴。
  • process.nextTick()可完全異步化API武契。API要么100%同步要么100%異步是很重要的,可以通過process.nextTick()去達(dá)到這種保證

process.nextTick()使用示例

  • 最簡單的例子
  • process.nextTick()對于API的開發(fā)很重要
最簡單的例子
console.log('start');
process.nextTick(() => {
  console.log('nextTick callback');
});
console.log('scheduled');
// start
// scheduled
// nextTick callback
process.nextTick()可用于控制代碼執(zhí)行順序

process.nextTick()可用于賦予用戶一種能力,去保證方法在對象完成constructor后但是在I/O發(fā)生前調(diào)用吝羞。

function MyThing(options) {
  this.setupOptions(options);
  process.nextTick(() => {
    this.startDoingStuff();
  });
}
const thing = new MyThing();
thing.getReadyForStuff(); // thing.startDoingStuff() 在準(zhǔn)備好之后再調(diào)用兰伤,而不是在初始化就調(diào)用
API要么100%同步要么100%異步時(shí)很重要的

API要么100%同步要么100%異步是很重要的,可以通過process.nextTick()去使得一個(gè)API完全異步化達(dá)到這種保證钧排。

// 可能是同步敦腔,可能是異步的API
function maybeSync(arg, cb) {
  if (arg) {
    cb();
    return;
  }
  fs.stat('file', cb);
}
// maybeTrue可能為false可能為true,所以foo(),bar()的執(zhí)行順序無法保證恨溜。
const maybeTrue = Math.random() > 0.5;
maybeSync(maybeTrue, () => {
  foo();
});
bar();

如何使得API完全是一個(gè)async的API呢符衔?或者說如何保證foo()在bar()之后調(diào)用呢?
通過process.nextTick()完全異步化糟袁。

// 完全是異步的API
function definitelyAsync(arg, cb) {
  if (arg) {
    process.nextTick(cb);
    return;
  }
  fs.stat('file', cb);
}

如何理解process.nextTick()

你也許會發(fā)現(xiàn)process.nextTick()不會在代碼中出現(xiàn)判族,即使它是異步API的一部分。這是為什么呢项戴?因?yàn)?code>process.nextTick()不是event loop的技術(shù)部分形帮。取而代之的是,nextTickQueue會在當(dāng)前的操作完成后執(zhí)行周叮,不考慮event loop的當(dāng)前階段辩撑。在這里,operation的定義是指從底層的C/C++處理程序到處理需要執(zhí)行的JavaScript的轉(zhuǎn)換仿耽。

回過頭來看我們的程序合冀,任何階段你調(diào)用process.nextTick(),所有傳遞進(jìn)process.nextTick()的callback會在event loop繼續(xù)前完成解析项贺。這會造成一些糟糕的情況君躺,通過建立一個(gè)遞歸的process.nextTick()調(diào)用,它允許你“starve”你的I/O开缎。棕叫,這樣可以使得event loop不到達(dá)poll階段。

為什么說process.nextTick()是更加強(qiáng)大的異步專家啥箭?

process.nextTick()比setTimeout()更精準(zhǔn)的延遲調(diào)用

為什么說“process.nextTick()比setTimeout()更精準(zhǔn)的延遲調(diào)用”呢谍珊?
不要著急,帶著疑問去看下文即可急侥∑鲋停看懂就能找到答案。

為什么Node.js要設(shè)計(jì)這種遞歸的process.nextTick()呢 坏怪?這是因?yàn)镹ode.js的設(shè)計(jì)哲學(xué)的一部分是API必須是async的贝润,即使它沒有必要。 看下下面的例子:

function apiCall(arg, callback) {
    if(typeof arg !== 'string'){
        return process.nextTick(callback, new TypeError('argument should be string'));
    }
}

代碼片段做了argument的檢查铝宵,如果它不是string類型的話打掘,它會將一個(gè)error傳遞進(jìn)callback中华畏。這個(gè)API最近進(jìn)行了更新,允許將參數(shù)傳遞到process.nextTick()尊蚁,從而允許在callback之后傳遞的任何參數(shù)作為回調(diào)的參數(shù)進(jìn)行傳遞亡笑,這樣就不用嵌套函數(shù)了。

我們現(xiàn)在做的是將一個(gè)error傳遞到user横朋,但是必須在我們允許執(zhí)行的代碼執(zhí)行完之后仑乌。通過使用process.nextTick(),我們可以保證apiCall總是在用戶代碼的其余部分和允許事件循環(huán)繼續(xù)之前運(yùn)行它的callback琴锭。為了實(shí)現(xiàn)這一點(diǎn)晰甚,JS call stack可以被展開,然后immediately執(zhí)行提供的回調(diào)决帖,從而允許一個(gè)人遞歸調(diào)用process.nextTick()而不至于拋出RangeError: Maximum call stack size exceeded from v8.

一句話概括的話就是:process.nextTick()可以保證我們要執(zhí)行的代碼會正常執(zhí)行厕九,最后再拋出這個(gè)error。這個(gè)操作是setTimeout()無法做到的地回,因?yàn)槲覀儾⒉恢缊?zhí)行那些代碼需要多長時(shí)間扁远。

是怎么做到process.nextTick(callback)比setTimeout()更嚴(yán)格的延遲調(diào)用的呢?
process.nextTick(callback)可以保證在這一次事件循環(huán)的call stack 解除(unwound)后落君,在下一次事件循環(huán)前穿香,調(diào)用callback。

可以把原因再講得詳細(xì)一點(diǎn)嗎绎速?

process.nextTick()會在這一次event loop的call stack清空后(下一次event loop開始前)再調(diào)用callback。而setTimeout()是并不知道什么時(shí)候call stack清空的焙蚓。我們setTimeout(cb, 1000)纹冤,可能1s后,由于種種原因call 棧中還留存了幾個(gè)函數(shù)沒有調(diào)用购公,調(diào)大到10秒又很不合適萌京,因?yàn)樗赡?.1秒就執(zhí)行完了。

相信有一定開發(fā)經(jīng)驗(yàn)的同學(xué)一看就懂宏浩,一看就知道process.nextTick()的強(qiáng)大了知残。
心里默念:“終于不用調(diào)坑爹的setTimeout延遲參數(shù)了!”

強(qiáng)大的process.nextTick()解決的實(shí)際問題

這個(gè)哲學(xué)會導(dǎo)致一些潛在問題比庄。下面來看下這段代碼:

let bar;
//  它是異步求妹,但是同步調(diào)用callback
function someAsyncApiCall(callback) { callback(); }
// callback在someAsyncApiCall完成前調(diào)用
someAsyncApiCall(() => {
  // 因?yàn)閟omeAsyncApiCall還沒有完成,bar還未賦值
  console.log('bar', bar); // undefined
});
bar = 1;

用戶定義了有一個(gè)異步簽名的someAsyncApiCall()佳窑,但是它實(shí)際上同步執(zhí)行了制恍。當(dāng)someAsyncApiCall()調(diào)用的時(shí)候,內(nèi)部的callback在異步操作還沒完成前就調(diào)用了神凑,callback嘗試獲得bar的引用净神,但是作用域內(nèi)是沒有這個(gè)變量的何吝,因?yàn)閟cript還沒有執(zhí)行到bar = 1這一步。

有什么辦法可以保證在賦值之后再調(diào)用這個(gè)函數(shù)呢鹃唯?

通過將callback傳遞進(jìn)process.nextTick()爱榕,script可以成功執(zhí)行,并且可以訪問到所有變量和函數(shù)等等坡慌,并且在callback調(diào)用之前已經(jīng)初始化好呆细。 它擁有允許不允許事件循環(huán)繼續(xù)的優(yōu)點(diǎn)。對于用戶在event loop想要繼續(xù)運(yùn)行之前alert一個(gè)error是很有用的八匠。

下面是通過process.nextTick()改進(jìn)的上面的代碼:

let bar;
function someAsyncApiCall(callback) {
    process.nextTick(callback);
}
someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});
bar = 1;

還有一個(gè)真實(shí)世界的例子:

const server = net.createServer(() => {}).listen(8080);
server.on('listening', () => {});

當(dāng)我們傳遞一個(gè)端口號進(jìn)去時(shí)絮爷,端口號會被立刻綁定。所以'listening' callback可以被立即調(diào)用梨树。問題是.on('listening');這個(gè)callback可能還沒設(shè)置呢坑夯?這要怎么辦?

為了做到在精準(zhǔn)無誤的監(jiān)聽到listen的動作將對‘listening’事件的監(jiān)聽操作抡四,隊(duì)列到nextTick()柜蜈,從而可以允許代碼完全運(yùn)行完畢。 這可以使得用戶設(shè)置任何他們想要的事件指巡。

為什么要用process.nextTick()淑履?

  • 允許用戶處理error,清除不需要的資源藻雪,或者在事件循環(huán)前再次嘗試請求
  • 有時(shí)確保callback在call stack unwound(解除)后秘噪,event loop繼續(xù)循環(huán)前 調(diào)用

允許用戶處理error,清除不需要的資源勉耀,或者在事件循環(huán)前再次嘗試請求

這里有一個(gè)匹配用戶期望的例子指煎。

const server = net.createServer();
server.on('connection', (conn) => { });

server.listen(8080);
server.on('listening', () => { });

listen()在event.loop循環(huán)的開始運(yùn)行,但是listening callback被放置在setImmediate()中便斥。除非傳入hostname至壤,否則立即綁定端口。event loop在處理的時(shí)候枢纠,它必須在poll階段像街,這也就是意味著沒有機(jī)會接收到連接,從而允許在偵聽listen事件前觸發(fā)connection事件晋渺。

有時(shí)確保callback在call stack unwound(解除)后镰绎,event loop繼續(xù)循環(huán)前 調(diào)用

再來看一個(gè)例子:
運(yùn)行一個(gè)繼承了EventEmitter的function constructor,它想在constructor內(nèi)部發(fā)出一個(gè)'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內(nèi)理解emit一個(gè)event跟狱,因?yàn)閟cript不會運(yùn)行到用戶監(jiān)聽event響應(yīng)callback的位置。所以在constructor內(nèi)部户魏,可以使用process.nextTick設(shè)置一個(gè)callback在constructor完成之后emit這個(gè)event驶臊,所以最終的代碼如下:

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);
  // 一旦分配了handler處理程序挪挤,就使用process.nextTick()發(fā)出這個(gè)事件
  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函數(shù)準(zhǔn)確在這一次call stack清空后,下一次event loop開始前調(diào)用关翎。

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()的學(xué)習(xí)以及對源碼的理解扛门,我們得出:
流寫入本地執(zhí)行work(),若接收到有效的數(shù)據(jù)包纵寝,開始process.nextTick()遞歸论寨。

  • 開始nextTick的條件:if(packet)/if (packets.length) 也就是說有接收到websocket包時(shí)開始。
  • 遞歸nextTick的過程:work()->nextTickWork()->process.nextTick(work)爽茴。
  • 結(jié)束nextTick的條件:packet為空或者packets為空葬凳,通過completeParse=null,done()結(jié)束遞歸室奏。
  • 如果對work不加process.nextTick會怎樣火焰?
function nextTickWork () {
  if (packets.length) {
    work() // 注意這里
  }
}

會造成當(dāng)前的event loop永遠(yuǎn)不會中止,一直處于阻塞狀態(tài)胧沫,造成一個(gè)無限循環(huán)昌简。
正是因?yàn)橛辛藀rocess.nextTick(),才能確保work函數(shù)準(zhǔn)確在這一次call stack清空后绒怨,下一次event loop開始前調(diào)用纯赎。

參考鏈接:

期待和大家交流,共同進(jìn)步南蹂,歡迎大家加入我創(chuàng)建的與前端開發(fā)密切相關(guān)的技術(shù)討論小組:

image

努力成為優(yōu)秀前端工程師碎紊!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末佑附,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子仗考,更是在濱河造成了極大的恐慌,老刑警劉巖词爬,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秃嗜,死亡現(xiàn)場離奇詭異,居然都是意外死亡顿膨,警方通過查閱死者的電腦和手機(jī)锅锨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恋沃,“玉大人必搞,你說我怎么就攤上這事∧矣剑” “怎么了恕洲?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵塔橡,是天一觀的道長。 經(jīng)常有香客問我霜第,道長葛家,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任泌类,我火速辦了婚禮癞谒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘刃榨。我一直安慰自己弹砚,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布枢希。 她就那樣靜靜地躺著桌吃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪晴玖。 梳的紋絲不亂的頭發(fā)上读存,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機(jī)與錄音呕屎,去河邊找鬼让簿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛秀睛,可吹牛的內(nèi)容都是我干的尔当。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼蹂安,長吁一口氣:“原來是場噩夢啊……” “哼椭迎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起田盈,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤畜号,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后允瞧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體简软,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年述暂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了痹升。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡畦韭,死狀恐怖疼蛾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情艺配,我是刑警寧澤察郁,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布衍慎,位于F島的核電站,受9級特大地震影響绳锅,放射性物質(zhì)發(fā)生泄漏西饵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一鳞芙、第九天 我趴在偏房一處隱蔽的房頂上張望眷柔。 院中可真熱鬧,春花似錦原朝、人聲如沸驯嘱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鞠评。三九已至,卻和暖如春壕鹉,著一層夾襖步出監(jiān)牢的瞬間剃幌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工晾浴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留负乡,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓脊凰,卻偏偏與公主長得像抖棘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子狸涌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容