前言
JavaScript 提倡書寫異步方法, 這樣可以更好地利用事件隊列機制, 來高效的無阻塞的運行應用。但這容易帶來了大量的異步回調嵌套,常常就會出現俗稱“回調地獄(callback hell)”的現象。
特別的當多個異步要求按照順序執行,其結果多級依賴時,多級的回調嵌套不僅使得代碼變難看難懂,更使得調試、重構的過程異常復雜。所以就有了異步流程控制,可以更方便的用看似同步的方法來寫異步的東西,讓程序邏輯清晰易懂。下面就來談談異步流程處理的幾種方式。
一、回調函數的方式
例如1,你會遇到這樣的一種需求,獲取一組地址信息,但是異步返回的是對應地址的code,拿到該數據后,需要對每一項轉換成中文地名,全部轉換完成后返回頁面。
// 獲取一組地址信息
$.get('/api/getAddressData',{},function(data){
let list = data.addressList;
// 在回調函數中,對轉換好的數據進行操作
let transfer = after(list.length,function(data){
console.log('轉換好了',data)
})
for(let i =0; i< list.length; i++){
let item = list.[i]
transfer(item);
}
});
function after(times,callback){
let list = [];
return function(data){
// 將數據進行轉換
$.get('/api/getTransferAddress',{code:item.code},function(data){
data['addressName'] = data.addressName;
list.push(data);
// 所有數據轉換完成后,調用回調函數,把轉換好的數據返回給回調中使用
if(--times === 0){
callback(list);
}
})
}
}
例如2,文件讀取 異步i/o。1.txt,2.txt,3.txt的結果多級依賴。這種方式的做法就是在回調函數中,執行下一次的異步事務,依次嵌套執行。
// 讀取 異步i/o
let fs = require('fs');
fs.readFile('./1.txt','utf8',function(err,data){ // 先讀取1.txt
if(err) return console.log(err);
fs.readFile(data,'utf8',function(err,data){ //從1.txt得到的下一個要讀取的文件的路徑==> ./2.txt
if(err) return console.log(err);
console.log(data);
fs.readFile(data,'utf8',function(err,data){ //從2.txt得到的下一個要讀取的文件的路徑==> ./3.txt
if(err) return console.log(err);
console.log(data);
});
});
});
從以上代碼可以看出,采用回調函數進行異步處理的方式,代碼邏輯比較復雜,不易于閱讀,而且常常容易出現“回調地獄”的寫法。
二、promise 方式
1、什么是promise?
Promise是異步編程的一種解決方案,它有三種狀態,分別是pending-進行中、resolved-已完成、rejected-已失敗。
在2015年6月, ES2015(即 ECMAScript 6、ES6) 正式發布。其中 Promise 被列為正式規范,成為 ES6 中最重要的特性之一。在此之前使用promise一般都是借用第三方庫,例如常用的q庫、co庫、bluebird等。
優點:可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操作更加容易。
缺點:
1)無法取消 Promise,一旦新建它就會立即執行,無法中途取消
2)無如果不設置回調函數,Promise 內部拋出的錯誤,不會反應到外部
3)當處于 Pending 狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)
2、基本API
- Promise#then
// 基本用法
promise.then(onFulfilled, onRejected);
let promise = new Promise(function(resolve, reject) {
setTimeout(function(){
resolve("成功的數據");
},10)
});
promise.then(function(value) {
console.log(value); // ==> 成功的數據
}).catch(function(error) {
console.log(error);
});
- Promise#catch
// 捕獲錯誤的方法,then沒有傳遞錯誤函數,過程中拋出的錯誤,都能捕獲到;
// 等價于 promise.then(undefined, onRejected) 的語法糖
promise.then(function(value) {
console.log(value);
}).then(function(){
console.log(value);
}).catch(function(error) {
console.log(error);
});
- Promise#resolve
// Promise的成功靜態方法
Promise.resolve([1,2,3]).then(function(data){
console.log(data);
});
- Promise#reject
// Promise的失敗靜態方法
Promise.reject(new Error("error"));
-
Promise#all
生成并返回一個新的promise對象。
Promise.all接收的參數是一個數組,參數里面放置的是promise對象,最后返回的數據也是按照參數順序,返回一個數組,對應的值就是對應的promise對象成功返回的值
** 注意:all的方法是所有promise都成功了,才算成功,否則就算失敗
let p1 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve("異步數據1");
},10)
})
let p2 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve("異步數據2");
},10)
})
let p3 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve("異步數據2");
},10)
})
Promise.all([p1,p2,p3]).then(function(data){
console.log(data) // ==>["異步數據1", "異步數據2", "異步數據2"]
}, function(err){
console.log(err)
})
-
Promise#race
生成并返回一個新的promise對象。
參數 promise 數組中的任何一個promise對象如果變為resolve或者reject的話, 該函數就會返回,并使用這個promise對象的值進行resolve或者reject
** 注意:只要有一個promise成功了 就算成功。如果第一個失敗了就失敗了
let p1 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve("異步數據1");
},10)
})
let p2 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve("異步數據2");
},10)
})
let p3 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve("異步數據2");
},10)
})
Promise.race([p1,p2,p3]).then(function(data){
console.log(data) // ==>"異步數據1" 取決于promise的執行時間,返回第一個成功的promise的結果
}, function(err){
console.log(err)
})
三、generator方式
generator(生成器)是ES6標準引入的新的數據類型。一個generator看上去像一個函數,但可以返回多次。原理是將一個函數劃分成若干個小函數,每次調用時移動指針,內部是一個條件判斷,執行對應的邏輯
- genrator 函數要用* 來比標識,yield(暫停 產出)
- 它會將函數分割成部分,調用一次next就會繼續向下執行
- 返回結果是一個迭代器 迭代器有一個next方法
- yield后面跟著的是value的值,異步一般跟著的是promise對象
- yield等號前面的是我們當前調用next傳進來的值
- 第一次next傳值是無效的
異步 generator主要和promise搭配使用,co庫可以自動的將generator進行迭代
// 使用generator處理異步流程,讓p1和p2兩個異步事務順序執行
// 首先安裝co庫 npm install co
let co = require('co');
let p1 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve("異步數據1");
},10)
})
let p2 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve("異步數據2");
},10)
})
function *getData(){
let list = []
let a = yield p1;
list.push(a)
let b = yield p2;
list.push(b)
return list
}
// co庫自動進行迭代
co(getData()).then(function (data) {
console.log(data) // ==>["異步數據1", "異步數據2"]
})
四、async / await 方式
在最新的ES7(ES2017)中提出的前端異步特性:async、await。
async用來修飾函數,聲明一個函數是異步的。而await是用于等待異步完成。并且await只能在async函數中使用。
實際上async和await(語法糖) 相當于 co + generator,但是少了co的靈活, co不僅可以包裝Promise還可以包裝數組, 還可以包裝thunk, 而await自動包裝后的東西只能是原始值, 或者 Promise
通常async、await都是和Promise一起使用的。
因為async返回的都是一個Promise對象同時async適用于任何類型的函數上。這樣await得到的就是一個Promise對象(如果不是Promise對象的話那async返回的是什么 就是什么);
await得到Promise對象之后就等待Promise接下來的resolve或者reject。
那么async / await 解決的問題有哪些呢?
1.回調地獄
2.并發執行異步,在同一時刻同步返回結果 Promise.all
3.解決了返回值的問題
4.可以實現代碼的try/catch;
let p1 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve("異步數據1");
},10)
})
let p2 = new Promise(function(resolve, reject){
setTimeout(function(){
resolve("異步數據2");
},10)
})
async function getData(){
let list = [];
try{
let content1 = await p1;
list.push(content1); // content1就是p1這個promise對象resovle返回的數據
let content2 = await p2;
list.push(content2); // content2就是p2這個promise對象resovle返回的數據
return list
}catch(e){ // 如果出錯會catch
console.log('err',e)
}
}
// async函數返回的是promise,
getData().then(function(data){
console.log(data); // ==>["異步數據1", "異步數據2"]
},function(err){
console.log(err)
})