JS中的非同步解決方案

YXi發表於2019-07-24

JS中非同步處理方案

本章介紹三種非同步處理方案:

  • 回撥函式(callback)
  • promise
  • async/await

如果想要深入瞭解 promisegenerator+coasync/await 的話,建議參考我的這篇文章《深入理解 promise、generator+co、async/await 用法》

回撥函式(callback)

回撥函式應該屬於最簡單粗暴的一種方式,主要表現為在非同步函式中將一個函式進行引數傳入,當非同步執行完成之後執行該函式

話不多說,上程式碼:

// 有三個任務console.log(1)console.log(2)console.log(3)
// 過5s執行任務1,任務1執行完後,再過5s執行任務2.....
window.setTimeout(function(){
	console.log(1)
	window.setTimeout(function(){
		console.log(2)
		window.setTimeout(function(){
			console.log(3)
		},5000)
	},5000)
},5000)
複製程式碼

看出這種方式的缺點了嗎?沒錯,試想,如果再多幾個非同步函式,程式碼整體的維護性,可讀性都變的極差,如果出了bug,修復的排查過程也變的極為困難,這個便是所謂的 回撥函式地獄

promise

promise簡單的說就是一個容器,裡面儲存著某個未來才會結束的時間(通常是一個非同步操作)的結果。從語法上說,promise就是一個物件,從它可以獲取非同步操作的訊息。promise提供統一的API,各種非同步操作都可以用同樣的方法處理。

如何理解:

  • 沒有非同步就不需要promise
  • promise本身不是非同步,只是我們去編寫非同步程式碼的一種方式

promise有所謂的 4 3 2 1

4大術語
一定要結合非同步操作來理解
既然是非同步,這個操作需要有個等待的過程,從操作開始,到獲取結果,有一個過程的

  • 解決(fulfill)指一個 promise 成功時進行的一系列操作,如狀態的改變、回撥的執行。雖然規範中用 fulfill 來表示解決,但在後世的 promise 實現多以 resolve 來指代之
  • 拒絕(reject)指一個 promise 失敗時進行的一系列操作
  • 終值(eventual value)所謂終值,指的是 promise 被解決時傳遞給解決回撥的值,由於 promise 有一次性的特徵,因此當這個值被傳遞時,標誌著 promise 等待態的結束,故稱之終值,有時也直接簡稱為值(value)
  • 據因(reason)也就是拒絕原因,指在 promise 被拒絕時傳遞給拒絕回撥的值

3種狀態
在非同步操作中,當操作發出時,需要處於等待狀態
當操作完成時,就有相應的結果,結果有兩種:

  • 成功了
  • 失敗了

