iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 13
0
Modern Web

《你的地圖會說話? WebGIS與JavaScript的情感交織》系列 第 13

[5-2] Callback & Promise - 解決request非同步的四種解法

  • 分享至 

  • twitterImage
  •  

本系列文章已出版實體書籍:
「你的地圖會說話?WebGIS 與 JavaScript 的情感交織」(博碩文化)
WebGIS啟蒙首選✖五家地圖API✖近百個程式範例✖實用簡易口訣✖學習難度分級✖補充ES6小知識


本篇文章請搭配
[5-1] 環域與繪圖工具 - 以Leaflet Draw實現


延續昨天講的繪圖工具,當繪圖完成後會觸發回調函式,
因此我們繪圖完後續要做的動作都可以寫在這個callback function。

今天我們想要在繪圖完成後做幾件事情:

↓ 依照需求稍微修改了昨天繪圖事件發生的函式

        LMap.on(L.Draw.Event.CREATED, function (e) {
            var data = {};
            var center, radius, marker;
            var layer = e.layer;
            var type = e.layerType;
            drawItem.addLayer(layer);

            if (type === 'circle') {
                center = layer.getLatLng();
                radius = layer.getRadius();
                marker = L.marker([center.lat, center.lng]).addTo(LMap);

                axios.get('https://api.nlsc.gov.tw/other/TownVillagePointQuery/120.698659/24.156250/4326')
                    .then((response) => {
                        data = response.data;
                    }).catch((err) => {
                        console.log('錯誤:', err);
                    });


                marker.bindPopup(`
                            <p>經度: ${center.lng.toFixed(6)}</p> 
                            <p>緯度: ${center.lat.toFixed(6)}</p>
                            <p>半徑: ${radius} (m)</p>
                            <p>縣市: ${data.ctyName} </p>
                            <p>鄉鎮市區: ${data.townName} </p>
                            <p>村里: ${data.villageName} </p>
                            <p>地段: ${data.sectName} </p>
                        `);
            }
        });

繪圖完成後建立L.marker,並且用axios呼叫API,
呼叫完API後用bindPopup方法建立資訊視窗。
看似一切都很合理。才怪

https://ithelp.ithome.com.tw/upload/images/20200928/20130604f598zg9om3.jpg
雖然經緯度及半徑都有正常顯示。可是行政區資訊怎麼通通undefuned!/images/emoticon/emoticon04.gif
因為我們axios尚未取得到data資料時,bindPopup就已經先執行。
JavaScript是一種非同步的程式語言,也就是說,
並非像其他程式語言依照程式一行一行執行,並且在上一行執行完時才會執行下一行。
JS執行程式時,會先將每段程式加入任務序列,並且在任務序列排定其執行優先順序,
而舉凡發出request的動作執行順序都排在很後面(Ex: Ajax、Fetch、Axios),
因此必須依靠其他手段來讓程式建立先後順序。

  • 方法一、 寫在axios提供的回調函式中

axios做為支援promise語法的函式庫,把要做的事情寫在then之中準沒錯!

axios.get('https://api.nlsc.gov.tw/other/TownVillagePointQuery/120.698659/24.156250/4326')
                    .then((response) => {
                        data = response.data;
                        marker.bindPopup(`
                            <p>經度: ${center.lng.toFixed(6)}</p> 
                            <p>緯度: ${center.lat.toFixed(6)}</p>
                            <p>半徑: ${radius} (m)</p>
                            <p>縣市: ${data.ctyName} </p>
                            <p>鄉鎮市區: ${data.townName} </p>
                            <p>村里: ${data.villageName} </p>
                            <p>地段: ${data.sectName} </p>
                        `);
                    }).catch((err) => {
                        console.log('錯誤:', err);
                    });

除了使用XMLHttpRequest的原始人類以外,不論是ajax、fetch、axios都適用!
不過這個方法有講跟沒講一樣,寫在人家寫好的回調函式中誰不會?
可如果今天後續要做的事情又臭又長有100行,難道要讓這段request寫100行?
如此一來,就必須想方法把其他動作寫成function分離出去。

  • 方法二、 setTimeout

疑疑?跟setTimeout有什麼關係?可別小瞧這個毫不起眼的語法!
只要把執行順序較早的人,設定固定時間讓他延後執行就好啦!

       LMap.on(L.Draw.Event.CREATED, function (e) {
            var data = {};
            var center, radius, marker;
            var layer = e.layer;
            var type = e.layerType;
            drawItem.addLayer(layer);

            if (type === 'circle') {
                center = layer.getLatLng();
                radius = layer.getRadius();
                marker = L.marker([center.lat, center.lng]).addTo(LMap);


                console.time('Timing');
 axios.get('https://api.nlsc.gov.tw/other/TownVillagePointQuery/120.698659/24.156250/4326')
                    .then((response) => {
                        data = response.data;
                    }).catch((err) => {
                        console.log('錯誤:', err);
                    });
                console.timeEnd('Timing');

                setTimeout(function () {
                    marker.bindPopup(`
                            <p>經度: ${center.lng.toFixed(6)}</p> 
                            <p>緯度: ${center.lat.toFixed(6)}</p>
                            <p>半徑: ${radius} (m)</p>
                            <p>縣市: ${data.ctyName} </p>
                            <p>鄉鎮市區: ${data.townName} </p>
                            <p>村里: ${data.villageName} </p>
                            <p>地段: ${data.sectName} </p>
                        `);
                }, 10);
            }
        });

↑ 使用setTimeout讓bindPopup方法等候10毫秒執行,
可以使用console.time、console.timeEnd來偵測實際執行的時間來決定等候時間。

