從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則是有點像是函數柯里化,返回一個函數地址。好了文章到這裏就結束了。

相關文章
相關標籤/搜索