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

深入理解nodejs中的异步编程

时间:2023-04-03 17:35:29 Node.js

介绍因为javascript默认是单线程的,这意味着代码不能创建新的线程并行执行。但是对于原本运行在浏览器中的javascript来说,单线程的同步执行环境显然不能满足页面点击、鼠标移动等响应用户的功能。所以浏览器实现了一组API,可以让javascript以回调的形式异步响应页面请求事件。更进一步,nodejs引入了非阻塞I/O,从而将异步的概念扩展到文件访问、网络调用等方面。今天,我们将深入探讨各种异步编程的优缺点和发展趋势。同步异步和阻塞非阻塞在讨论nodejs的异步编程之前,先讨论一个比较容易混淆的概念,即同步、异步、阻塞和非阻塞。所谓阻塞和非阻塞是指进程或线程在操作或读写数据时是否需要等待,等待过程中是否可以进行其他操作。如果需要等待,而线程或进程在等待过程中无法进行其他操作,只能傻等,那么我们就说这个操作被阻塞了。反之,如果进程或线程在操作或数据读写的过程中可以进行其他操作,那么我们就说这个操作是非阻塞的。同步和异步是指访问数据的方式。同步是指需要主动读取数据。此读取过程可能是阻塞的或非阻塞的。异步就是不需要主动读取数据,是被动通知。很显然,javascript中的回调是一种被动通知,我们可以称之为异步调用。javascript中的回调javascript中的回调是一个非常典型的异步编程的例子:document.getElementById('button').addEventListener('click',()=>{console.log('buttonclicked!');})中在上面的代码中,我们为按钮添加了一个点击事件监听器。如果检测到点击事件,就会触发回调函数,并输出相应的信息。回调函数是一个普通的函数,只是它作为参数传递给addEventListener,只有在事件触发时才会被调用。上一篇我们讲的setTimeout和setInterval其实就是异步回调函数。回调函数的错误处理如何在nodejs中处理回调错误信息?nodejs采用了一种非常巧妙的方法。在nodejs中,任何回调函数的第一个参数都是错误对象。我们可以通过判断错误对象是否存在来处理相应的错误。fs.readFile('/file.json',(err,data)=>{if(err!==null){//处理错误console.log(err)return}//没有错误,然后处理数据。控制台.log(data)})callbackhelljavascript回调很不错,有效的解决了同步处理的问题。但不幸的是,如果我们下一步需要依赖回调函数的返回值,就会陷入这个回调地狱。调用回调地狱有点夸张,但也从一个方面反映了回调函数的问题。fs.readFile('/a.json',(err,data)=>{if(err!==null){fs.readFile('/b.json',(err,data)=>{//回调内部回调})}})如何解决?不要怕ES6引入了Promise,ES2017引入了Async/Await来解决这个问题。ES6中的Promise什么是PromisePromise是一种针对异步编程的解决方案,比传统的“回调函数和事件”方案更加合理和强大。所谓Promise,简单来说就是一个容器,里面装的是将来会结束的事件(通常是异步操作)的结果。从语法上讲,Promise是一个对象,可以从中获取异步操作的消息。Promise的特点Promise有两个特点:对象的状态不受外界影响。Promise对象表示一个异步操作,有三种状态:Pending(进行中)、Resolved(完成,也称为Fulfilled)和Rejected(失败)。只有异步操作的结果才能确定当前处于哪个状态,其他任何操作都不能改变这个状态。状态一旦改变,就不会再改变,随时都可以得到这个结果。改变Promise对象的状态只有两种可能性:从Pending到Resolved和从Pending到Rejected。这与事件(Event)完全不同。事件的特点是错过了再听,就得不到结果。Promise的优点Promise在同步操作的过程中表达了异步操作,避免了层层嵌套的回调函数。Promise对象提供统一的接口,可以更轻松地控制异步操作??。Promise的缺点是Promise不能被取消。一旦创建,将立即执行,不能中途取消。如果没有设置回调函数,Promise内部抛出的错误将不会反映到外部。当处于Pending状态时,无法得知当前正在进行到哪个阶段(刚刚开始还是即将完成)。Promise的用法Promise对象是用于生成Promise实例的构造函数:varpromise=newPromise(function(resolve,reject){//...一些代码if(/*异步操作成功*/){resolve(value);}else{拒绝(错误);}});promise可以连接thenoperation,thenoperation可以连接两个函数参数,第一个函数参数是构建Promise时resolve的值,第二个函数参数是拒绝构建Promise的error。promise.then(function(value){//成功},function(error){//失败});我们看一个具体的例子:functiontimeout(ms){returnnewPromise(((resolve,reject)=>{setTimeout(resolve,ms,'done');}))}timeout(100).then(value=>控制台日志(值));Promise中调用了一个setTimeout方法,会定时触发resolve方法,并传入参数done。最后,程序输出完成。PromiseExecutionOrder一旦一个Promise被创建,它就会被立即执行。但是Promise.then中的方法会在一个调用周期后再次被调用。让我们看下面的例子:日志('第3步');});console.log('Step2');output:Step1Step2Step3async和awaitPromise当然没问题,我们把回调地狱变成了链式调用。我们使用then来连接多个Promise,上一个promise的解析结果就是下一个promise中then的参数。链式调用的缺点是什么?比如我们从一个promise中解析出一个值,我们需要根据这个值进行一些业务逻辑处理。如果业务逻辑很长,我们需要在接下来的then中写一段很长的业务逻辑代码。这使得我们的代码看起来非常冗余。那么有没有办法直接返回resolveinpromise的结果呢?答案是等待。当await以promise为前缀时,调用代码将停止,直到promise被解决或拒绝。注意await必须放在async函数中。我们来看一个async和await的例子:constlogAsync=()=>{returnnewPromise(resolve=>{setTimeout(()=>resolve('小马'),5000)})}上面我们定义了一个logAsync函数,它返回一个Promise,因为Promise内部使用setTimeout来解析,所以我们可以认为它是异步的。如果我们使用await来获取resolve的值,我们需要把它放在一个异步函数中:constdoSomething=async()=>{constresolveValue=awaitlogAsync();console.log(resolveValue);}异步执行顺序await其实就是等待promise的resolve结果。我们结合上面的例子:constlogAsync=()=>{returnnewPromise(resolve=>{setTimeout(()=>resolve('小马'),1000)})}constdoSomething=async()=>{constresolveValue=awaitlogAsync();console.log(resolveValue);}console.log('before')doSomething();console.log('after')上面例子输出:beforeafter小马可以看到aysnc是异步执行的,它的顺序是在当前的之后循环。asyncasync的特性会让所有后续函数都变成Promise,即使后续函数没有显式返回Promise。constasyncReturn=async()=>{return'asyncreturn'}asyncReturn().then(console.log)因为then后面只能接Promise,所以可以看出async把一个普通的函数封装成了一个Promise:constasyncReturn=async()=>{returnPromise.resolve('asyncreturn')}asyncReturn().then(console.log)总结promise避免了回调地狱,将callback内部的callback重写为then形式的链式调用。但是链式调用不方便阅读和调试。于是async和await出现了。async和await将链式调用变成了类似于程序顺序执行的语法,这样更容易理解和调试。我们来看一个对比,先看Promise的使用:constgetUserInfo=()=>{returnfetch('/users.json')//获取users的列表.then(response=>response.json())//解析JSON.then(users=>users[0])//选择第一个user.then(user=>fetch(`/users/${user.name}`))//获取用户数据.then(userResponse=>userResponse.json())//解析JSON}getUserInfo()将其重写为异步并等待:constgetUserInfo=async()=>{constresponse=awaitfetch('/users.json')//get用户列表constusers=awaitresponse.json()//解析JSONconstuser=users[0]//选择第一个用户constuserResponse=awaitfetch(`/users/${user.name}`)//获取用户dataconstuserData=awaituserResponse.json()//解析JSONreturnuserData}getUserInfo()可以看到业务逻辑变得更加清晰了。同时我们得到了很多中间值,也方便我们调试。本文作者:flydean程序那些事本文链接:http://www.flydean.com/nodejs-async/本文来源:flydean的博客欢迎关注我的公众号:《程序那些事儿》最通俗的解读,最深刻的干货,最简洁的教程,还有很多你不知道的小技巧等你来发现!