上一章節解釋了什麼是同步/非同步的特性,我們也了解什麼是callback function。
接下來討論的議題,建議先了解同步/非同步、callback function,會比較好理解。
假設一個情境,我們執行了ABCD任務,它們都擁有非同步特性:
(function misA() {
setTimeout(() => {
console.log('任務A執行完畢');
}, Math.floor(Math.random() * 5000));
})();
(function misB() {
setTimeout(() => {
console.log('任務B執行完畢');
}, Math.floor(Math.random() * 5000));
})();
(function misC() {
setTimeout(() => {
console.log('任務C執行完畢');
}, Math.floor(Math.random() * 5000));
})();
(function misD() {
setTimeout(() => {
console.log('任務D執行完畢');
}, Math.floor(Math.random() * 5000));
})();
我們加了時間亂數,所以無法確保哪個任務先完成,這部分已經在同步/非同步章節闡述過了。
那今天如果,我們希望ABCD任務,能依序執行呢?
還記得有什麼方式可以讓我們控制任務流程的嗎?
沒錯,就是使用callback function:
function misA(callback) {
setTimeout(() => {
console.log('任務A執行完畢');
callback();
}, Math.floor(Math.random() * 2000));
};
function misB(callback) {
setTimeout(() => {
console.log('任務B執行完畢');
callback();
}, Math.floor(Math.random() * 2000));
};
function misC(callback) {
setTimeout(() => {
console.log('任務C執行完畢');
callback();
}, Math.floor(Math.random() * 2000));
};
function misD() {
setTimeout(() => {
console.log('任務D執行完畢');
}, Math.floor(Math.random() * 2000));
};
misA(() => {
misB(() => {
misC(() => {
misD();
});
});
});
上面跟callback function的範例幾乎一樣,只不過我們加了setTimeout( )來模擬非同步的情況。
使用這樣的方式,可以確保每個任務執行完後,再執行呼叫下一個任務callback function。
可是又看到了Callback Hell,即便我們已經使用箭頭函式,任務內容也都是空的,但可讀性依舊很差。
以往當需要處理setTimeout( )、XMLHttpRequest物件,等各種非同步的方式時,就必須得用上述的方式來處理,造成的麻煩顯而易見。
有鑑於此,ES6新增了一個物件「promise」,專門用來解決同步/非同步的問題,其實早在其他的框架或函式庫,有類似promise作法,直到ES6才被納入原生標準之一。
截至目前為止,已經可以在各大主流瀏覽器的新版運作了,但IE,恩….你知道的XD。
圖片來源:MDN Promise
首先promise物件一開始會有以下狀態:
promise物件一開始是等待狀態(pending),只有兩種執行結果,不是成功(fulfilled)就是失敗(rejected)。
Promise建構子的語法結構:
new Promise((resolve, reject) => {
statement
})
resolve:執行成功時要呼叫的函式。
reject:執行失敗時要呼叫的函式。
statement:程式主體,非同步處理的部分。
將Promise物件放入函式,並回傳,此函式便具有Promise的功能。
function asyncFunc(value) {
return new Promise(function (resolve, reject) {
if (value)
resolve(value);
else
reject(reason);
});
}
如果輸入的值具有truthy行為,就執行resolve(value);若否則執行reject(reason)。
將非同步的code,封裝在Promise物件內,並利用Promise物件所提供的方法達到流程控制。
下一步要做的是使用Promise物件非常重要的方法,then,then中文是然後,意思是回傳Promise物件並使用then方法來處理promise物件的狀態。
語法如下:
p.then(onFulfilled, onRejected);
p.then((response) => {
// 執行成功
}, (error) => {
// 執行失敗
});
then方法,有2個參數可用:
then函式會回傳一個新的promise物件,這樣做的目的是能夠使用方法串接(method chaining),讓多個非同步運算,可以一直串接下去。
我們來做個範例:
function async (value) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (value)
resolve(value);
else
reject('無輸入值');
}, 500);
})
}
async ('ABC').then(
response => {
console.log(`輸入值:${response}`);
}, error => {
console.log(`錯誤:${error}`);
}
);
async ().then(
response => {
console.log(`輸入值:${response}`);
}, error => {
console.log(`錯誤:${error}`);
}
);
輸入字串,由resolve( )函式回傳value('ABC'),這時物件的狀態是實現(fulfilled),呼叫then函式的onFulfilled,為避免搞混,參數使用response,接收value值,輸出結果。
未輸入值,狀態為拒絕(rejected),呼叫呼叫then函式的onRejected,將string('無輸入值')傳遞給error參數。
剛剛的範例還看不出來promise的好處,所以我們再增加一個非同步任務:
async ('ABC').then(
response => {
console.log(`第1次輸入值:${response}`);
return async ('DEF');
}, error => {
console.log(`錯誤:${error}`);
}
).then(
response => {
console.log(`第2次輸入值:${response}`);
}, error => {
console.log(`錯誤:${error}`);
}
);
第一次成功執行後,又再次的呼叫函式,再處理一次,所以我們可以一直使用then()函式做相同的工作,確保非同步任務的執行順序。
then函式的2個參數,分別處理實現(fulfilled)與拒絕(rejected)狀態,不過為了方便方法串接(method chaining),通常我們只在then函式,傳入第一個處理實現(fulfilled)的函式參數。
至於拒絕(rejected)狀態,會使用catch()函式來處理。
今天的議題先到這邊,我們明天再討論catch( )。
總結:
使用Promise與callback function,可以把多個非同步處理,執行流程轉化為有序的排列,並提共更完善的錯誤處理機制。
參考來源:
MDN Promise
從Promise開始的JavaScript異步生活
重新認識 JavaScript: Day 26 同步與非同步