事实上async function只不过是对Promise一个很好的封装,从es6到es7,而async异步方法确实实现起来 也可以让代码变得很优雅,下面就由浅到深具体说说其中的原理。
长篇预警
promise是es6中实现的一个对象,它接收一个函数作为参数。这个函数又有两个参数,分别是 resolve和reject。
const a = new Promise(function(resolve, reject){
console.log(1)
resolve(3)
reject(5)
console.log(4)
})
console.log('outter')
console.log(a)
复制代码
结果如下:
这里先提一个关键resolve与reject两个方法事实上对应着两个出口,是为了传递我们在方法中的参数(这里对应3和5)。不太了解也没事,下面会一直用到我这里说的概念。然后我们先不看resolve与reject两个方法,先看1和4的顺序,也就是说虽然
console.log(4)
在最后一行,但是依然比resolve先执行。尽管如此,写代码时还是应该要注意。(再看看上面,出口自然应该放在逻辑的最后一步)。
回到正题,从上面代码的执行可以发现promise对象的生成过程本身是不会阻塞正常代码的执行的。并且 promise内部的所有代码都会按照顺序执行,最后再执行你的出口方法(resolve和reject)。
这两个出口方法分别对应着将你的promise对象转化为两种状态。promise对象本身具有三种状态。执行状态 (pending),成功状态(resolved),失败状态(rejected)。(看看上面的关键,既然是出口,那么自然只会执行第一个出口了)。也就是说如果先碰到reject,执行了reject,那么就不会执行resolve,反之亦然。 也就是说Promise对象可以从pending状态 变化为resolved状态或者是rejected状态,但是resolved状态和rejected状态之间可没办法相互转化(已经从出口出去了)。有的小伙伴可能这时候就想看看了pending状态了,因为rejected状态很方便嘛,我只要将你的resolve和reject两行代码互换自然就可以看到了,那么pending状态怎么看呢?
const a = new Promise(function(resolve, reject){
console.log(1)
setTimeout( () => {
resolve('inner')
})
})
console.log('outter')
console.log(a)
复制代码
很简单,我们这里将出口方法放在了time任务队列中,(具体js单线程方面参考阮一峰老师博客就好,这里不再赘述)。那么这时候代码按照顺序执行下来,a还没从出口出来,自然是pending状态了。
有的小伙伴这时候可能着急了,你BB了这么久,没看出哪里异步了啊, 这不都是顺序执行的?唯一一个异步 还是靠的原来的setTimeout,坑人呢。别急,下面开始才是异步方案,上面只是必要的铺垫。
对于任意一个已经成为resolved状态或者是rejected状态的promise对象(这里就是a)。我们都可以用then方法来接收。而这个then方法,它就是异步的。
const a = new Promise(function(resolve, reject){
console.log(1) //1
resolve('inner')
})
console.log('outter') //2
a.then(v => {
console.log(v) //4
})
console.log(a) //3
复制代码
行末的备注是为了方便小伙伴们能够看清执行的顺序。首先,结合代码我们可以发现所有的then方法,都是异步的,而这个then方法中的参数v,就是我们resolve出口方法中传递的参数(记得之前的提一个关键吗)。有心的人就有问题了,那么这时候执行了then以后,你都没返回,那么我看人家好多then链式调用,是不是你漏写了啊。不BB,上代码。
const a = new Promise(function(resolve, reject){
console.log(1)
resolve('inner')
})
const b = a.then(v => {
console.log(v)
})
setTimeout( () => {
console.log(b)
})
复制代码
首先说明一下,所有的setTimeout(包括setInterval)都默认至少有一个4ms,就算你不写。并且setTimeout是浏览器提供的另一个线程来实现,而promise则是作为es6的规范。(如果用node就好解释了,我更倾向于认为Promise是类似于nexttick之类的接口。浏览器环境下的js并不像node具有多个队列,只有一个主线程运行队列,Promise一定会在当前主线程队列运行完毕的最后一个)。不了解的node也无所谓,这里只需要记住promise一定比setTimeout快!setTimeout有4ms呢!言归正传,这里就可以发现了,实质上就算我们不返回,它也会帮我们返回一个promise对象,但是因为我们没有在出口方法中传递参数,因此这里的参数就是undefined。那么如果需要参数的话,也很简单,手动返回一个就是了。(其实这有点类似于es6中的class的constructor,接下来我就找时间写个关于这个的文章。)这里先不提其他,接着看代码
const a = new Promise(function(resolve, reject){
resolve('inner')
})
const b = a.then(v => {
return new Promise((resolve, reject) => {
resolve(v)
})
})
setTimeout( () => {
console.log(b)
},1000)
复制代码
看,我们只要手动返回一个promise对象,再传递参数就好了。但是到这里事实上可能有的小伙伴已经想报警了。这个b是异步执行的,然后这个b里面的a中的返回的promise是同步的,然后再执行then的话又是这个异步的同步中的异步。再看下面的代码。
a.then(v => {
return new Promise((resolve, reject) => {
resolve(v)
})
}).then(v => {
}).then(v =>{
})
复制代码
哎哟,then一多,好丑啊。代码一点也不优雅,是的。这确实是个问题,这才引出了async的解决方案,但还不到谈那个的时间。让我们先把promise说完。
可能有小伙伴发现了,reject你一直都没说呢?是的,先说完resolve再说这个,其实我个人理解rejcet为抛出一个异常,我们可以在then中去处理,但是我们也可以在catch中处理(我推荐这种,至于为什么,我把两种写法列出来你就明白了)。
现在假设有一个业务逻辑,需要判断之后我们再决定走哪个出口。下面第一种是用then的
const a = new Promise(function(resolve, reject){
if(0) {
resolve('成功了')
} else {
reject('错误了')
}
})
a.then( v => {
console.log(v)
return new Promise((resolve, reject) => {
resolve(v)
})
}, e => {
console.log(e)
})
复制代码
是不是丑的一比?then多了以后叫人怎么看啊,一堆链式还夹杂一个逗号我去。我们再看看用catch的
a.then( v => {
console.log(v)
return new Promise((resolve, reject) => {
resolve(v)
})
}).catch(e => {
console.log(e)
})
复制代码
结果图如上,我就不贴了。是不是很优雅?(额。。。。单纯指的是相对then来说。)
总而言之,resolve,reject。对应两种状态,两种出口,出口中传递参数。出口之前都为同步。出口之后,then,catch都是异步,并且我们可以在then和catch中接收之前同步的传出来的参数。并且要注意的是resolve状态和reject两种出口我们要用不同的方式来接收。一种我认为是成功,一种是异常,异常必须要去捕获。
说到这里其实promise也差不多了,再提两个方法,一个是Promise.race,一个是Promise.all。注意了,这两个都是类方法。Promise.race方法是将多个 Promise 实例,包装成一个新的 Promise 实例。
const result = Promise.race([a, b, c]);
复制代码
a,b,c都是promise的实例,这三个实例哪一个先结束,就先返回一个。result就变成哪一个。举个场景就明白了。现在我们需要一张图片,这个图片异步加载,但是它是哪张我不关心(只要是给定的三张中的一张),我定了三个异步任务,先返回的那张我放到html上。嗯,就这么简单。
Promise.all。他必须要接收的promise实例全部变为resolve才返回(返回这些promise实例中resolve中的参数组成的数组),有一个变成reject,它就返回这个reject的参数。直接举例子。我们需要异步加载三张图片,但是我必须要三张全部加载完我一起显示,我不要一张一张的出来。三张都出来就是resolve,任意一张失败了不好意思我就都不给你显示。
剩下的还有一些promise方法我就不多说了,用的也不多,直接看文档就好了。
重头戏来了,async function!
实际上,async function的使用方法跟普通函数一模一样,如果你在async function中没有使用await关键字 的话,从某种程度上来说它就是普通函数。。。。先来个代码压压惊。
async function test() {
console.log(1)
const a = await new Promise(function(resolve, reject){
resolve(3)
})
console.log(a)
}
test()
console.log(2)
复制代码
注意上面代码中数字就暗示了执行的先后顺序!好了先别在意这段代码,只是让你看看。先往下看。下面我会再次由浅入深谈谈原理。。。
还是一样,代码说话
async function test() {
console.log(1)
const a = await new Promise(function(resolve, reject){
resolve(3)
})
console.log('我是被处理后的:', a)
}
const b = test()
console.log('我是还没被处理后的:', b)
复制代码
实质上,
async function只是把我们这一整个函数用promise包装了一下。(还记得之前说的promise 没走到出口前都是同步的吗?)所以这里我们的1会先输出,但是await这个关键字正是奇妙之处。它会阻塞之后的代码执行,我们之前比如说
resolve(3)
,然后我们在then中接收这个参数,但是现在await直接就帮我们处理了这个参数,也就是await会处理这个promise对象,再返回里面的参数。而处理之前,我们主线程任务必须先运行完。因此这里我们打印的b的结果正是一个处于pending状态的promise对象。其次,async function只要一到await,那么函数这时候就等同于异步了, (见上面代码,再看看这段话的开头)。总而言之就是当我们运行到await的时候,在这个async function内部在await之后的所有代码都会 等待await处理完毕结果之后才会执行,而当await开始处理结果,不好意思,这就不属于同步的范围了。(
是不是异步极其优雅的实现方法!!!)
有的同学就问了,那么await只能处理promise对象吗,不是的。见代码
async function test() {
console.log(1)
const b = await '我常常因为自己不够优秀而感到恐慌'
console.log(b)
const a = await new Promise(function(resolve, reject){
resolve(3)
})
console.log('我是被处理后的:', a)
}
const b = test()
console.log('我是还没被处理后的:', b)
复制代码
看吧,一个字符串照样能处理,小伙伴们可以自行尝试,包括数字,函数的返回值,都可以处理(提取)。但是就一个特点,async function 中的第一个await之后的所有代码都属于异步范畴!
那await就这么 无敌吗?不好意思不是的,它处理不了reject出口。
async function test() {
const a = await new Promise(function(resolve, reject){
reject(3)
})
}
test()
复制代码
报错了。。咋办呢。。。
async function test() {
try {
const a = await new Promise(function(resolve, reject){
reject('完蛋,我会被捕获')
})
} catch(e) {
console.log(e)
}
}
test()
复制代码
很简单,套一个try catch嘛。也就是任何一个可能从reject出口出来的,我们都要用try,catch捕获一下。 虽然有点麻烦,但是相比之前那些丑陋的代码,已经好了太多了。
好了,到这里也就结束了。不了解node的小伙伴们可以撤了。下面贴一个在node中自己实现的promisify方法。
const fs = require('fs');
function promisify(f) {
return function() { //虽然这里函数没参数,但运行时肯定会有参数哦
let args = Array.prototype.slice.call(arguments)
return new Promise((resolve,reject) => {
args.push((err, result) => {
if(err) {
reject(err)
} else {
resolve(result)
}
})
f.apply(null, args)
})
}
}
readFile = promisify(fs.readFile);
//基础版
readFile('./app.js').then( data => {
console.log(data.toString())
}).catch(e => {
console.log(e)
})
//进阶版! 使用async await
async funtion test() {
try {
const content = await readFile('./app.js')
console.log(content)
} catch(e) {
}
}
复制代码
回调地狱问题在node中非常明显,而我们通过promisify可以将一个函数转化为Promise对象。node中任何一个函数的最后一个回调函数一定是(err, data) => {}
。因此这里我们就把其作为数组的最后一项。如果err我们就从reject出口 出去,如果成功就从resolve出口出去。而第一步promisify则是有点像是函数柯里化,返回一个函数地址。好了文章到这里就结束了。通过写文章自己也再巩固一遍知识也是很好的。不足之处希望小伙伴们提出。
第一次写文章,好紧张。有没有什么潜规则啊!!!