大家在js編程中肯定遇到過異步問題,實踐中大家在異步問題上也不斷提出新的解決方案,這里就梳理一下異步解決方案。
回調函數
回調函數是最原始的解決方案,邏輯上很容易理解。看個例子:
function f1(value, callback) {
console.log('f1');
setTimeout(
() => {
value = setValue('f2');
callback.call(this, value);
}
, 1000
);
}
function f2(value){
console.log(value);
}
function setValue(value) {
return value;
}
let value;
f1(value, f2);
結果.png
這里的
f2
需要f1
延時1s完成value
的重新賦值后再打印value
,比較簡單的處理就是把f2
作為f1
的回調函數。
這是歷史最悠久的方案
- 優點:兼容性好
- 缺點:當回調函數不斷嵌套時,代碼會橫向發展,形成“意大利面”(Italian noodles)代碼
Promise對象
隨著歷史的發展,在前端社區的某些庫中有人提出了Promise
方案,后來這個方案也被寫入官方標準。本文不再談各種前端庫中Promise
的實現,只談目前ES規范
中的Promise
。
幾個要點:
-
Promise
是一個原生對象,我們使用Promise
對象是利用它的API。 -
Promise
對象的異步操作有三種狀態:pending
(進行中)、fulfilled
(已成功)和rejected
(已失敗)。回調函數只在fulfilled
(已成功)和rejected
(已失敗)狀態下觸發。 -
Promise
對象的實例可以調用then
方法指定不同狀態的回調函數(或catch
方法注冊rejected
(已失敗)狀態的回調函數)
使用Promise
對象重新實現上文的例子:
let value;
function f1() {
console.log('f1');
const promise = new Promise((resolve, reject) => {
setTimeout(
() => {
let value = setValue('f2');
if (!!value) {
resolve(value);
} else {
reject('error');
}
}
, 1000);
});
return promise;
}
function f2(value){
console.log(value);
}
function setValue(value) {
return value;
}
f1()
.then(
f2,
(error) => console.log(error)
);
結果.png
可以看到函數
f1
返回了一個Promise
對象的實例,這個實例就具有.then/.catch
等方法注冊回調函數。再看一個使用
Promise
實現的AJAX
封裝
function fetchRemote(url, method='GET', param=null) {
const promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.setRequestHeader("Accept", "application/json");
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4){
return;
}
if (xhr.status === 200 || xhr.status === 304) {
resolve(xhr.response);
} else {
reject(xhr.status);
}
};
xhr.send(param);
});
return promise;
}
// 調用
fetchRemote(url)
.then(
(response) => {
todo...
}
)
.catch(
(error) => {
todo...
}
)
這里提一個小細節: 請注意XMLHttpRequest對象的實例具有實例方法onreadystatechange()
,實際上這是一個注冊回調函數的api,每一次XMLHttpRequest實例對象的readyState/status
被改變時,回調函數都會被異步觸發
此外,Promise
對象也提供了一些新API適應不同場景,例如.race/.all
。
// Promise.race() 適用于等待多個promise第一個完成的場景
Promise.race(task1, task2, task3...).then().catch();
// Promise.all() 適用于等待多個promise同時完成的場景
Promise.all(task1, task2, task3...).then().catch();
Generator函數
學習過Generator函數
的同學應該知道,Generator函數
最大的特點就是不會順序執行到底,遇到yield
語句時會暫停,直到調用next
方法才會繼續執行。顯然,這種特性很適合異步場景。
還是實現最初的小例子:
const setValue = value => value;
let value;
function* f1() {
console.log('f1');
yield new Promise((resolve, reject) => {
setTimeout(() => {
value = setValue('f2');
if (!!value) {
resolve(value);
} else {
reject('error');
}
}, 1000);
});
}
function f2 (value) {
console.log(value);
}
let g = f1();
g.next().value
.then(f2)
.catch((error) => {console.error(error);});
結果.png
再看一個遠程請求的例子:
function* fetchData(url) {
const data = yield fetch(url);
yield handler(data);
}
const g = fetchData('http://url/api');
g.next();
g.next();
可以看到,通過使用Generator函數
的屬性,異步邏輯變得非常簡單,只需要在異步等待的函數處“暫停”即可。這種語法在redux-saga
的庫中有很好的應用。
async函數
async函數
顧名思義,就是為解決異步場景而生的。
async函數
返回的是一個Promise
對象,所以async函數
返回的結果可以調用.then()
/.catch()
方法,本質上和promise對象
用法類似,只是有了語法糖書寫更簡便。
還是這個熟悉的老例子:
const setValue = value => value;
let value;
function f1() {
console.log('f1');
return new Promise((resolve, reject) => {
setTimeout(() => {
value = setValue(null);
if (!!value) {
resolve(value);
} else {
reject('error');
}
}, 1000);
});
}
function f2 (value) {
console.log(value);
}
async function fn() {
return await f1();
}
fn()
.then(
(value) => {console.log(value);}
)
.catch(
(error) => console.error(error)
);
結果.png
簡單解釋一下,
async
和await
需要互相配合,await
只能在async函數
中使用,await
等的是promise
對象,如果await
得到的不是promise
對象則會把普通對象直接作為promise
的value
并更新狀態為resolved
。可以很明顯看出,一般情況下
async函數
不具有優勢,它的優勢在處理then
鏈。看個例子:
async function manycbs() {
const arg1 = 'xxx';
const arg2 = await step1(arg1);
const arg3 = await step2(arg1, arg2);
const result = await step3(arg1, arg2, arg3);
console.log(result);
}
manycbs();
這樣就可以看出async函數
可以把異步寫得像同步一樣。