所謂同步執行,就是從前往後執行,一句執行完了執行下一句。
而非同步執行,則不會等待非同步程式碼執行完,直接執行後面的程式碼;等非同步程式碼執行完返回結果之後再進行處理。
比如一些耗時操作會使用非同步執行的方式,以提高執行效率,最常見的莫過於Ajax網路請求。
以jQuery Ajax為例,假設有這樣的需求:
從地址A獲取資料a,從地址B獲取資料b,然後把資料a和b提交到介面C
ES5:使用callback實現
使用最原始的方式直接來寫,大概要寫成這樣子:
// 顯示載入提示
$.showLoading();
// 發起ajax請求
$.ajax({
url: 'https://example.com/api/a',
type: 'get',
success: function(res1){
$.ajax({
url: 'https://example.com/api/b',
type: 'get',
success: function(res2){
$.ajax({
url: 'https://example.com/api/c',
type: 'post',
success: function(res3){
$.hideLoading();
$.toast('請求成功');
},
error: function(res3){
$.alert('請求失敗: ' + res3);
}
});
},
error: function(res2){
$.alert('請求失敗: ' + res2);
$.hideLoading();
}
});
},
error: function(res1){
$.alert('請求失敗: ' + res1);
$.hideLoading();
}
});
複製程式碼
這就產生了回撥巢狀的問題,寫出來的程式碼既難看又不方便維護。
要解決這個問題,可以寫個函式包裝一下,把相似的邏輯分離出來執行,比如:
// 請求過程包裝函式
function Process(){
this.requests = []; // 儲存請求方法
this.count = 0; // 記錄執行到第幾個
this.result = []; // 儲存返回結果
this.onComplete = null; // 請求完成回撥
this.onSuccess = null; // 請求成功回撥
this.onError = null; // 請求出錯回撥
// 執行請求的方法
this.continue = function(){
if(this.count < this.requests.length){
var fn = this.requests[i];
fn();
this.count += 1;
} else {
this.onComplete(this.result);
}
}
}
// 建立物件
var p = new Process();
// 將請求方法放入
p.requests.push(function(){
$.ajax({
url: 'https://example.com/api/a',
type: 'get',
success: function(res){
p.onSuccess(res);
},
error: function(res){
p.onError(res);
}
});
});
p.requests.push(function(){
$.ajax({
url: 'https://example.com/api/b',
type: 'get',
success: function(res){
p.onSuccess(res);
},
error: function(res){
p.onError(res);
}
});
});
// 當請求成功
p.onSuccess = function(res){
// 儲存返回結果
p.result.push(res);
// 執行下一個
p.continue();
};
// 當請求失敗
p.onError = function(res){
$.alert('請求失敗: ' + res);
$.hideLoading();
};
// 當請求完成
p.onComplete = function(result){
$.ajax({
url: 'https://example.com/api/c',
type: 'post',
data: {
a: result[0],
b: result[1]
},
success: function(res){
$.hideLoading();
$.toast('請求成功');
},
error: function(res){
p.onError(res);
}
});
};
// 顯示載入提示
$.showLoading();
// 執行請求
p.continue();
複製程式碼
這樣通過一個包裝函式統一進行處理,從而避免了回撥巢狀的問題。
根據不同的業務邏輯,不同的程式設計習慣,這類函式實現的方式也各不相同。那麼有沒有一種方式,可以適用所有非同步呼叫呢?
ES6:Promise
Promise就給出了這樣一套規範,可以使用Promise來處理非同步操作。
Promise規範詳細的內容和實現比較複雜,需要另寫一篇文章,這裡就只介紹一下Promise的用法。
如何用Promise執行jQuery Ajax請求:
// 首先建立一個Promise物件例項,傳入一個執行函式
// 執行成功時呼叫resolve方法,傳入返回結果
// 執行失敗時呼叫reject方法,傳入錯誤原因
let p = Promise(function(resolve, reject){
$.ajax({
url: 'https://example.com/api/a',
type: 'get',
success: function(res){
resolve(res);
},
error: function(res){
reject(res);
}
});
});
// promise有一個then方法,在then方法中執行回撥
// 第一個是成功回撥,第二個是失敗回撥
p.then(function(data){
console.log('請求成功', data);
}, function(err){
console.error('請求失敗', err);
});
複製程式碼
也可以只傳入成功回撥,在後面用catch方法來捕獲錯誤,這樣可以統一處理錯誤:
// 使用catch方法捕獲錯誤
p.then(function(data){
console.log('請求成功', data);
}).catch(function(err){
console.error('請求失敗', err);
});
複製程式碼
Promise的鏈式呼叫:
// 在回撥中返回新的Promise,可以在下一個then中執行回撥,實現鏈式呼叫
p.then(function(data){
console.log('A請求成功', data);
return new Promise(function(resolve, reject){
$.ajax({
url: 'https://example.com/api/b',
type: 'get',
success: function(res){
resolve(res);
},
error: function(res){
reject(res);
}
});
});
}).then(function(data){
console.log('B請求成功', data);
}).catch(function(err){
console.error('請求失敗', err);
});
複製程式碼
前面需求的完整Promise方式實現:
// 用於返回jQuery Ajax的Promise方法,從而簡化程式碼
let promisifyAjaxRequest = function(url, data){
let method = data ? 'post' : 'get';
return new Promise(function(resolve, reject){
$.ajax({
url: url,
type: method,
data: data,
success: function(res){
resolve(res);
},
error: function(res){
reject(res);
}
});
});
};
// 顯示載入提示
$.showLoading();
// 用於儲存請求資料結果
let result = [];
promisifyAjaxRequest('https://example.com/api/a').then(function(data){
result.push(data);
return promisifyAjaxRequest('https://example.com/api/b');
}).then(function(data){
result.push(data);
return promisifyAjaxRequest('https://example.com/api/c', {
a: result[0],
b: result[1]
});
}).then(function(data){
$.hideLoading();
$.toast('請求成功');
}).catch(function(err){
$.alert('請求失敗: ' + err);
$.hideLoading();
});
複製程式碼
可以看到使用Promise之後的程式碼變得更加簡潔和統一。
但是Promise也有不足之處,就是使用起來不是特別直觀。
ES7:async+await
在ES7中,提供了async和await關鍵字,被稱為非同步的終極解決方案。
這個方案其實也是基於Promise實現的,具體細節就先不展開,還是重點來看使用方式。
使用async+await來實現前面需求:
// 因為基於Promise實現,所以同樣需要返回Promise
let promisifyAjaxRequest = function(url, data){
let method = data ? 'post' : 'get';
return new Promise(function(resolve, reject){
$.ajax({
url: url,
type: method,
data: data,
success: function(res){
resolve(res);
},
error: function(res){
reject(res);
}
});
});
};
// 函式定義前面加上async關鍵字
async function ajaxRequests(){
// promise方法前面加上await關鍵字,可以接收返回結果
let a = await promisifyAjaxRequest('https://example.com/api/a');
let b = await promisifyAjaxRequest('https://example.com/api/b');
let data = await promisifyAjaxRequest('https://example.com/api/c', {
a: a,
b: b
});
return data;
}
// 顯示載入提示
$.showLoading();
// 執行請求
// 用法同Promsie
ajaxRequets().then(function(data){
$.hideLoading();
$.toast('請求成功');
}).catch(function(err){
$.alert('請求失敗: ' + err);
$.hideLoading();
});
複製程式碼
可以看到,async+await的寫法更加直觀,也更像同步的寫法。