在閱讀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)用纯赎。
參考鏈接:
- https://nodejs.org/uk/docs/guides/event-loop-timers-and-nexttick/
- https://nodejs.org/dist/latest-v13.x/docs/api/process.html#process_process_nexttick_callback_args
- https://github.com/mqttjs/MQTT.js/blob/master/lib/client.js
- https://github.com/FrankKai/FrankKai.github.io/issues/204
期待和大家交流,共同進(jìn)步南蹂,歡迎大家加入我創(chuàng)建的與前端開發(fā)密切相關(guān)的技術(shù)討論小組:
- SegmentFault技術(shù)圈:ES新規(guī)范語法糖
- SegmentFault專欄:趁你還年輕犬金,做個(gè)優(yōu)秀的前端工程師
- 知乎專欄:趁你還年輕,做個(gè)優(yōu)秀的前端工程師
- Github博客: 趁你還年輕233的個(gè)人博客
- 前端開發(fā)QQ群:660634678
- 微信公眾號: 生活在瀏覽器里的我們 / excellent_developers
努力成為優(yōu)秀前端工程師碎紊!