当前位置: 首页 > 后端技术 > Node.js

强大的异步expertprocess.nextTick()

时间:2023-04-03 20:54:22 Node.js

在阅读mqtt.js源码的时候遇到了一段很迷惑的代码。process.nextTick(work)在nextTickWork中调用,函数work调用nextTickWork。为什么要递归?有点像死循环?这是怎么回事,我们来系统地研究一下process.nextTick()。writable._write=function(buf,enc,done){completeParse=doneparser.parse(buf)work()//startnextTick}functionwork(){varpacket=packets.shift()if(packet){那。_handlePacket(packet,nextTickWork)//注意这里}else{vardone=completeParsecompleteParse=nullif(done)done()}}functionnextTickWork(){if(packets.length){process.nextTick(work)//注意这里}else{vardone=completeParsecompleteParse=nulldone()}}首先熟悉process.nextTick()语法(回调和可选args)process.nextTick()知识点process.nextTick()使用示例最简单的示例流程。nextTick()可以用来控制代码执行的顺序process.nextTick()可以完全异步API怎么理解process.nextTick()为什么process.nextTick()是更强大的异步专家?Process.nextTick()比setTimeout()更严格。通过调用process.nextTick()解决的实际问题更加严格。为什么要使用process.nextTick()?允许用户处理错误、清除不必要的资源或在事件循环之前再次尝试请求。有时确保在事件循环继续循环之前调用堆栈展开(移除)之后调用回调。optionalargs)process.nextTick(callback[,...args])callback回调函数argscallcallbac附加参数process.nextTick()知识点process.nextTick()会在k执行时将回调添加到“下一个tick队列”。FIFO出队如果递归调用process.nextTick(),可能会造成死循环,需要及时终止递归process.nextTick(),控制代码执行顺序。保证方法在对象完成其构造函数之后但在任何I/O发生之前被调用。process.nextTick()完全异步API。API是100%同步或100%异步非常重要。这种保证可以通过process.nextTick()来实现。示例console.log('start');process.nextTick(()=>{console.log('nextTickcallback');});console.log('scheduled');//start//scheduled//nextTickcallbackprocess.nextTick()可用于控制代码执行顺序process.nextTick()可用于使用户能够确保在对象完成构造函数之后但在I/O发生之前调用方法。functionMyThing(options){this.setupOptions(options);process.nextTick(()=>{this.startDoingStuff();});}constthing=newMyThing();thing.getReadyForStuff();//事物。startDoingStuff()在准备就绪后调用,而不是在初始化时调用。API是100%同步或100%异步的。API是100%同步或100%异步非常重要。你可以通过process.nextTick()来让一个API完全异步来实现这个保证。//可能是同步的,可能是异步的APIfunctionmaybeSync(arg,cb){if(arg){cb();return;}fs.stat('file',cb);}//maybeTrue可能为false可能为true,所以无法保证foo()和bar()的执行顺序。constmaybeTrue=Math.random()>0.5;maybeSync(maybeTrue,()=>{foo();});酒吧();如何使API完全异步?或者如何确保foo()在bar()之后被调用?通过process.nextTick()完全异步。//完全异步的API函数definitelyAsync(arg,cb){if(arg){process.nextTick(cb);return;}fs.stat('file',cb);}如何理解process.nextTick()你可能会发现process.nextTick()并没有出现在代码中,尽管它是异步API的一部分.为什么是这样?因为process.nextTick()不是事件循环的技术部分。相反,nextTickQueue将在当前操作完成后执行,而不管事件循环的当前阶段。在这里,操作的定义是指从底层的C/C++处理程序到需要执行的JavaScript的转换。回头看看我们的程序,在你调用process.nextTick()的任何阶段,所有传递给process.nextTick()的回调都会在事件循环继续之前被解析。这会导致一些糟糕的情况,通过设置递归process.nextTick()调用,让您“饿死”您的I/O,这样事件循环就不会到达轮询阶段。为什么process.nextTick()是更强大的异步专家?process.nextTick()延迟调用比setTimeout()更准确为什么说“process.nextTick()延迟调用比setTimeout()更准确”?别着急,带着问题阅读以下内容。明白了就可以找到答案。为什么Node.js要设计这个递归的process.nextTick()?这是因为Node.js的部分设计理念是API必须是异步的,即使它不是必须的。看看下面的例子:functionapiCall(arg,callback){if(typeofarg!=='string'){returnprocess.nextTick(callback,newTypeError('argumentshouldbestring'));}}代码片段确实检查参数,如果它不是字符串类型,它将向回调传递错误。此API最近更新为允许将参数传递给process.nextTick(),允许在回调之后传递的任何参数作为参数传递给回调,从而消除了对嵌套函数的需要。我们现在正在做的是向用户传递一个错误,但只有在我们允许执行的代码执行完毕之后。通过使用process.nextTick()我们可以保证apiCall总是在用户代码的其余部分之前和允许事件循环继续之前运行它的回调。为此,可以展开JS调用堆栈,然后立即执行提供的回调,从而允许递归调用process.nextTick()而不会抛出RangeError:Maximumcallstacksizeexceededfromv8.)简而言之就是:process.nextTick()可以保证我们要执行的代码会正常执行,最后抛出这个错误。这个操作不能通过setTimeout()来完成,因为我们不知道执行那些代码需要多长时间。如何使process.nextTick(callback)成为比setTimeout()更严格的延迟调用?process.nextTick(callback)可以保证本次事件循环的调用栈unwound后,在下一次事件循环之前,回调会被调用。你能更详细地解释一下原因吗?process.nextTick()会在事件循环的调用栈清空后(下一个事件循环开始前)调用回调。而setTimeout()并不知道什么时候调用栈被清除。对于我们的setTimeout(cb,1000),可能1秒后,由于各种原因,调用栈中还剩下几个函数没有被调用。增加到10秒不合适,因为可能1.1秒就执行完了。相信有一定开发经验的同学一看就会明白,一看就会知道process.nextTick()的强大。我在心里默念:“终于不用再调整setTimeout延迟参数了!”强大的process.nextTick()解决实际问题。这种理念会导致一些潜在的问题。我们看这段代码:letbar;//是异步的,但是同步调用了回调函数someAsyncApiCall(callback){callback();}//回调调用someAsyncApiCall(()=>{//因为someAsyncApiCall还没有完成,bar还没有被赋值console.log('bar',bar);//undefined});酒吧=1;用户定义了带有异步签名的someAsyncApiCall(),但它实际上是同步执行的。当调用someAsyncApiCall()时,在异步操作完成之前调用内部回调。回调试图获取bar的引用,但是scope中并没有这个变量,因为脚本还没有执行到bar=1这一步。有没有办法保证这个函数在赋值后被调用?通过将回调传递给process.nextTick(),脚本可以成功执行,并且可以访问所有变量、函数等,并且在调用回调之前已经初始化。它的优点是允许不允许的事件循环继续。对于用户在事件循环想要继续运行之前提醒错误很有用。这里是上面的代码通过process.nextTick()改进后的:1});酒吧=1;一个真实世界的例子:constserver=net.createServer(()=>{}).listen(8080);server.on('监听',()=>{});当我们传入一个端口号时,端口号会立即被绑定。因此可以立即调用“监听”回调。问题是.on('listening');这个回调可能还没有设置?我应该怎么办?为了准确监听listen动作,将'listening'事件的监听操作排队到nextTick(),让代码完整运行。这允许用户设置他们想要的任何事件。为什么要使用process.nextTick()?允许用户处理错误、清除不必要的资源或在事件循环之前再次尝试请求。有时确保回调在调用堆栈展开(unwound)之后,事件循环继续循环之前被调用。允许用户处理错误,清除不必要的资源,或者在事件循环之前再次尝试请求下面是一个符合用户期望的示例。constserver=net.createServer();server.on('connection',(conn)=>{});server.listen(8080);server.on('listening',()=>{});听()在event.loop开始时运行,但监听回调放在setImmediate()中。除非传入主机名,否则立即绑定端口。事件循环在处理时,必须处于轮询阶段,这意味着没有机会接收连接,允许连接事件在监听监听之前触发事件。有时确保在调用堆栈展开(unwound)之后,事件循环继续循环之前调用回调。让我们看另一个例子:运行一个继承EventEmitter的函数构造函数,它想在构造函数内部发出一个'event'事件。constEventEmitter=require('事件');constutil=require('util');函数MyEmitter(){EventEmitter.打电话(这个);这。发出('事件');实用程序。inherits(MyEmitter,EventEmitter);constmyEmitter=newMyEmitter();myEmitter.on('event',()=>{console.log('aneventoccurred!');//什么都没发生});无法理解在构造函数中发出事件,因为脚本不会运行到用户监听事件响应回调的位置。所以在构造器内部,可以使用process.nextTick设置一个回调,在构造器完成后发出事件,所以最终代码如下:constEventEmitter=require('events');constutil=require('util');functionMyEmitter(){EventEmitter.call(this);//分配处理程序后,使用process.nextTick()发出此事件process.nextTick(()=>{this.emit('event');});}util.inherits(MyEmitter,EventEmitter);constmyEmitter=newMyEmitter();myEmitter.on('event',()=>{console.log('aneventoccurred!');//aneventoccurred!'});回头再看mqtt.js接收消息使用的消息事件源码中的process.nextTick()process.nextTick()保证本次清空调用栈之后和之前的work函数是准确的下一个事件循环开始传输。writable._write=function(buf,enc,done){completeParse=doneparser.parse(buf)work()//startnextTick}functionwork(){varpacket=packets.shift()if(packet){那。_handlePacket(packet,nextTickWork)//注意这里}else{//终止process.nextTick()的递归vardone=completeParsecompleteParse=nullif(done)done()}}functionnextTickWork(){if(packets.length){process.nextTick(work)//注意这里}else{//停止process.nextTick()的递归vardone=completeParsecompleteParse=nulldone()}}通过学习process.nextTick()和理解源码,我们得到:流写入本地执行work(),如果收到有效数据包,则启动process.nextTick()递归。nextTick启动的条件:if(packet)/if(packets.length)即收到一个websocket包时启动。递归nextTick的过程:work()->nextTickWork()->process.nextTick(work)。nextTick结束条件:packet为空或packets为空,completeParse=null,done()结束递归。如果没有添加process.nextTick会发生什么?functionnextTickWork(){if(packets.length){work()//这里注意}}会导致当前的事件循环永不停止,一直处于阻塞状态,导致死循环。正是因为有process.nextTick(),我们才能保证work函数恰好在本次清除调用栈之后,下一次事件循环开始之前被调用。参考链接:https://nodejs.org/uk/docs/gu...https://nodejs.org/dist/lates...https://github.com/mqttjs/MQT...https://github.com/FrankKai/F...期待与您交流,共同进步:微信公众号:大前端/excellent_developers前端问答互助星球:t.zsxq.com/yBA2Biq力求成为优秀的前端工程师!