同步代码和异步代码的执行顺序
要弄懂异步api要先搞懂它和同步api的区别,我们先从代码执行顺序说起。同步代码从上到下执行,前面的代码会阻塞后面代码的执行,比如我用for语句循环打印10个数然后再打印一条输出语句:
for (let i = 0; i < 100; i++) {
console.log(i)
}
console.log('end')
控制台输出:
可以看到直到数字顺序打印完毕最后一句输出语句才执行。
现在我们来看异步代码
console.log('代码开始执行...')
setTimeout(() => {
console.log('2秒之后执行的代码')
}, 2000)
setTimeout(() => {
console.log('0秒之后执行的代码')
}, 0)
console.log('代码执行完毕...')
控制台打印的结果:
0秒之后执行的代码并没有立即执行,因为异步API不会等待代码执行完成后再向下执行代码
在从上到下执行代码的过程中,并不是遇到什么执行什么,而是先将所有的同步API执行完再执行异步API。通过几张图来简单理解一下上面这段代码:
代码从上往下依次执行的过程中,它遇到第一句代码console.log,由于console是同步api,所以这段代码首先会在同步代码区中执行输出‘代码开始执行了’
接着它遇到了一个定时器,定时器属于异步api,那么它会把定时器代码放到异步代码执行区当中,异步代码的执行是交给宿主环境去执行(比如浏览器,nodejs),js的主线程继续执行同步代码区域的代码,宿主环境执行完异步代码紧接着它会把异步api对应的回调函数放在回调函数队列当中,注意:回调函数队列中的代码是没有执行的,正在等待主线程的调用
接下来代码继续往下走,又遇到一个0秒后执行的定时器,但不管你是0秒1秒还是2秒,只要遇到异步api,它就会把代码放到异步代码执行区中让宿主环境进行处理。
代码继续往下执行,遇到了console.log由于是同步api,所以这个输出会在同步代码执行区中去执行
到目前为止,同步代码已经执行完毕,宿主环境也执行完了定时器代码并把它的回调函数放到了回调函数队列中,现在js主线程才会在同步代码执行区中依次调用回调函数去执行,然后输出0秒后执行的代码
,2秒之后执行的代码
。
到此为止这段代码就执行完了。
同步api和异步api获得返回值
同步api可以拿到执行结果但是异步api是不可以的。看下面一段代码:
function getMsg () {
setTimeout(() => {
return fn()
})
}
function fn () {
return {msg:'hello'}
}
const msg = getMsg()
console.log('🚀 ~ file: 03_异步代码获取返回值.js ~ line 7 ~ msg', msg)
代码执行结果是undefined
回调函数
既然无法直接接受异步函数返回的值那如何拿到异步函数返回的结果呢?答案是回调函数,回调函数其实就是自己写的代码拿给另一个函数调用,也就是说将函数作为参数传给另一个函数,举个例子
function fn1 (callback) {
callback()
}
fn1(function () {
console.log('hello');
})
运行结果:
既然回调函数是个函数,那么自然也可以接受参数
function fn1 (callback) {
callback(123)
}
fn1(function (n) {
console.log(n);
})
运行结果:
如果fn1是异步函数的话,根据回调函数的执行顺序我们可以知道,系统会在fn1执行完毕后在回调函数队列中找到callback函数去执行,而callback的参数中正是异步函数的处理结果,因此可以拿到。我们来改造一下getMsg函数获得返回的值
function getMsg(callback){
setTimeout(() => {
callback({msg:'hello'})
},2000)
}
getMsg(function (msg) {
console.log(msg);
})
控台台输出:
我们现在已经拿到了回调函数的执行结果,但是注意在getMsg函数的内部我们不能通过return的方式拿到返回值,因为异步api不会阻塞
代码的执行,我们在setTimeout
后面虽然什么也没写,但是函数会默认返回undefined
.相当于:
function getMsg(callback){
setTimeout(() => {
callback({msg:'hello'})
}, 2000)
return undefined
}
getMsg(function (msg) {
console.log(msg);
})
为什么要使用Promise
由于异步api不会阻塞后续代码的执行,因此我们在执行后面代码的时候可能异步api还没有拿到结果,那么问题来了,如果后面的代码执行依赖于前面异步api的执行结果呢?比如我要读取一个文件然后输出读取的结果
const fs = require('fs')
fs.readFile('a.txt', (err, result) => { })
console.log('读取文件的结果');
那么我们会想,你直接把console写到回调函数里不就能操作异步的执行结果了吗,确实可以,但是某些情况下会出现一些问题,比如我要依次读取三个文a.txt,b.txt,c.txt。那么我要在a文件的回调函数里面读取b文件,然后在b文件的回调函数里读取c文件,这样会导致回调函数的嵌套层次过多,这些代码时难以维护的,接下来我们演示一下这些代码会长什么样子
const fs = require('fs')
fs.readFile('./a.txt', 'utf8', (err, result1) => {
console.log(result1)
fs.readFile('./b.txt', 'utf8', (err, result2) => {
console.log(result2)
fs.readFile('./c.txt', 'utf8', (err, result3) => {
console.log(result3)
})
})
})
运行结果:
可以看到确实是依次读取的,现在我们只是嵌套了3层,如果我们有10层,20层那么代码左边显然是一个大于号的形式了,你写完之后可能自己都不想看这个代码了,这种回调了之后再回调,一层一层的嵌套这样的代码我们把它形象的比喻成“回调地狱”。
为了解决这个问题,在es6中就为我们提供了Promise,它可以为我们解决回调地狱问题。
注意Promise并没有提供什么新的功能,它只是异步编程语法上的改进,可以让我们将异步api的执行和结果进行分离
来看它的基础语法
let p = new Promise((resolve, reject) => {
})
promise本身是一个构造函数,要使用new构造符创建它的实例对象,它的参数是一个匿名函数,在这个匿名函数当中promise是想让你把原本的异步api代码放到这个匿名函数当中,比如说一个定时器
let p = new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
} else {
}
},2000)
})
那么当这个定时器执行完毕后它会有一个结果出来,promise它不希望你在函数的内部去处理结果,他希望你拿到外面去处理,那么怎么达到这个目的呢?
在匿名函数的参数中resolve和reject其实是两个函数,当异步api有返回结果的时候你可以去调用这个函数,并且把异步api的执行结果通过参数的形式传递给它。
let p = new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
resolve({name:'zs'})
} else {
}
},2000)
})
当异步函数执行失败的时候可以调用reject函数,把失败的结果传递给它
let p = new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
resolve({name:'zs'})
} else {
reject('error occured')
}
},2000)
})
传递处理结果之后如何在外面拿到它呢?promose的实例对象上有一个then方法,在promise里面当异步函数执行完成后调用了resolve函数,调用它其实是在执行then方法里面的回调函数
let p = new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
resolve({name:'zs'})
} else {
reject('error occured')
}
},2000)
})
p.then((result) => {console.log(res)})
同理,我们可以通过.catch
拿到reject里面的失败信息
p.then((result) => {
console.log(result)
}).catch((err) => {
console.log(err)
})
现在,我们用promise来解决依次读取三个文件的问题
首先创建3个promise对象
const fs = require('fs')
let p1 = new Promise((resolve, reject) => {
fs.readFile('./a.txt', 'utf-8', (err, res) => {
if (!err) {
resolve(res)
} else {
reject(err)
}
})
})
let p2 = new Promise((resolve, reject) => {
fs.readFile('./b.txt', 'utf-8', (err, res) => {
if (!err) {
resolve(res)
} else {
reject(err)
}
})
})
let p3 = new Promise((resolve, reject) => {
fs.readFile('./c.txt', 'utf-8', (err, res) => {
if (!err) {
resolve(res)
} else {
reject(err)
}
})
})
但是你直接new 三个promise出来也是不行的,因为你只要new一个promise对象,它里面的异步api就会立刻执行,你需要保证3个promise的执行顺序,那么我们就需要建立三个函数,把3个promise分别放到3个函数里,你需要哪个异步api先执行就先调用哪一个函数
const fs = require('fs')
function p1() {
return new Promise((resolve, reject) => {
fs.readFile('./a.txt', 'utf-8', (err, res) => {
if (!err) {
resolve(res)
} else {
reject(err)
}
})
})
}
function p2() {
return new Promise((resolve, reject) => {
fs.readFile('./b.txt', 'utf-8', (err, res) => {
if (!err) {
resolve(res)
} else {
reject(err)
}
})
})
}
function p3() {
return new Promise((resolve, reject) => {
fs.readFile('./c.txt', 'utf-8', (err, res) => {
if (!err) {
resolve(res)
} else {
reject(err)
}
})
})
}
p1()
.then((r1) => {
console.log(r1)
return p2()
})
.then((r2) => {
console.log(r2)
return p3()
})
.then((r3) => {
console.log(r3)
})
终极解决方案:async方法
虽然promise已经解决了回调地狱的问题,但在异步函数外包裹一层promise然后又在里面手动调用resolve,reject,并在外面使用then和catch链式编程还是显得有很麻烦,在es6中新增的异步函数语法就可以完美解决这个问题,实际上这个异步函数就是基于promise对象的基础上进行了一层封装,把那些看起来臃肿的代码封装起来,然后开放一些关键字供我们使用。他可以让我们将异步代码写成同步代码的形式,使代码变得清晰明了。
只需要在普通函数前面加上async
关键字,普通函数就会变成异步函数,其次,异步函数的默认返回值是promise对象
async function fn1() {
}
console.log(fn1())
控制台:
可以看到返回的是一个promise对象,如果在fn1里面返回一个数据
async function fn1 () {
return 123
}
console.log(fn1())
再次打印:
我们知道promise对象可以调用then
和catch
方法
async function fn1() {
return 123
}
fn1().then((res) => {
console.log(res)
})
错误信息可以通过throw
关键字在异步函数里拿到,throw
一旦执行后面的代码便不再执行,在外部通过catch
去拿到错误
async function fn1() {
throw 'error occured'
return 123
}
fn1()
.then((res) => {
console.log(res)
})
.catch((err) => {
console.log(err)
})
await 关键字
await
关键字的特点:
1、只能在async
修饰的方法中使用
2、它能够暂停异步函数的执行,等待promise返回结果后再向下执行
async function p1() {
return 'p1'
}
async function p2() {
return 'p2'
}
async function p3() {
return 'p3'
}
async function run() {
let r1 = await p1()
let r2 = await p2()
let r3 = await p3()
console.log(r1)
console.log(r2)
console.log(r3)
}
run()
控制台:
注意:await后面必须要跟一个promise对象才能拿到返回的值
现在我们来改造一下依次读取三个文件的案例
由于fs.readFile
方法没有返回promise对象,它是通过回调函数拿到值,在nodejs中提供了一个promisify
的方法对现有函数进行包装,让我们可以用异步函数的方法操作该api
const fs = require('fs')
const promisify = require('util').promisify
const readFile = promisify(fs.readFile)
async function read () {
let r1 = await readFile('./a.txt','utf-8')
let r2 = await readFile('./b.txt','utf-8')
let r3 = await readFile('./c.txt', 'utf-8')
console.log(r1)
console.log(r2)
console.log(r3)
}
read()
控制台: