js 非同步事件練習


紀錄一下 javascript 中非同步的寫法範例

非同步範例

01 Promise

Sample Page 01

使用 new Promise(resolve,reject) 去模擬三個事件的執行順序,可以想見若未來還有其他事件要增加,最終的寫法肯定會越來越像氣功波,且難以維護

在事件內返回的是一個 Promise,以便讓後續的 then()語法可以接續下去,setTimeout()是用來模擬 ajax 或是其他需要執行較長時間的程序,在程序結束完畢之後,透過resolve()改變 Promise 的狀態為執行成功,這樣子在外面的執行事件偵測到就會往下執行then()區塊

假設第一個事件是透過 ajax 取得後端資料,取得資料後需要再後續步驟接續處理資料,可以將資料傳入給resolve()函式,這樣子在後續的then()就可以作為輸入參數來使用

// 事件範例
work01(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(ms => {
      let time = new Date();
      resolve({ title, time });
    }, ms);
  });
},

// 執行事件範例
this.work01(3000).then(info => {
  this.showInfo(info);
  this.work02(2000).then(info => {
    this.showInfo(info);
    this.work03(1000).then(info => {
      this.showInfo(info);
    });
  });
});

02 async / await

Sample Page 02

事件方法與先前使用Promise並無不同,但是在執行事件的方法中,需要宣告為async表示此方法為一個非同步方法,並於該方法之內,將需要等候執行完成的方法前端加上await表示此方法需要等候執行結束才可以往下繼續執行,使用語法糖來改寫Promise的最大好處是:程式碼階層數量不再受到事件數量影響,便於閱讀也容易維護、撰寫

// 以 async / await 改寫執行事件
async doJob() {
  const r1 = await this.work01(3000);
  this.showInfo(r1);
  const r2 = await this.work02(2000);
  this.showInfo(r2);
  const r3 = await this.work03(1000);
  this.showInfo(r3);
}
// or 使用inline method 凸顯程式意圖
async doJob() {
  this.showInfo(await this.work01(3000));
  this.showInfo(await this.work02(2000));
  this.showInfo(await this.work03(1000));
}

03 Promise catch error

Sample Page 03

若要加上錯誤處理,則是添加上reject()區段,並於執行事件的方法中加上catch()來捕捉例外

jquery ajax API 返回的就是 promise,不需要再自己包一個

new Vue({
  el: "#app",
  data: {
    infos: [],
  },

  methods: {
    ajaxPromise(params) {
      return $.ajax({
        type: params.type || "get",
        async: params.async || true,
        url: params.url,
        data: params.data || "",
      });
    },
    setData(data) {
      this.infos = data;
    },

    async doJob() {
      this.ajaxPromise({ url: "dataNotExist.json" })
        .then((data) => {
          this.setData(data);
        })
        .catch((err) => {
          console.log(err);
        });
    },
  },
  mounted() {
    this.doJob();
  },
});

04 async / await catch error

Sample Page 04

若改寫成 async / awaitPromise 發生錯誤時會拋出reject()的異常,因此在執行事件中需要透過try...catch...來處理

new Vue({
  el: "#app",
  data: {
    infos: [],
  },

  methods: {
    ajaxPromise(params) {
      return $.ajax({
        type: params.type || "get",
        async: params.async || true,
        url: params.url,
        data: params.data || "",
      });
    },
    setData(data) {
      this.infos = data;
    },

    async doJob() {
      try {
        let data = await this.ajaxPromise({ url: "dataNotExist.json" });
        this.setData(data);
      } catch (error) {
        console.log(error);
      }
    },
  },
  mounted() {
    this.doJob();
  },
});

實際經驗

因為對非同步真的不熟,在實務上也犯了將$.ajax再包一層 promise 卻發現無法正確工作,查了很久才發現這件事情,特別紀錄一下

使用$.ajax語法就已經是回傳非同步的 Promise 了,如果要用來控制程式流程,可以像下面這樣寫

{
  // 只是拿來做console的ID用
  // ref:https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript
  function makeId(length) {
    var result = "";
    var characters =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    var charactersLength = characters.length;
    for (var i = 0; i < length; i++) {
      result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }
    return result;
  }

  // 記得要return,才會將promise回傳出去
  function test(params) {
    let id = makeId(3);
    console.log(`${id} start`);
    return $.ajax({
      url: "http://localhost:40001/mock/8/Common/GetZipCode",
      method: "POST",
    }).then(() => {
      console.log(`${id} end`);
    });
  }

  // 宣告為async,並於回傳的promise方法之前加上 await 修飾詞
  async function init() {
    await test();
    await test();
  }

  init();
}