【JS進階】非同步 Promise、Async/Await

Aaron Chang
3 min readNov 6, 2022

--

JavaScript 最容易搞混的特性之一:「同步 (Synchronous) 與 非同步 (Asynchronous)」的差別,同步是指必須先弄完一項之後再去處理另一項整個流程會被前一個步驟卡住,像是先完成 A 才能做 B、C、D …;非同步則是不用等待 A 做完才做 B、C,而是這三個事情可以同時發送出去。

在 Web 開發中,我們需要處理非同步操作,例如:電腦向網路或伺服器發出請求,在等待過程中,電腦可以在繼續執行其他任務。

而JavaScript使用事件循環,讓其在非同步操作時,先執行其他任務,最初,JavaScript使用Callback Function來處理非同步操作,由於巢狀式的結構,讓代碼難以閱讀。

在ES6後,誕生了Promise語法,讓代碼增加了可讀性,ES8則出現了Async/Await語法,是一種語法糖,讓promise更加簡潔,有更高的可讀性。

Promise

Promise 是一個表示非同步運算的最終完成或失敗的物件,Promise有以下3種狀態:

  1. Pending 擱置:初始狀態
  2. Fulfilled 實現:操作已成功完成,promise 現在有一個resolved值。
  3. Rejected 拒絕:表示操作失敗了
Promise在生活中案例

舉一個Promise生活化的例子,一個洗完機初始狀態是Pending,當放入碗盤後,會出現兩種情況,第一種是碗盤洗乾淨,呈現Fulfilled,第二種情況是Rejected因為洗碗精未添加,出現錯誤(Error)。

建立 promise 的方法

要創建一個新Promise物件,要使用new關鍵字和promise建構器

const executorFunction = (resolve, reject) => { };
const myFirstPromise = new Promise(executorFunction);

promise建構器包含一個executor function,executor function包含兩個參數:參數 resolve()reject() ,當resolve() 被呼叫時,會將promise狀態由pending轉為fullfilled,且resolved值將被傳入resolve()

若是reject() 被調用(invoked),此時promise狀態則從pending轉變成rejected,發生錯誤的訊息則會傳入reject()

以下是「在線上訂購太陽眼鏡」作為promise範例

//庫存
const inventory = {
sunglasses: 1900,pants: 1088,bags: 1344};// promise程式碼const myExecutor = (resolve, reject) => { if (inventory.sunglasses > 0) { resolve('Sunglasses order processed.'); } else { reject('That item is sold out.'); }}const orderSunglasses = () =>{ return new Promise(myExecutor);}const orderPromise = orderSunglasses();console.log(orderPromise)

如何使用Promise?

這邊要介紹一個JavaScript的非同步函式:setTimeout() ,以毫秒為單位設置延遲執行程式碼,包含兩個參數: callback function及延遲的毫秒數

const delayedHello = () => {
console.log('Hi! This is an asynchronous greeting!');
};

setTimeout(delayedHello, 2000);

以上面這段程式碼來看,setTimeout()「至少」延遲2秒(2000毫秒),會甚麼是「至少」呢?而不是精確時間?

這裡牽涉到JavaScript的事件循環(Event Loop),當延遲2秒後,delayedHello() 會被安排到執行序列上,但其他同步程式碼則會繼續執行,因此有可能多於2秒後才執行delayedHello()

了解setTimeout() 後,可以利用這個函式的特性建構一個非同步的promise

const returnPromiseFunction = () => {
return new Promise((resolve, reject) => {
setTimeout(( ) => {resolve('I resolved!')}, 1000);
});
};

const prom = returnPromiseFunction();

promise方法 — .then()

回到洗完機的例子,在promise物件中有個.then() 方法,這個方法用來表示當promise完成時(無論成功或失敗)然後(then)要做甚麼?

.then() 是一個高階函式( higher-order function),以兩個callback函式做為參數,這兩個callback函式也稱作處理器(handler),第一個是success handler function或稱onFulfilled函式,當條件滿足時運行、第二個則是failure handler function或稱onRejected function,當操作失敗時執行。

let prom = new Promise((resolve, reject) => {
let num = Math.random();
if (num < .5 ){
resolve('Yay!');
} else {
reject('Ohhh noooo!');
}
});

const handleSuccess = (resolvedValue) => {
console.log(resolvedValue);
};

const handleFailure = (rejectionReason) => {
console.log(rejectionReason);
};

prom.then(handleSuccess, handleFailure);

由於不確定promise是成功或失敗,因此要針對兩種情況都給予相對應的程式碼

promise方法 — catch()

編寫簡潔的程式碼,有個原則稱為關注點分離(Separation of concerns,SoC),將代碼分離成不同的部分,每個部分處理特定的任務,能夠快速瀏覽代碼,增加可讀性,並知道如果某些功能不起作用,該去哪裡尋找,加快debug速度。

可以將剛剛提到原則運用在.then()中,將呈現如下程式碼

prom
.then((resolvedValue) => {
console.log(resolvedValue);
})
.then(null, (rejectionReason) => {
console.log(rejectionReason);
});

