# 非同步機制

# Promise

一個 Promise 物件可能會處於以下幾種狀態:

  • 擱置(pending):初始狀態,不是 fulfilled 與 rejected
  • 實現(fulfilled):表示操作成功地完成
  • 拒絕(rejected):表示操作失敗了

# Promise.then

# 回傳值

發生於 非同步函式then 的 callback 中:

  • 回傳一個值,則 then 回傳的 promise 會以此值被 實現(resolved)

    Promise.resolve(1)
      .then((value) => value + 1)
      .then((value) => `${value}-This synchronous usage is virtually pointless`)
      .then((value) => console.log(value));
    
    1
    2
    3
    4
  • 拋出一個例外,則 then 回傳的 promise 會以此例外被 否決(rejected)

    Promise.resolve()
      .then(() => {
        // Makes .then() return a rejected promise
        throw 'Oh no!';
      })
      .catch((reason) => {
        console.error('onRejected function called: ', reason);
      });
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 回傳一個被實現的 promise,則 then 回傳的 promise 會是此 被實現的 promise 值

    Promise.resolve('foo').then(function(string) {
      return new Promise(function(resolve, reject) {
        setTimeout(function() {
          string += 'bar';
          resolve(string);
        }, 1);
      });
    });
    
    // [[PromiseState]]: "fulfilled"
    // [[PromiseResult]]: "foobar"
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  • 回傳一個被否決的 promise,則 then 回傳的 promise 會以此值被否決

# Promise.all

同時處理多個非同步,但必須所有傳入的值都是 fulfilled 才會讓 Promise.all() 回傳的 promise fulfilled,否則會 rejected。

function asyncFunc() {
  return Promise.all([fakeFetch(), fakeFetch()]).then(([result1, result2]) => {
    console.log(result1);
    console.log(result2);
  });
}
1
2
3
4
5
6

若改用 async / await 就能這樣使用:


 




async function asyncFunc() {
  const [result1, result2] = await Promise.all([fakeFetch(), fakeFetch()]);
  console.log(result1);
  console.log(result2);
}
1
2
3
4
5

# Promise.allSettled

返回一個所有給定的 promise 都已經 fulfilledrejected 的陣列,其中的每一個物件表示對應的 promise 結果。

通常用於 多個彼此不依賴 的非同步任務。

相比之下,Promise.all() 較適合彼此相互依賴或其中任一個 reject 時立即結束。

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) =>
  setTimeout(reject, 100, 'foo')
);

Promise.allSettled([promise1, promise2]).then((data) =>
  data.forEach((item) => console.log(item))
);

// [
//   {status: 'fulfilled', value: 3}
//   {status: 'rejected', reason: 'foo'}
// ]
1
2
3
4
5
6
7
8
9
10
11
12
13

# Promise.prototype.any

適用於一次處理多個非同步事件,並且抓出第一個 fulfilled 的 promise,其餘 promise 的返回結果就不做處理,只有當全部都 rejected 才會進行錯誤處理。

範例可以 參考連結 (opens new window)

或是想動態引入模組 (import()),但有多個來源可以引入,但只需引入最快的那一個即可:

const lodash = await Promise.any([
  import('https://primary.example.com/lodash'),
  import('https://secondary.example.com/lodash'),
]);
1
2
3
4

# AggregateError

若所有傳入的 Promise 都 rejected,則會以 AggregateError rejected,並會保留所有 rejection reasons。

Promise.any([
  Promise.reject('Oops 1'),
  Promise.reject('Oops 2'),
  Promise.reject('Oops 3'),
]).catch((error) => {
  console.log(error instanceof AggregateError);
  console.log(error.errors);
  console.log(error.message);
  console.log(error.stack);
});