一共是3種狀態,如下:

  • 等待態(Pending (也叫進行態)
  • 執行態(Fulfilled)(也叫成功態)
  • 拒絕態(Rejected) (也叫失敗態)

圖片載入失敗

針對每一種狀態,有一些規範:

等待態(Pending)
處於等待態時,promise 需滿足以下條件:

  • 可以遷移至執行態或拒絕態

執行態(Fulfilled)
處於執行態時,promise 需滿足以下條件:

  • 不能遷移至其他任何狀態
  • 必須擁有一個不可變的終值

拒絕態(Rejected)
處於拒絕態時,promise 需滿足以下條件:

  • 不能遷移至其他任何狀態
  • 必須擁有一個不可變的據因

2種事件
針對3種狀態,只有如下兩種轉換方向:

  • pending –> fulfilled
  • pendeing –> rejected

在狀態轉換的時候,就會觸發事件:

  • 如果是pending –> fulfiied,就會觸發onFulFilled事件
  • 如果是pendeing –> rejected,就會觸發onRejected事件

在呼叫resolve方法或者reject方法的時候,就一定會觸發事件

需要註冊onFulFilled事件 和 onRejected事件
針對事件的註冊,Promise物件提供了then方法,如下:
promise.then(onFulFilled,onRejected)

針對 onFulFilled,會自動提供一個引數,作為終值(value)
針對 onRejected,會自動提供一個引數,作為據因(reason)

1個物件
promise

注:只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他的操作都無法改變這個狀態

簡單來講,就還是promise中有著三種狀態pending,fulfilled,rejected。在程式碼中我們可以控制狀態的變更

new Promise(function(resolve,reject){
	console.log("pending");
	console.log("pending");
	resolve();
	reject();
})
複製程式碼

建立一個Promise物件需要傳入一個函式,函式的引數是resolve和reject,在函式內部呼叫時,就分別代表狀態由pending=>fulfilled(成功),pending=>rejected(失敗)

一旦promise狀態發生變化之後,之後狀態就不會再變了。比如:呼叫resolve之後,狀態就變為fulfilled,之後再呼叫reject,狀態也不會變化

在建立promise物件,只需要根據需求,轉換狀態即可。無非就是呼叫兩個函式:

  • resolve,傳遞value
  • reject,傳遞reason

Promise物件在建立之後會立刻執行,因此一般的做法是使用一個函式進行包裝,然後return一個promise物件

function betray(){
	return new Promise(function(resolve,reject){
		...//非同步操作
	})
}
複製程式碼

在使用時可以通過promise物件的內建方法then進行呼叫,then有兩個函式引數,分別表示promise物件中呼叫resolve和reject時執行的函式

function betray(){
	return new Promise(function(resolve,reject){
		setTimeout(function(){
			resolve();
		},1000)
	})
}


betray().then(function(){
	...//對應resolve時執行的邏輯
},function(){
	...//對應reject時執行的邏輯
})
複製程式碼

也可以用 catch 來執行失敗態

catch方法,用於註冊 onRejected 回撥

在這裡要明白兩件事情:

  • catch其實是then的簡寫,then(null,callback)
  • then方法呼叫之後,仍然返回的是promise物件,所以可以鏈式呼叫

使用如下:

betary().then(res=>...//對應resolve時執行的邏輯).catch(err=>...//對應reject時執行的邏輯)
複製程式碼

可以使用多個then來實現鏈式呼叫,then的函式引數中會預設返回promise物件

betray().then(function(){
	...//對應resolve時執行的邏輯
},function(){
	...//對應reject時執行的邏輯
})
.then(function(){
	...//上一個then返回的promise物件對應resolve狀態時執行的邏輯
},function(){
	...//上一個then返回的promise物件對應reject狀態時執行的邏輯
})
複製程式碼

使用promise來解決回撥地獄的做法就是使用then的鏈式呼叫

function fnA(){
	return new Promise(resolve=>{
		...//非同步操作中resolve
	})
}
function fnB(){
	return new Promise(resolve=>{
		...//非同步操作中resolve
	})
}
function fnC(){
	return new Promise(resolve=>{
		...//非同步操作中resolve
	})
}

fnA()
.then(()=>{
	return fnB()
})
.then(()=>{
	return fnC()
})
複製程式碼

特點是:

  • then方法通常是表示非同步操作成功時的回撥,也可以用catch方法表示非同步操作失敗時的回撥
  • 在呼叫的時候then在前後,catch在後
  • then方法可以呼叫多次,前一個then的返回值,會作為後一個then的引數
  • 支援鏈式呼叫

async/await

async、await是什麼?

async顧名思義是“非同步”的意思,async用於宣告一個函式是非同步的。而await從字面意思上是“等待”的意思,就是用於等待非同步完成。並且await只能在async函式中使用

通常async、await都是跟隨Promise一起使用的。為什麼這麼說呢?因為async返回的都是一個Promise物件同時async適用於任何型別的函式上。這樣await得到的就是一個Promise物件(如果不是Promise物件的話那async返回的是什麼 就是什麼);

await得到Promise物件之後就等待Promise接下來的resolve或者reject。

async、await解決了什麼?

傳統的回撥地獄式寫法:

getData(a=>{
	getMoreData(a,b=>{
		getMoreData(b,c=>{
			console.log(c)
		});
	});
});
//不行了,再多寫要迷了
複製程式碼

Promise改進後的寫法:

getData()
.then(a=>getMoreData(a))
.then(b=>getMoreData(b))
.then(c=>getMoreData(c))
複製程式碼

async/await改進後:

(async()=>{
	const a = await getData;
	const b = await.getMoreData(a);
	const c = await.getMoreData(b);
	const d = await.getMoreData(c);
})();
複製程式碼

async、await寫法

先來看看同步寫法:

console.log(1);

setTimeout(function () {
  console.log(2);
}, 1000);

console.log(3);
複製程式碼

輸出結果:

1
3
2
複製程式碼

可以看到輸出的順序並不是我們程式碼中所寫的那樣,下面來看下async、await是如何解決這個問題的

(async function () {

  console.log(1);

  await new Promise(function (resolve, reject) { 
    setTimeout(function () {
      console.log(2);
      resolve();
    }, 1000);
  });

  console.log(3);

}())
複製程式碼

輸出結果:

1
2
3
複製程式碼

可以看到這種寫法的輸出已經符合了我們的預期

async 的定義:

  • async函式會返回一個Promise物件
  • 如果async函式中是return一個值,這個值就是Promise物件中resolve的值
  • 如果async函式中是throw一個值,這個值就是Promise物件中reject的值

await 的定義:

  • await只能在async裡面
  • await後面要跟一個promise物件

常規的promise物件會被js先暫存到eventloop(事件佇列)中,因為js是單執行緒執行的,等執行棧空了之後,才會將事件佇列中的事件取出放入執行棧中執行

上述程式碼中先是將整段程式碼改造成了一個async(async可以用於任何函式)函式,然後又將setTimeOut改造成了一個Promise物件


使用第三方Promise庫

下面簡單介紹一下第三方的Promise庫

對開發中使用promise進行小結:

  • 沒有非同步,就不需要promise
  • 不使用promise,其實也是可以解決非同步程式設計的問題。使用promise,會使非同步的編碼變得更加優雅,功能會更強
  • 在進行promise程式設計的使用,有如下兩個場景:
    • 直接使用別人封裝好的promise物件,比如fetch、axios
    • 需要自己封裝promise物件

注意:axios和fetch必須使用promise方式,如:

圖片載入失敗!

針對自己封裝promise物件,又可以有如下兩種方式:

  • 自己封裝
  • 可以使用第三方的promise庫

比如,針對第三方的promise庫,有兩個知名的庫:

  • bluebird
  • q.js

可以利用bluebird 和 q.js 快速的生成promise物件。

以bluebird為例,在服務端演示其用法。

第一步:安裝

圖片載入失敗!

第二步:使用

圖片載入失敗!

是不是覺得 so-easy

以上就是解決JS非同步的三種方法,還有好多不足之處,希望可以繼續學習和深入理解


^_<

相關文章