https://ithelp.ithome.com.tw/upload/images/20200928/20130604CaoGrZYftF.jpg
↑ 畫多個圓
https://ithelp.ithome.com.tw/upload/images/20200928/20130604p8TAJCMkBw.jpg
↑ 實際執行時間

  • 方法三、 建立callback function參數

一直用別人的callback function,會不會想自己寫寫看回調函式?
就讓我們把每個動作都寫成function並且用callback去呼叫吧!

         var GetDistrictData = (x, y, radius, callback = function () { }) => {
            let showData = {};

            axios.get(`https://api.nlsc.gov.tw/other/TownVillagePointQuery/${x}/${y}/4326`)
                .then((response) => {
                    let data = response.data;

                    showData["經度"] = x.toFixed(6);
                    showData["緯度"] = y.toFixed(6);
                    showData["半徑"] = `${radius.toFixed(2)} (m)`;
                    showData["縣市"] = data.ctyName || '查無縣市';
                    showData["鄉鎮市區"] = data.townName || '查無鄉鎮市區';
                    showData["村里"] = data.villageName || '查無村里';
                    showData["地段"] = data.sectName || '查無地段';

                    callback(showData);

                }).catch((err) => {
                    console.log('錯誤:', err);
                    callback({ errorMessage: err });
                });

            return showData;
        }

↑ 建立一個GetDistrictData函式,目的是發出request取得資料並處理回傳資料,
這個function不處理後續跟UI相關的動作。
callback函式做為參數傳入function,並在函式中axios呼叫完才執行。

        var SetInfoWindow = (marker, data = {}) => {
            let field = Object.keys(data);
            let str = "";
            field.forEach((item) => {
                str += `<p>${item}: ${data[item]} </p>`;
            });
            marker.bindPopup(str);
        }

↑ 建立一個SetInfoWindow函式,目的是把marker新建資訊視窗,
並且用Object.keys方式,綁上data物件中的所有屬性值。

        LMap.on(L.Draw.Event.CREATED, function (e) {
            var data = {};
            var center, radius, marker;
            var layer = e.layer;
            var type = e.layerType;
            drawItem.addLayer(layer);

            if (type === 'circle') {
                center = layer.getLatLng();
                radius = layer.getRadius();
                marker = L.marker([center.lat, center.lng]).addTo(LMap);

                data = GetDistrictData(center.lng, center.lat, radius, function (thisData) {
                    SetInfoWindow(marker, thisData);
                });
            }
        });

↑ 將繪圖事件改為如上。先呼叫GetDistrictData並且在其callback function傳入SetInfoWindow。
建立callback function參數的方式雖然好用。然而,當有許多複雜的邏輯順序時,就必須寫很多callback function,一層又一層的巢狀結構被建立,形成回呼地域(callback hell)。

        A吃前菜(function () {
            B喝湯(resultA, function (resultB) {
                C吃麵包(resultB, function (resultC) {
                    D吃牛排(resultC, function (resultD) {
                        E吃甜點(resultD, function (resultE) {
                            F喝飲料(resultE, function (resultF) {
                                G結帳(resultF, function (resultG) {
                                    console.log(resultG);
                                });
                            });
                        });
                    });
                });
            });
        });

↑ callback hell

  • 方法四、 ES6 Promise

ES6新增了Promise物件可以達到callback的效果,並且解決了callback的問題。

  • 可靠性。Promise一經回傳的東西就無法被更改,解決了callback的參數可以被竄改的問題。
  • 寫法直觀。用then語法取代callback hell的巢狀結構。
  • 控制權回歸。每個resolve及reject最多只會被呼叫一次,避免多次呼叫造成的錯誤。
        var GetDistrictData = (x, y, radius) => {
            return new Promise(function (resolve, reject) {
                let showData = {};
                axios.get(`https://api.nlsc.gov.tw/other/TownVillagePointQuery/${x}/${y}/4326`)
                    .then((response) => {
                        let data = response.data;

                        showData["經度"] = x.toFixed(6);
                        showData["緯度"] = y.toFixed(6);
                        showData["半徑"] = `${radius.toFixed(2)} (m)`;
                        showData["縣市"] = data.ctyName || '查無縣市';
                        showData["鄉鎮市區"] = data.townName || '查無鄉鎮市區';
                        showData["村里"] = data.villageName || '查無村里';
                        showData["地段"] = data.sectName || '查無地段';

                        resolve(showData);

                    }).catch((err) => {
                        console.log('錯誤:', err);
                        reject({ errorMessage: err })
                    });
            });
        }

↑ 將GetDistrictData函式改為return一個Promise物件

LMap.on(L.Draw.Event.CREATED, function (e) {
            var data = {};
            var center, radius, marker;
            var layer = e.layer;
            var type = e.layerType;
            drawItem.addLayer(layer);

            if (type === 'circle') {
                center = layer.getLatLng();
                radius = layer.getRadius();
                marker = L.marker([center.lat, center.lng]).addTo(LMap);

                GetDistrictData(center.lng, center.lat, radius)
                    .then((thisData) => SetInfoWindow(marker, thisData));
            }
        });

↑ 將繪圖事件改為如上。呼叫完GetDistrictData後,用then語法繼續接下來的動作。

https://ithelp.ithome.com.tw/upload/images/20200928/20130604nUeQeaxWGu.jpg
↑ 成功秀出行政區資訊。


今天簡單介紹了幾種方法解決request非同步的問題,
大家有沒有開始著手寫Promise了呢?
趕快回去把公司的程式全部重構成Promise吧!
結果越改頭越痛!/images/emoticon/emoticon03.gif


上一篇
[5-1] 環域與繪圖工具 - 以Leaflet Draw實現
下一篇
[5-3] 點線面的接口 - 以配接器模式 Adapter Design Pattern 重構
系列文
《你的地圖會說話? WebGIS與JavaScript的情感交織》30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言