JS中非同步處理方案
本章介紹三種非同步處理方案:
- 回撥函式(callback)
- promise
- async/await
如果想要深入瞭解 promise
、generator+co
、async/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非同步的三種方法,還有好多不足之處,希望可以繼續學習和深入理解