Node.js——异步编程与回调

张开发
2026/4/6 13:23:44 15 分钟阅读

分享文章

Node.js——异步编程与回调
异步编程与回调1、回调函数2、使用async/await的异步编程3.1、Promise基础2.2、为什么使用async/await2.3、async/await的使用2.4、使用async/await异步编程的优点3、示例JavaScript本身是单线程编程。所谓单线程编程就是一次只能完成一个任务。如果有多个任务必须等待前一个任务完成后才会继续执行下一个任务因此单线程编程的效率非常低。为了解决这个问题Node.js中加入了异步编程模块。利用好Node.js异步编程会给开发带来很大的便利。1、回调函数什么是回调呢比如我们在编写JavaScript脚本时不知道用户何时点击按钮因此通常会为按钮的点击事件定义一个事件处理程序该事件处理程序会接收一个函数用来在点击事件被触发时调用这就是所谓的回调。因此回调本质上是一个函数它可以作为值传递给另一个函数并且只有在特定事件发生时才会被执行。Node.js异步编程的直接体现就是回调函数Node.js中使用了大量的回调函数Node.js中的大部分API都支持回调函数如第7章中讲解过的操作文件的方法基本上都同时提供了同步和异步操作的方法并且默认使用的都是异步操作在异步操作方法中都需要传递一个callback回调函数。回调函数的简单应用functionfooA(){return1}functionfooB(a){return2a}//fooA是一个函数但这里作为一个参数在fooB函数中被调用letcfooB(fooA())console.log(c)3异步调用回调函数。leta0;functionfooA(x){console.log(x)}functiontimer(time){setTimeout(function(){a6},time);}console.log(a);timer(3000);fooA(a);0 0程序并没有按照我们的设想执行这是因为虽然timer函数中将变量a设置为6但是程序执行时由于timer函数中使用了setTimeout其不会阻塞后面代码的执行因此程序并不会等待timer函数执行完而是直接执行了最后一行的fooA函数而此时还没有经过3秒的时间所以a的值仍是0。如果想达到我们希望的效果应该在timer函数中加入一个回调函数作为参数然后调用时将fooA函数作为参数传递给timer函数即代码修改如下leta0functionfooA(x){console.log(x)}functiontimer(time,callback){setTimeout(function(){a6callback(a);//使用回调函数执行输出操作},time);}console.log(a)timer(3000,fooA)0 62、使用async/await的异步编程上面讲解了Node.js中的回调函数但回调只适用于简单的异步场景当程序中有很多回调时代码会变得非常复杂而且调试也会很麻烦因此在ES2015标准中新增了Promise特性用来帮助处理异步代码而不涉及使用回调。在更高级的ES2017标准中又新增了async/await语法使得异步编程更加简单。3.1、Promise基础Promise是ES2015标准中提供的一种处理异步代码而不会陷入回调地狱的方式它本质上是一个对象使用new Promise()构造函数可以创建该对象newPromise()构造函数中需要传入一个具有resolve和reject参数的函数形式如下letpnewPromise(function(resolve,reject){});其中resolve表示异步操作执行成功后的回调函数其参数通常用data表示​reject表示异步操作执行失败后的回调函数其参数通常用err表示​。Promise对象共有3种状态。pending进行中​Promise对象刚被创建时的状态表示异步操作还未完成。fulfilled已完成​表示异步操作已经完成并返回了一个值。rejected已拒绝​表示异步操作失败返回一个错误信息。functionrunAsync(){letpnewPromise((resolve,reject){setTimeout(function(){console.log(执行异步操作1)resolve(promise1)},1000)})returnp;}runAsync();运行上面代码后会输出“执行异步操作1”​但其中的resolve(‘promise1’)并没有执行它的作用是什么呢前面我们提到resolve是异步操作执行成功后要执行的回调函数那么它如何执行呢Promise对象提供了then方法用来指定执行resolve回调。例如下面的代码使用上面创建的Promise对象并在then方法中执行resolve回调runAsync().then(function(data){console.log(data)})运行上面代码会输出以下结果执行异步操作1promise1从上面的示例可以看出then方法中的函数就类似于一个回调函数但它能够在异步操作完成之后被执行这就是Promise的好处它能够将原来的回调函数分离出来在异步操作执行完后再去执行回调函数。另外使用Promise实现异步还有一个最大的特点链式调用回调函数即它可以在then方法中继续创建Promise对象并返回然后继续调用then来进行回调操作。例如按照上面runAsync函数的方式再定义两个runAsync2和runAsync3函数代码如下functionrunAsync2(){letpnewPromise(function(resolve,reject){setTimeout(function(){console.log(执行异步操作2)resolve(promise2)},2000)})returnp;}functionrunAsync3(){letpnewPromise(function(resolve,reject){setTimeout(function(){console.log(执行异步操作3)resolve(promise3)},1000)})returnp;}然后使用链式方式调用代码如下runAsync().then(function(data){console.log(data)returnrunAsync2()}).then(function(data){console.log(data)returnrunAsync3()}).then(function(data){console.log(data)})运行上面代码结果如下执行异步操作1 promise1 执行异步操作2 promise2 执行异步操作3 promise3上面我们讲解了使用then方法可以执行resolve回调那么reject回调如何执行呢reject的作用是把Promise的状态设置为rejected我们同样可以在then方法中执行。例如修改上面定义的runAsync函数其中定义一个flag变量默认为false判断flag为true时使用resolve回调传递值否则使用reject回调传递值。代码如下functionrunAsync(){flagfalsevarpnewPromise(function(resolve,reject){setTimeout(function(){if(flag){console.log(执行异步操作)resolve(promise)}elsereject(执行异步操作失败)},1000)})returnp}然后在Promise对象的then方法中分别执行resolve回调和reject回调代码如下runAsync().then(function(data){console.log(data);},function(err){console.log(err);})运行上面修改后的代码由于flag变量为false所以输出结果为执行异步操作失败除了then方法Promise对象还提供了一个catch方法也可以执行reject回调其使用方法与then类似。例如上面代码可以修改如下runAsync().then(function(data){console.log(data);}).catch(function(err){console.log(err);});2.2、为什么使用async/awaitES2015中引入Promise主要是为了解决异步回调的问题但是由于它自身语法的复杂性在ES2017标准中引入了async/await。async/await减少了Promise的样板并且减少了Promise链式调用的“不破坏链条”的限制它使得代码看起来像是同步的但它是异步的并且在后台无阻塞。因此通过使用async/await实现异步编程是一种更好的方式。2.3、async/await的使用通过前面的讲解我们知道ES2015标准下的异步函数会返回Promise例如下面的代码constAsyncOper(){returnnewPromise(resolve{setTimeout(()resolve(执行操作),1000)})}在使用async/await对上面代码进行异步回调时只需要在声明的函数前面加上async关键字并在要调用的函数名前面加上await即可。这里需要注意的是客户端函数必须被定义为async。例如下面代码中要异步调用上面定义的AsyncOper函数首先需要使用async关键字定义一个匿名的函数然后在要调用的AsyncOper函数前面加上await关键字代码如下constAsyncOper(){returnnewPromise(resolve{setTimeout(()resolve(执行操作),1000)})}constuseAsyncasync(){console.log(awaitAsyncOper())}useAsync();执行操作Node.js中在任何函数之前加上async关键字就意味着该函数会返回Promise即使代码中没有显式返回Promise例如下面两段代码是等效的//第1个函数constFunc1async(){return测试}Func1().then(alert)//使用alert弹出信息测试函数//第2个函数constFunc2(){returnPromise.resolve(测试)}Func2().then(alert)//使用alert弹出信息测试函数使用async/await执行异步回调。constfsrequire(fs);//定义异步函数判断是否为文件夹asyncfunctionisDir(path){returnnewPromise((resolve,reject){fs.stat(path,(err,stats){if(err){return;}if(stats.isDirectory()){resolve(true);}else{resolve(false);}})})}letpathdemo;letdirArr[];fs.readdir(path,async(err,data){if(err){return;}//遍历for(leti0;idata.length;i){//异步调用idDir函数if(awaitisDir(path/data[i])){dirArr.push(data[i]);}}console.log(dirArr);})2.4、使用async/await异步编程的优点Promise的出现解决了传统回调函数导致的“地狱回调”问题但它的语法导致其发展成一个回调链遇到复杂的业务场景时这样的语法是不美观的async/await代码看起来更加简洁使得异步代码看起来像同步代码而await的本质其实就是可以提供等同于同步效果的等待异步返回能力的语法糖只有这一句代码执行完才会执行下一句。被async修改的函数会默认返回一个Promise对象的resolve值因此对async函数可以直接使用then方法返回值就是then方法传入的函数。async/await是基于Promise实现的可以说是改良版的Promise它不能用于普通的回调函数。async/await与Promise一样是非阻塞的。3、示例async/ await是基于 Promise 的语法糖用来让异步代码写起来像同步代码提高可读性和可维护性。async→ 声明一个异步函数await→ 等待一个 Promise 完成只能在 async 函数中使用基本用法asyncfunctionfoo(){returnhello;}等价于functionfoo(){returnPromise.resolve(hello);}asyncfunctiongetData(){constresawaitfetch(/api/data);constdataawaitres.json();console.log(data);}async/await写法asyncfunctiongetData(){try{constresawaitfetch(/api/data);constdataawaitres.json();console.log(data);}catch(err){console.error(err);}}错误处理方式一try / catch推荐asyncfunctionload(){try{constresawaitfetch(/api/data);constdataawaitres.json();}catch(error){console.error(请求失败,error);}}方式二Promise.catch()asyncfunctionload(){constresawaitfetch(/api/data).catch(err{console.error(err);});}并行VS串行错误示例串行慢constaawaitfnA();constbawaitfnB();正确并且写法const[a,b]awaitPromise.all([fnA(),fnB()]);async / await 的几个关键点await 后面不一定是 Promiseconstresultawait123;console.log(result);// 123//非 Promise 值会被自动包装为 Promise.resolve(value)async 函数中 return 的值:asyncfunctiontest(){return1;}test().then(vconsole.log(v));// 1await 只能在 async 函数中使用://错误functiontest(){awaitfetchData();// SyntaxError}//正确asyncfunctiontest(){awaitfetchData();}async / await 与事件循环的关系console.log(start);asyncfunctionasyncFn(){console.log(async start);awaitPromise.resolve();console.log(async end);}asyncFn();console.log(end);start async start end async end原因await后面的代码会被放入 微任务队列当前同步代码执行完后才执行

更多文章