WeHelp
callback、Promise 和 async/await 簡介
2022-10-06 10:52:20
本篇文章會介紹 javascript 的非同步程式設計發展,從最初的 [Callback](https://developer.mozilla.org/zh-TW/docs/Glossary/Callback_function) 技術,到 2015 年的 [Promise](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Promise) ([ES6](https://262.ecma-international.org/6.0/)),到 2017年的 [async](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/async_function)/[await](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/await) ([ES8](https://262.ecma-international.org/8.0/))。 如果你還沒有理解非同步程式設計,可以參考[同步](https://developer.mozilla.org/zh-TW/docs/Glossary/Synchronous)、[非同步](https://developer.mozilla.org/zh-TW/docs/Glossary/Asynchronous)和[非同步的 JavaScript 介紹](https://developer.mozilla.org/zh-TW/docs/Learn/JavaScript/Asynchronous/Introducing) - 一個貫穿全文的例子 - 同步程式設計逛夜市 - Callback 逛夜市 - Promise 逛夜市 - Promise.all 逛夜市 - async/await 逛夜市 - 比較各個方法 - 總結 - 參考資料 ## 一個貫穿全文的例子 有一天你去逛夜市,逛夜市一定要**買雞排和珍奶** ``` javascript class Food { name = '雞排' } class Drink { name = '珍奶' } ``` 而且**雞排和珍奶都到手你才會開動**,換句話說當你買完雞排你不會先吃掉,一定會等到珍奶也買好。 ``` javascript function eat(food, drink) { // 開動 console.log(food.name, drink.name) } ``` ## 同步程式設計逛夜市 買雞排和買珍奶都要排隊各 2 秒,使用同步等待,也就是**人在現場等候** ``` javascript function wait(second) { const ms = second * 1000 const start = new Date() while (true) { if (new Date() - start > ms) { break } } } ``` 先去買雞排,再去買珍奶,開動的時候已經等 4 秒 ``` javascript function orderFood() { // 買雞排 wait(2) // 同步等待 2 秒 return new Food() } function orderDrink() { // 買珍奶 wait(2) // 同步等待 2 秒 return new Drink() } function main() { const food = orderFood() const drink = orderDrink() eat(food, drink) } ``` ## Callback 逛夜市 夜市攤販引進協助後續服務,人不用現場等,攤販備餐完,**攤販呼叫後續事項** ``` javascript function wait(second, callback, arg) { const ms = second * 1000 setTimeout(callback, ms, arg) } function orderFood(callback) { // 買雞排 wait(2, callback, new Food()) // 非同步等待 2 秒,完成後呼叫後續函式 } function orderDrink(callback) { // 買珍奶 wait(2, callback, new Drink()) // 非同步等待 2 秒,完成後呼叫後續函式 } ``` 而你打算接到電話後,去看看是不是雞排珍奶都完成 ``` javascript let food = undefined let drink = undefined function callme() { if (food !== undefined && drink !== undefined) { eat(food, drink) } } ``` 你告訴雞排攤位,做好了放在那邊,並打電話告訴你 (callme) ``` javascript function setFoodAndCallme(newFood) { food = newFood callme() } ``` 你也告訴珍奶攤位一樣的事情 ``` javascript function setDrinkAndCallme(newDrink) { drink = newDrink callme() } ``` Callback 出發逛夜市,由於你點完雞排立刻點珍奶,然後等他們通知,只用 2 秒多一些就拿完雞排珍奶 ``` javascript function main() { orderFood(setFoodAndCallme) orderDrink(setDrinkAndCallme) } ``` ## Promise 逛夜市 有一天攤販決定,不想處理後續事項,那件事不該是攤販的工作,所以**餐點完成後讓你自己處理後續事項** 其中 `resolve` 是指非同步處理成功,可以使用這個函式繳交回傳值,例如: `resolve(arg)` 代表回傳 arg ``` javascript function wait(second, arg) { const ms = second * 1000 return new Promise((resolve, reject) => setTimeout(() => resolve(arg), ms)); } function orderFood() { // 買雞排 return wait(2, new Food()) // 非同步等待 2 秒,完成後回傳物件 } function orderDrink() { // 買珍奶 return wait(2, new Drink()) // 非同步等待 2 秒,完成後回傳物件 } ``` 既然攤販不處理後續事項,那就自己處理,**攤販做好餐點告訴你 (`.then()`)** `.then()` 的參數是一個函式,函式裡是否有參數根據 resolve 決定,上面會帶 arg 進去作為參數,所以就接一個 arg 處理時間和 Callback 大略相同,但是最大不同點是,攤販不需要知道後續事項,交給你自己處理 ``` javascript function main() { orderFood().then(arg => setFoodAndCallme(arg)) orderDrink().then(arg => setDrinkAndCallme(arg)) } ``` ## Promise.all 逛夜市 由於雞排珍奶不同攤販,他們完成的時間不一樣,所以前面才需要 setFood 和 setDrink 今天你希望當**雞排、珍奶都完成時,再通知我去處理**,直接移除 ```setFoodAndCallme```, ```setDrinkAndCallme``` 和 ```callme``` 其中 ```[food, drink]``` 採用[解構賦值](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) ``` javascript function main() { Promise.all([orderFood(), orderDrink()]) .then(([food, drink]) => eat(food, drink)) } ``` ## async/await 逛夜市 今天客人希望當**雞排、珍奶都完成時,直接送給客人**,從 Promise.all 小改,就可以在 main 呼叫 eat,而不是 then main 函式有加 ```async```,Promise 前面有加 ```await``` ``` javascript async function main() { const [food, drink] = await Promise.all([orderFood(), orderDrink()]) eat(food, drink) } ``` ## 比較各個方法 ### wait ``` javascript /* 同步 */ function wait(second) { const ms = second * 1000 const start = new Date() while (true) { if (new Date() - start > ms) { break } } } /* Callback */ function wait(second, callback, arg) { const ms = second * 1000 setTimeout(callback, ms, arg) } /* Promise, Promise.all, async/await */ function wait(second, arg) { const ms = second * 1000 return new Promise((resolve, reject) => setTimeout(() => resolve(arg), ms)); } ``` ### order ``` javascript /* 同步 */ function orderFood() { // 買雞排 wait(2) // 同步等待 2 秒 return new Food() } /* Callback */ function orderFood(callback) { // 買雞排 wait(2, callback, new Food()) // 非同步等待 2 秒,完成後呼叫後續函式 } /* Promise, Promise.all, async/await */ function orderFood() { // 買雞排 return wait(2, new Food()) // 非同步等待 2 秒,完成後回傳物件 } ``` ### set and call 同步和 Promise.all 不需要這個部分,這個方法會讓程式變得複雜,可以的話使用 Promise.all 處理會更好 ``` javascript /* Callback, Promise */ let food = undefined function setFoodAndCallme(newFood) { food = newFood callme() } function callme() { if (food !== undefined && drink !== undefined) { eat(food, drink) } } ``` ### main ``` javascript /* 同步 */ function main() { const food = orderFood() const drink = orderDrink() eat(food, drink) } /* Callback */ function main() { orderFood(setFoodAndCallme) orderDrink(setDrinkAndCallme) } /* Promise */ function main() { orderFood().then(arg => setFoodAndCallme(arg)) orderDrink().then(arg => setDrinkAndCallme(arg)) } /* Promise.all */ function main() { Promise.all([orderFood(), orderDrink()]) .then(([food, drink]) => eat(food, drink)) } /* async/await */ async function main() { const [food, drink] = await Promise.all([orderFood(), orderDrink()]) eat(food, drink) } ``` ## 總結 - 同步程式設計就如同人在現場候餐 - 非同步程式設計,**減少等待時間**,**也讓等候時間可以處理其他事情** - Callback, Promise 和 async/await 是非同步程式設計的技術 - Callback 是將後續事項交給非同步函式,請他完成的時候呼叫 (將後續事項委託攤販) - Promise 消除非同步函式和 Callback 的耦合 (攤販不需要處理客人後續事項) - Promise.all 提供整合多個非同步函式的功能 (兩者都完成再通知我) - async/await 將回傳值取回到呼叫者,讓非同步程式設計概念更接近同步程式設計 (兩者都完成直接傳回給你) ## 參考資料 - [async](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/async_function) - [await](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/await) - [Callback](https://developer.mozilla.org/zh-TW/docs/Glossary/Callback_function) - [ECMAScript® 2015 Language Specification](https://262.ecma-international.org/6.0/) - [ECMAScript® 2017 Language Specification](https://262.ecma-international.org/8.0/) - [Promise](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Promise) - [同步](https://developer.mozilla.org/zh-TW/docs/Glossary/Synchronous) - [非同步](https://developer.mozilla.org/zh-TW/docs/Glossary/Asynchronous) - [非同步的 JavaScript 介紹](https://developer.mozilla.org/zh-TW/docs/Learn/JavaScript/Asynchronous/Introducing) - [解構賦值](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)
js