从promise到async function

     事实上async function只不过是对Promise一个很好的封装,从es6到es7,而async异步方法确实实现起来 也可让代码变得很优雅,下面就由浅到深具体说说其中的原理。
html

长篇预警

     promise是es6中实现的一个对象,它接收一个函数做为参数。这个函数又有两个参数,分别是 resolve和reject。node

const a = new Promise(function(resolve, reject){
	console.log(1)
	resolve(3)
	reject(5) 
	console.log(4)
})
console.log('outter')
console.log(a) 
	    
复制代码

结果以下:git

      这里先提一个关键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呢!言归正传,实质上这里是帮咱们返回了一个已是resolved状态的Promise(具体规则见mdn),而且由于咱们并无传递参数,所以这里接收到的参数就是undefined。接着看代码es6

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对象,再传递参数就行了(具体规则请看mdn)。可是到这里事实上可能有的小伙伴已经想报警了。这个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的github

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上。嗯,就这么简单。可是要注意,若是第一个结束的是错误的,同样也是算做跑最快的那个,返回给result。所以外面应该用catch接收一下,同时自行判断逻辑(可能由于网络的缘由须要咱们再执行一遍啦仍是啥)
     Promise.all。他必需要接收的promise实例所有变为resolve才返回(返回这些promise实例中resolve中的参数组成的数组),有一个变成reject,它就返回这个reject的参数。直接举例子。咱们须要异步加载三张图片,可是我必需要三张所有加载完我一块儿显示,我不要一张一张的出来。三张都出来就是resolve,任意一张失败了很差意思我就都不给你显示。
     剩下的还有一些promise方法我就很少说了,用的也很少,直接看文档就行了。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)
复制代码

     注意上面代码中数字就暗示了执行的前后顺序!好了先别在乎这段代码,只是让你看看。先往下看。下面我会再次由浅入深谈谈原理。。。

     仍是同样,代码说话bash

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捕获一下。 虽然有点麻烦,可是相比以前那些丑陋的代码,已经好了太多了。

     很好,下面来个再度进阶的,也是我我的以前遇到的一个坑。场景是这样的,我作了一个很是简单的爬虫(puppeteer),主要就是爬取图片而后下载到本地。 场景中有这样一段代码。

srcs.forEach( async(src) => {
		await srcImages(srcs[i], config.cat)
		console.log(1)
		page.waitfor(200)
	})
	async srcImages(){conole.log(2)//balabalabalabala具体逻辑就略过了} 
复制代码

     大概意思就是我爬取了每一个图片的网址,放在一个数组里,而后对数组里面每一个地址都调用一个函数,这个函数负责下载。而且这个函数就是用async包裹的,(还记得async就是把一整个函数用Promise包裹吗),而后每一次下载完我都等待200ms,避免操做太频繁把我IP封了。问题来了,小伙伴猜猜输出????
     结果证实,我一开始的想法彻底错误,这些1是连续打出来的。嘿嘿嘿,为何会这样呢?我来捋一捋,咱们一开始对数组第一项进行操做,遇到了第一个await,很好,后面代码所有异步等待。关键来了,主线任务接着运行,开始操做数组的第二项。。。。。就这样,把数组所有遍历完毕以后,咱们再所有一块儿下载(以前都所有挂在异步等待同步的主线程运行结束呢)。。。。。所有下载完后,咱们打印全部的2。。。。而后咱们再咱们打印全部的1。。。咱们再等待200ms * 数组长度的时间。。 。饿。。。坑人。。。那么我最后是怎么解决的?

async function test() {
			for(let i = 0; i < srcs.length; i++) {
				await srcImages(srcs[i], config.cat)
				await page.waitfor(200)
			}
		}
		test()
复制代码

     嘿嘿嘿,仍是用for循环来代替forEach好一些。这里也给我提了个警钟,当传统的那些forEach,map之流遇到async的时候,仍是应该注意一下的,可能会跟预想的逻辑不同哦。

     在这篇文章以后,后续的文章我应该都只会发到本身的博客上。每次发的时候应该都会在掘金沸点更新一下。但若是文章较长的话我应该也会先在掘金上更新一下(毕竟图片多了的话。掘金上外链直接生成,而不是存在本地)。

     好了,到这里也就结束了。不了解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则是有点像是函数柯里化,返回一个函数地址。好了文章到这里就结束了。

相关文章
相关标签/搜索