在閱讀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()
使用示例
process.nextTick()
可用於控制代碼執行順序process.nextTick()
可徹底異步化APIprocess.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發生前調用。前端
function MyThing(options) {
this.setupOptions(options);
process.nextTick(() => {
this.startDoingStuff();
});
}
const thing = new MyThing();
thing.getReadyForStuff(); // thing.startDoingStuff() 在準備好以後再調用,而不是在初始化就調用
複製代碼
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()
不會在代碼中出現,即便它是異步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()比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延遲參數了!」
這個哲學會致使一些潛在問題。下面來看下這段代碼:
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
努力成爲優秀前端工程師!