异步编程与回调
JavaScript本身是单线程编程。所谓单线程编程,就是一次只能完成一个任务。如果有多个任务,必须等待前一个任务完成后,才会继续执行下一个任务,因此,单线程编程的效率非常低。为了解决这个问题,Node.js中加入了异步编程模块。利用好Node.js异步编程,会给开发带来很大的便利。
1、回调函数
什么是回调呢?比如我们在编写JavaScript脚本时,不知道用户何时点击按钮,因此通常会为按钮的点击事件定义一个事件处理程序,该事件处理程序会接收一个函数,用来在点击事件被触发时调用,这就是所谓的回调。
因此,回调本质上是一个函数,它可以作为值传递给另一个函数,并且只有在特定事件发生时才会被执行。
Node.js异步编程的直接体现就是回调函数,Node.js中使用了大量的回调函数,Node.js中的大部分API都支持回调函数,如第7章中讲解过的操作文件的方法,基本上都同时提供了同步和异步操作的方法,并且默认使用的都是异步操作,在异步操作方法中都需要传递一个callback回调函数。
回调函数的简单应用:
js
function fooA() {
return 1
}
function fooB(a) {
return 2 + a
}
//fooA是一个函数,但这里作为一个参数在fooB函数中被调用
let c = fooB(fooA())
console.log(c)
3
异步调用回调函数。
js
let a = 0;
function fooA(x) {
console.log(x)
}
function timer(time) {
setTimeout(function () {
a = 6
}, time);
}
console.log(a);
timer(3000);
fooA(a);
0
0
程序并没有按照我们的设想执行,这是因为虽然timer函数中将变量a设置为6,但是程序执行时,由于timer函数中使用了setTimeout,其不会阻塞后面代码的执行,因此程序并不会等待timer函数执行完,而是直接执行了最后一行的fooA函数,而此时还没有经过3秒的时间,所以a的值仍是0。
如果想达到我们希望的效果,应该在timer函数中加入一个回调函数作为参数,然后调用时,将fooA函数作为参数传递给timer函数,即代码修改如下:
js
let a = 0
function fooA(x) {
console.log(x)
}
function timer(time, callback) {
setTimeout(function () {
a = 6
callback(a); //使用回调函数执行输出操作
}, time);
}
console.log(a)
timer(3000,fooA)
0
6
2、使用async/await的异步编程
上面讲解了Node.js中的回调函数,但回调只适用于简单的异步场景!当程序中有很多回调时,代码会变得非常复杂,而且调试也会很麻烦,因此在ES2015标准中新增了Promise特性,用来帮助处理异步代码而不涉及使用回调。在更高级的ES2017标准中,又新增了async/await语法,使得异步编程更加简单。
3.1、Promise基础
Promise是ES2015标准中提供的一种处理异步代码(而不会陷入回调地狱)的方式,它本质上是一个对象,使用new Promise()构造函数可以创建该对象,newPromise()构造函数中需要传入一个具有resolve和reject参数的函数,形式如下:
js
let p = new Promise(function(resolve, reject){
});
其中,resolve表示异步操作执行成功后的回调函数(其参数通常用data表示),reject表示异步操作执行失败后的回调函数(其参数通常用err表示)。Promise对象共有3种状态。
pending(进行中):Promise对象刚被创建时的状态,表示异步操作还未完成。fulfilled(已完成):表示异步操作已经完成,并返回了一个值。rejected(已拒绝):表示异步操作失败,返回一个错误信息。
js
function runAsync(){
let p = new Promise((resolve, reject)=>{
setTimeout(function(){
console.log('执行异步操作1')
resolve('promise1')
}, 1000)
})
return p;
}
runAsync();
运行上面代码后,会输出"执行异步操作1",但其中的resolve('promise1')并没有执行,它的作用是什么呢?
前面我们提到resolve是异步操作执行成功后要执行的回调函数,那么它如何执行呢?Promise对象提供了then方法,用来指定执行resolve回调。
例如,下面的代码使用上面创建的Promise对象,并在then方法中执行resolve回调:
js
runAsync().then(function(data){
console.log(data)
})
运行上面代码,会输出以下结果:
js
执行异步操作1
promise1
从上面的示例可以看出,then方法中的函数就类似于一个回调函数,但它能够在异步操作完成之后被执行,这就是Promise的好处,它能够将原来的回调函数分离出来,在异步操作执行完后,再去执行回调函数。
另外,使用Promise实现异步还有一个最大的特点:链式调用回调函数,即它可以在then方法中继续创建Promise对象并返回,然后继续调用then来进行回调操作。
例如,按照上面runAsync函数的方式再定义两个runAsync2和runAsync3函数,代码如下:
js
function runAsync2(){
let p = new Promise(function(resolve, reject){
setTimeout(function(){
console.log('执行异步操作2')
resolve('promise2')
}, 2000)
})
return p;
}
function runAsync3(){
let p = new Promise(function(resolve, reject){
setTimeout(function(){
console.log('执行异步操作3')
resolve('promise3')
}, 1000)
})
return p;
}
然后使用链式方式调用,代码如下:
js
runAsync()
.then(function(data){
console.log(data)
return runAsync2()
})
.then(function(data){
console.log(data)
return runAsync3()
})
.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回调传递值。代码如下:
js
function runAsync(){
flag=false
var p = new Promise(function(resolve, reject){
setTimeout(function(){
if(flag){
console.log('执行异步操作')
resolve('promise')
}
else
reject('执行异步操作失败')
}, 1000)
})
return p
}
然后在Promise对象的then方法中分别执行resolve回调和reject回调,代码如下:
js
runAsync()
.then(function(data){
console.log(data);
},
function(err){
console.log(err);
})
运行上面修改后的代码,由于flag变量为false,所以输出结果为:
js
执行异步操作失败
除了then方法,Promise对象还提供了一个catch方法,也可以执行reject回调,其使用方法与then类似。例如,上面代码可以修改如下:
js
runAsync()
.then(function(data){
console.log(data);
})
.catch(function(err){
console.log(err);
});
2.2、为什么使用async/await
ES2015中引入Promise主要是为了解决异步回调的问题,但是由于它自身语法的复杂性,在ES2017标准中引入了async/await。async/await减少了Promise的样板,并且减少了Promise链式调用的"不破坏链条"的限制,它使得代码看起来像是同步的,但它是异步的并且在后台无阻塞。因此,通过使用async/await实现异步编程是一种更好的方式。
2.3、async/await的使用
通过前面的讲解,我们知道ES2015标准下的异步函数会返回Promise,例如下面的代码:
js
const AsyncOper = () => {
return new Promise(resolve => {
setTimeout(() => resolve('执行操作'), 1000)
})
}
在使用async/await对上面代码进行异步回调时,只需要在声明的函数前面加上async关键字,并在要调用的函数名前面加上await即可。这里需要注意的是,客户端函数必须被定义为async。例如,下面代码中,要异步调用上面定义的AsyncOper函数,首先需要使用async关键字定义一个匿名的函数,然后在要调用的AsyncOper函数前面加上await关键字,代码如下:
js
const AsyncOper = () => {
return new Promise(resolve => {
setTimeout(() => resolve('执行操作'), 1000)
})
}
const useAsync = async () => {
console.log(await AsyncOper())
}
useAsync();
执行操作
Node.js中,在任何函数之前加上async关键字,就意味着该函数会返回Promise,即使代码中没有显式返回Promise,例如,下面两段代码是等效的:
js
//第1个函数
const Func1 = async () => {
return '测试'
}
Func1().then(alert) //使用alert弹出信息测试函数
//第2个函数
const Func2 = () => {
return Promise.resolve('测试')
}
Func2().then(alert) //使用alert弹出信息测试函数
使用async/await执行异步回调。
js
const fs = require('fs');
//定义异步函数,判断是否为文件夹
async function isDir(path) {
return new Promise((resolve, reject)=>{
fs.stat(path, (err, stats)=>{
if(err){
return;
}
if(stats.isDirectory()){
resolve(true);
} else {
resolve(false);
}
})
})
}
let path = 'demo';
let dirArr = [];
fs.readdir(path, async (err, data)=>{
if(err){
return;
}
//遍历
for(let i = 0; i < data.length; i++){
//异步调用idDir函数
if(await isDir(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 函数中使用)
基本用法:
js
async function foo() {
return 'hello';
}
等价于:
js
function foo() {
return Promise.resolve('hello');
}
js
async function getData() {
const res = await fetch('/api/data');
const data = await res.json();
console.log(data);
}
async/await写法:
js
async function getData() {
try {
const res = await fetch('/api/data');
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}
}
错误处理:
方式一:try / catch(推荐)
js
async function load() {
try {
const res = await fetch('/api/data');
const data = await res.json();
} catch (error) {
console.error('请求失败', error);
}
}
方式二:Promise.catch()
js
async function load() {
const res = await fetch('/api/data').catch(err => {
console.error(err);
});
}
并行VS串行:
错误示例(串行,慢)
js
const a = await fnA();
const b = await fnB();
正确并且写法:
js
const [a, b] = await Promise.all([fnA(), fnB()]);
async / await 的几个关键点:
await 后面不一定是 Promise:
js
const result = await 123;
console.log(result); // 123
//非 Promise 值会被自动包装为 Promise.resolve(value)
async 函数中 return 的值:
js
async function test() {
return 1;
}
test().then(v => console.log(v)); // 1
await 只能在 async 函数中使用:
js
//错误
function test() {
await fetchData(); // SyntaxError
}
//正确
async function test() {
await fetchData();
}
async / await 与事件循环的关系:
js
console.log('start');
async function asyncFn() {
console.log('async start');
await Promise.resolve();
console.log('async end');
}
asyncFn();
console.log('end');
start
async start
end
async end
原因:
- await后面的代码会被放入 微任务队列
- 当前同步代码执行完后才执行