於是另一個promise方法.catch()便派上用場

prom
.then((resolvedValue) => {
console.log(resolvedValue);
})
.catch((rejectionReason) => {
console.log(rejectionReason);
});

.catch() 中只有一個參數便是failure handler function

鏈結promises Promises chaining

在非同步程式中常見當一個promise完成時,再立即呼叫另一個promise,舉一個生活化的例子:把髒衣服放進洗衣機。如果衣服洗乾淨了,就要把衣服放在烘乾機裡。烘乾機運行後,如果衣服乾了,就可以折好收起來。

以下是一個簡單的promise鏈:

firstPromiseFunction()
.then((firstResolveVal) => {
return secondPromiseFunction(firstResolveVal);
})
.then((secondResolveVal) => {
console.log(secondResolveVal);
});
  • 調用一個firstPromiseFunction()返回promise函式
  • .then()使用匿名函式作為成功處理器(success handler)進行調用
  • 在成功處理程序(success handler)中,return一個新的 promise — — 調用第二個函數的結果,secondPromiseFunction()以及第一個 promise 的resolved值。
  • 調用第二個.then()來處理第二個promise執行
  • .then(),我們有一個成功處理器,它將第二個promise的resolved值console出來

promise常見錯誤

  1. 使用巢狀promise,而不是鏈結promise
returnsFirstPromise()
.then((firstResolveVal) => {
return returnsSecondValue(firstResolveVal)
.then((secondResolveVal) => {
console.log(secondResolveVal);
})
})

這種寫法將第一個promise鑲嵌在promise裡,如果有多個promise則造成程式碼混亂

2. 忘記return

returnsFirstPromise()
.then((firstResolveVal) => {
returnsSecondValue(firstResolveVal)
})
.then((someVal) => {
console.log(someVal);
})

這裡第二個.then(). 應該處理第二個 promise 的邏輯,但由於沒有return,所以.then()會在與第一個 promise 做相同的判斷!

Async/Await

Async/Await語法並沒有新增加的功能,而是promise的語法糖,讓我們在寫非同步的程式碼時,如同在寫同步的程式碼一般

Async

async function myFunc() {
// Function body here
};

myFunc();

async 函式會回傳一個promise,換句話說,我們可以使用promise中的方法.then().catch() ,async函式會出現以下3種情況:

  • 如果函式沒有return任何內容,回傳一個resolved值為undefined.
  • 如果函式return了一個非promise,回傳一個promise且該值為resolved值
async function fivePromise() { 
return 5;
}

fivePromise()
.then(resolvedValue => {
console.log(resolvedValue);
}) // Prints 5
  • 如果函式return一個promise,回傳該promise

Await Operator

async函式中總是與await一起使用,且await 只能在async函式本體(function body)中使用

await會暫停函式的執行,直到async函式的promise不再處於pending狀態,await會得到 promise 操作結果的值

const brainstormDinner = () => {   return new Promise((resolve, reject) => {   console.log(`I have to decide what's for dinner...`)   setTimeout(() => {      console.log('Should I make salad...?');      setTimeout(() => {         console.log('Should I make ramen...?');         resolve('beans');               }, 1000);             }, 1000);    }); };async function announceDinner() {let meal = await brainstormDinner();console.log(`I'm going to make ${meal} for dinner.`);}announceDinner();

處理錯誤

.catch()與promise鏈一起使用時,難以發現在promise鏈中的哪段程式碼引發了錯誤,也就是promise產生reject的情形,大幅增加debug難度。

有了async...await使用try...catch語法處理錯誤情況,使用這種語法,能夠以處理同步程式碼相同的方式處理錯誤。

async function usingTryCatch() {
try {
let resolveValue = await asyncFunction('thing that will fail');
let secondValue = await secondAsyncFunction(resolveValue);
} catch (err) {
// Catches any errors in the try block
console.log(err);
}
}

usingTryCatch();

由於async函式返回一個promise,仍然可以將原生 Promise.catch()async函式一起使用

以下將進行一個簡單的範例:

  1. cookBeanSouffle()隨機回傳一個resolve或reject的promise。當resolve時,promise 顯示'Bean Souffle';當reject時,顯示'Dinner is ruined!'
let randomSuccess = () => {
let num = Math.random();
if (num < .5 ){
return true;
} else {
return false;
}
};
let cookBeanSouffle = () => {
return new Promise((resolve, reject) => {
console.log('Fingers crossed... Putting the Bean Souffle in the oven');
setTimeout(()=>{
let success = randomSuccess();
if(success){
resolve('Bean Souffle');
} else {
reject('Dinner is ruined!');
}
}, 1000);
});
};

2. hostDinnerParty() 為一個async 函式,try 語法中執行當cookBeanSouffle() 此promise函式為resolve的情況,catch 後則寫入當錯誤發生,所執行的程式碼

async function hostDinnerParty(){
try {
let i = await cookBeanSouffle()
console.log(`${i} is served!`)
} catch (error){
console.log(error)
console.log('Ordering a pizza!')
}
}
hostDinnerParty()

--

--