从promise到async function

     事实上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则是有点像是函数柯里化,返回一个函数地址。好了文章到这里就结束了。通过写文章自己也再巩固一遍知识也是很好的。不足之处希望小伙伴们提出。

第一次写文章,好紧张。有没有什么潜规则啊!!!

猜你喜欢

转载自juejin.im/post/5b56d9555188251b134e7c51