在阅读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
努力成为优秀前端工程师!