// true
// ["Oops 1", "Oops 2", "Oops 3"]
// All promises were rejected
// AggregateError: All promises were rejected
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Promise.prototype.finally

  • 回傳值永遠是 Promise,而該 promise 的 PromiseState 取決於上一個 promise chain。

    // Promise {<fulfilled>: "OK"}
    Promise.resolve('OK').finally(() => {
      console.log('finally');
    });
    
    // Promise {<rejected>: "Error"}
    Promise.reject('Error').finally(() => {
      console.log('finally');
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • Promise.prototype.finally() 的 callback 沒有 argument

    .then().catch() 的 callback 會有 argument,而該 argument 是在 promise chain 中,前一個 Promise 的 fulfilled 值或 rejected 值。

    Promise.prototype.finally() 的 callback 是沒有 argument 的,若還是寫了 argument,其值也會是 undefined,不管 promise chain 中的前一個 Promise 的 fulfilled 或 rejected。

    Promise.resolve('OK').finally((value) => {
      console.log(value); // undefined
    });
    
    1
    2
    3
  • Promise.prototype.finally() 的 callback 會忽略 return 關鍵字

    回傳的 Promise 的 fulfilled 值或 rejected 值會是 前一個 promise chain 中的結果。

    Promise.resolve('OK')
      .finally(() => {
        console.log('finally...');
        return 'finally';
      })
      .then((value) => {
        console.log(value); // OK
      });
    
    // Promise {<fulfilled>: undefined}
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • Promise.prototype.finally() 的 callback 中使用 throw

    會讓回傳的 Promise rejected。

    Promise.resolve('OK').finally(() => {
      console.log('finally');
      throw new Error('Oops');
    });
    
    // Promise {<rejected>: Error: Oops
    
    1
    2
    3
    4
    5
    6

# Async function & await expression

# Async function 的回傳值永遠是 Promise

return 直接回傳值,回傳值會等同於將值傳入 Promise.resolve()

async function asyncFunc() {
  return 'hi';
}

asyncFunc(); // Promise {<fulfilled>: "hi"}

asyncFunc().then((value) => console.log(value)); // "hi"
1
2
3
4
5
6
7

若沒有回傳值,則等同於回傳 Promise.resolve(undefined)

async function asyncFunc() {
  'hello';
}

asyncFunc(); // Promise {<fulfilled>: undefined}

asyncFunc().then((value) => console.log(value)); // undefined
1
2
3
4
5
6
7

throw 某個值,回傳值會等同於將值傳入 Promise.reject()

async function asyncFunc() {
  throw new Error('Oops');
}

asyncFunc();
// Promise {<rejected>: Error: Oops
//     at asyncFunc (<anonymous>:2:9)
//     at <anonymous>:5:1}

asyncFunc().catch((error) => console.log(error));
// Error: Oops
//     at asyncFunc (<anonymous>:2:9)
//     at <anonymous>:1:1
1
2
3
4
5
6
7
8
9
10
11
12
13

# async / await 的運用

const baseUrl = 'https://jsonplaceholder.typicode.com';

async function fetchJSON(url) {
  try {
    const response = await fetch(url);
    return response.json();
  } catch (error) {
    console.log(error);
  }
}

async function fetchPost(id) {
  const apiURL = `${baseUrl}/posts/${id}`;
  return fetchJSON(apiURL);
}

async function fetchUser(id) {
  const apiURL = `${baseUrl}/users/${id}`;
  return fetchJSON(apiURL);
}

async function main() {
  const postId = 1;
  const post = await fetchPost(postId);

  const userId = post.userId;
  const user = await fetchUser(userId);

  console.log(user.username);
}

main();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 錯誤處理

如果要依順序處理多個非同步,並想 分別處理 錯誤,可以用 .catch() 來實做:


 




async function asyncFunc() {
  const result1 = await fakeFetch().catch((error) => {
    console.log(error.message);
  });
}
1
2
3
4
5

# Array.map 的非同步運用

Array.map 是 JavaScript 陣列裡常使用的方法,而 .map() 方法內,處理函式的機制是 同步的(synchronous),也就是如果我們想在裡面跑非同步的邏輯,是沒辦法等到我們非同步的工作完成。

如果我們想引入非同步邏輯,我們可以這樣做:

const asyncWorker = async (item) => {
  // 非同步的工作,會做一段時間
};

let results = arr.map(async (item) => {
  // 等待非同步工作完成
  await asyncWorker(item);

  return item;
});
1
2
3
4
5
6
7
8
9
10

另外,也可以藉由 for 迴圈達到非同步的效果:

/**
 * 非同步執行 map 方法
 * @param {array} array 陣列資料
 * @param {function} callback 回乎函式
 */
const asyncMap = async (array, callback) => {
  const result = [];
  for (let index = 0; index < array.length; index++) {
    const data = await callback(array[index], index, array);
    result.push(data);
  }
  return result;
};
/**
 * 延遲事件
 * @param {function} callback 回乎函式
 * @param {number} seconds 延遲秒數
 */
const timeout = (callback, seconds) => {
  return new Promise((resolve) =>
    setTimeout(() => {
      callback();
      resolve();
    }, seconds)
  );
};
/**
 * 點擊按紐事件
 */
const clickHandler = async () => {
  // 記得這邊要加上 await 不然會取得滿滿的 promise
  return await asyncMap([1, 2, 3], async (item) => {
    const wait = Math.floor(Math.random() * 10);
    await this.timeout(() => {
      // ...
    }, wait * 1000);
    return item;
  });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
試一試

Get [1,2,3] order by order

    The final data:[]

    # 平行執行,依序列出

    透過 Array.map 不會暫停並等待非同步函式的運行特性,加上 for...of 的等待回應內容,達到平行執行,依序列出。

    const arrayData = [
      { num: 1, time: 500 },
      { num: 2, time: 3000 },
      { num: 3, time: 1500 },
      { num: 4, time: 1000 },
    ];
    
    const promiseFn = (num, time) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(num + 10);
        }, time);
      });
    };
    
    async function parallelFn() {
      const data = arrayData.map(async (item) => {
        // 此行的 await 不會暫停函式運行
        const res = await promiseFn(item.num, item.time);
        return res;
      });
      // 此時的 data 內的 promise 是尚未 resolve 狀態
      console.log(data);
    
      for (const res of data) {
        console.log(await res);
      }
    }
    
    parallelFn();
    // [Promise, Promise, Promise, Promise]
    // 11
    // 12
    // 13
    // 14
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

    # 使用 async / await 動態引入模組

    不同的模組有不同數量的功能函式,如果一同導入會使資源浪費,現在可以使用 async / await 來動態導入這些依賴,但是這個方法僅適用於 node.js 環境

    // math.js
    const add = (num1, num2) => num1 + num2;
    
    export { add };
    
    1
    2
    3
    4
    // index.js
    const doMath = async (num1, num2) => {
      if (num1 && num2) {
        const math = await import('./math.js');
        console.log(math.add(num1, num2));
      }
    };
    
    doMath(5, 10); // 15
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    # 參考

    Async Functions & await expression (opens new window)

    Promise.prototype.finally() (opens new window)

    Promise.allSettled() (opens new window)

    Promise.any() & AggregateError (opens new window)

    async/await 的奇淫技巧 (opens new window)

    当 async/await 遇上 forEach (opens new window)

    JS async/await 系列:延伸運用篇 (opens new window)

    Last Updated: 2021/2/25 上午8:00:30