使用 Promise.all() 解決多次的 API Callback
在 JavaScript 中蠻容易遇見像這樣 callback hell 的程式碼:
像這樣巢狀非同步的程式碼,對人腦來說真的很不友善,再後來學會了 Promise、Async / Await 來避免這樣寫出這樣過度巢狀化的程式碼。
Google Maps Service
這陣子正在寫一個 telegram bot,用到了 Google Maps 的服務,主要是為了用來計算兩點之間的距離,要使用這個服務,首先你需要到 Google API Console 去建立一個新專案:
接著點擊資料庫去選擇你需要的 API 服務:
完成後再點擊憑證的地方去複製你的 API 金鑰就可以使用 Google 提供的 API 服務了。
API 的 Callback
我們在 Node.js 中,使用 fs 來讀取檔案時,會看到如下的程式碼:
fs.readFile(file, [options], callback);
在 Node.js 的核心模組,通常在 callback 的部份我們都會優先處理錯誤:
fs.readFile('someFile.md', (err, data) => {if (err) throw err;console.log(data);});
這次開發 telegram bot 因為使用了 Google Maps 服務因此選了一個 google-maps-service-js 的 library 來處理 Google Maps 相關的計算,因為要計算兩點的距離,根據官方的文件,我們必須使用 distanceMatrix
這個方法來計算:
client.distanceMatrix({origins, destinations}, (err, response) => {if (err) {console.log(err);return;};const { rows } = response.json;const { distance: { text } } = rows[0]['elements'][0];console.log(text);});
這個部份我們可以得到兩點計算後的距離,然後把 response.json
解構賦值,我們可以得到兩點之間的距離到底是多少(text 是一個 String,ex: 5.0 km)。
這樣看起來好像順利可以取得距離了,但是像在 JavaScript 會有 scope 的問題,有些值你無法把他 return 出來時,就會用到 callback 了,上方的程式碼在 callback 的地方是一個 Anonymous Function,所以就算 return 了值,也無法取得,如果透過 callback 方式:
const client = require('@google/maps').createClient({key: 'your API key here'});const origins = { lat: 0123456, lng: 9987654 }; // `origins` and `position` just a exampleconst position = { lat: 123456, lng: 7890123 };function callbackDistance(origins, position, callback) {client.distanceMatrix({origins, destinations}, (err, response) => {if (err) return callback(err);const { rows } = response.json;const { distance: { text } } = rows[0]['elements'][0];callback(null, text);});}callbackDistance(googleMapsClient, origins, position, (err, data) => {if (err) throw err;console.log(data);});
這裡的 callback 遵循了 Node.js 的 callback function style 優先處理錯誤,但是後來我發現透過 callback 的方式來取得值的方式無法滿足我的需求,今天假設我對這個 distanceMatrix 做了多次的 request,然後得到多個值儲存成陣列再做計算的話呢?callback 好像讓事情變得更難處理呢!
嘗試使用 Promise
因為以上的問題,我後來嘗試使用了 Promise,這也讓程式看起來像是同步的感覺。
const fake = {origins: { lat: 0123456, lng: 9987654 },destinations: { lat: 123456, lng: 7890123 }};function promiseDistance(point) {return new Promise((resolve, reject) => {const { origins, destinations } = point;client.distanceMatrix({origins, destinations}, (err, response) => {if (err) return reject(err);const { rows } = response.json;const { distance: { text } } = rows[0]['elements'][0];resolve(text);});});}
我們可以透過這樣的方式來取得距離:
promiseDistance(somePoint).then(res => console.log(res));
乾淨!相對於 callback 讓人感覺舒服多了,接下來我們來處理多筆資料:
const stores = [{"id": "4fe0da0f-fd90-4c17-83ae-2806d6daff28","latitude": "22.66054200","longitude": "120.51543900"},{"id": "7b2f0e7a-e138-47f7-b10f-9a04d4bd343a","latitude": "22.00505500","longitude": "120.74336830"},{"id": "81348d86-844b-47a2-b349-d492e56eee34","latitude": "22.67599980","longitude": "120.50244030"}];let point = {origins: { lat: 121.512386, lng: 25.051269 };};// newStores is => [...Promise { <pending> }]const newStores = stores.map(store => {point.destinations = { lat: store['latitude'], lng: store['longitude'] };return promiseDistance(point).then(distance => distance);});
這時候我們有一個 stores 含有多筆經緯度的資料,而我們有個原點,把原點跟每個經緯度來做計算得到距離,在 stores.map
的部份可以看到我每次回傳的都是一個 Promise,promiseDistance(point) 把我們的點傳入進去,接著會得到 API 會傳回來的結果,這個地方是非同步,可以透過 console.log(newStores) 知道這個回傳陣列內的值都是一個 Promise {<pending>}
的狀態。
使用 Promise.all() 處理 […Promise { <pending> }]
在這個部份我本來也不知道該如何解決的,於是在 MDN 翻了 Promise 相關的文件找到了這個方法法 — Promise.all()。
Promise.all(iterable) 傳入的值要是一個 iterable object,像是陣列就是一個 iterable object(更多 iterable 請參考 — MDN iterable),當你傳入的 iterable 內的 Promise 都為 resolved ,Promise.all 的 狀態就會轉為 resolved,反之假設其中一個為 rejected 的狀態,Promise.all 則會轉為 rejected 的狀態。
剛剛 nextStores 都是狀態在 pending 的 Promise,接著我們把它放入 Promsie.all(),來等待這些 pending 的狀態轉為 resolved or rejected,這樣我們就可以對 API 做多次的呼叫,Promise.all 會等待所有個 Promise 都完成狀態轉化後才會在繼續執行 .then 或 .catch,感覺就像在寫同步的程式碼,感覺超棒的!
// newStores: https://gist.github.com/neighborhood999/d030dd4d3422681fc3266753a5e143fcPromise.all(newStores).then(results => {const nextStores = results.map(store => {// do something});return nextStores;.then(nextStores => {// do something}).catch(err => console.log(err);});