iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 8
5
Modern Web

JavaScript 之旅系列 第 8

JavaScript 之旅 (8):Promise.prototype.finally()

  • 分享至 

  • xImage
  •  

本篇介紹 ES2018 (ES9) 提供的 Promise.prototype.finally()

下面是幾個非同步處理很常見的情境:

  • 進入某頁面時,會立即發 AJAX request,在拿到 response 之前都會顯示「正在載入...」的訊息,不管是拿到 response,還是發生錯誤,都會隱藏「正在載入...」
  • 不管某個操作是否完成,都要紀錄 log
  • 建立 DB 連線來搜尋資料時,不管是成功拿到,還是中途出現錯誤,都要關閉連線釋放資源

以上情境都有一個共通點:不管做什麼事,最後都要做某件事。

也許你會想到 try-finally,會希望非同步處理的 Promise 上也有 finally 的功能 (我自己是沒想過啦 XD),這就是今天要介紹的 Promise.prototype.finally()

在過去原生的 Promise 沒有提供 finally 功能時,很多 library 都在非同步處理的 API 上實作了 finally() 方法,此方法是用來註冊一個在 promise settled 時 (即 fulfilled 或 rejected) invoke 用的 callback。

更多 library 的實作可參閱:

在 ES2018 (ES9) 提供了 Promise.prototype.finally() 新的 Promise method。當 promise settled 時 (即 fulfilled 或 rejected),會執行指定的 callback。

Promise Chain

先說明什麼是 promise chain,因為之後會常常看到這個專有名詞。

將多個 Promise 串在一起,以表達一個序列的非同步執行步驟,而這個序列就是 promise chain。

那為何是 chain?因為每次在 Promise 上呼叫 .then().catch().finally()Promise method 時,都會建立並回傳新的 Promise。例如:

Promise.resolve('OK')
  .then(result => {
    console.log(result);
    return Promise.resolve('Hi')
  })
  .then(result => {
    console.log(result);
    return Promise.reject('Oops');
  })
  .catch(error => {
    console.log(error);
  })
  .finally(() => {
    console.log('finally');
  });

// OK
// Hi
// Oops
// finally

Promise.prototype.finally() 的回傳值永遠是 Promise

Promise.prototype.finally() 的回傳值永遠是 Promise 物件,該 promise 可能會 fulfilled 或 rejected,那何時會 fulfilled?還是會 rejected?

先講結論:要看你的 promise chain 是怎麼寫的

  • .finally() 的前一個 Promise 是 fulfilled,那 .finally() 回傳的 Promise 就會是 fulfilled
  • .finally() 的前一個 Promise 是 rejected,那 .finally() 回傳的 Promise 就會是 rejected

先看幾個範例:

假設我先執行 Promise.resolve('OK'),該 promise 會立即 fulfilled,將 OK 傳給 .then() 的 callback,所以第一個輸出訊息會是 OK,接著執行 .finally(),並將 .finally() 的回傳值存在一個名為 promiseA 的變數:

let promiseA = Promise.resolve('OK')
  .then(result => {
    console.log(result);
  })
  .finally(() => {
    console.log('finally');
  });

// OK
// finally

接著印出 promiseA,它是一個 Promise 物件,該 promise 已經 fulfilled 了,且 fulfilled 的值為 undefined

console.log(promiseA);
// Promise {<fulfilled>: undefined}

那為何 fulfilled 的值會是 undefined,因為在 promise chain 中,.finally() 的前一個 Promise 是 .then() 回傳的,而 .then() 的 callback 沒有回傳值,所以才會是 undefined

所以不要搞錯了,promiseA 存的不是 Promise.resolve('OK') 回傳的 Promise 物件,而是最後一個 promise chain 的。

那再看下一個範例,這次拿到 .then() 這個步驟,一樣將的回傳值存起來,存在一個名為 promiseB 的變數:

let promiseB = Promise.resolve('OK')
  .finally(() => {
    console.log('finally');
  });

// finally

接著印出 promiseB,該 promise 一樣已經 fulfilled 了,但這次 fulfilled 的值是 OK

console.log(promiseB);
// Promise {<fulfilled>: "OK"}

為什麼會是 OK?因為在 promise chain 中,.finally() 的前一個 Promise 是 Promise.resolve('OK') 回傳的,該 Promise fulfilled 的值就是 OK,所以才會是 OK

所以就如同前面結論說的,.finally() 回傳的 Promise 是 fulfilled 還是 rejected,是依據 promise chain 中前一個 Promise 來決定的。

Promise.prototype.finally() 的 callback

Promise.prototype.finally() 的 callback 沒有 argument

.then().catch() 的 callback 會有 argument,而該 argumemt 是在 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
Promise.reject('Oops')
  .finally(value => {
    console.log(value);
  });

// undefined

Promise.prototype.finally() 的 callback 會被忽略 return

Promise.prototype.finally() 的 callback 中的 return 會被忽略,但回傳的 Promise 的 fulfilled 值或 rejected 值會是 promise chain 中,前一個 Promise 的 fulfilled 值或 rejected 值。:

例如:Promise.resolve('OK') 會立即 fulfilled,接著在 .finally()return 會被忽略,但 .finally() 回傳的 Promise 的 fulfilled 值會跟 Promise.resolve('OK') 回傳的 fulfilled 值相同。

Promise.resolve('OK')
  .finally(() => {
    console.log('finally...');
    return 'finally';
  })
  .then(value => {
    console.log(value);
  });

若拆開 promise chain 就會更容易看出來:

let promiseA = Promise.resolve('OK');
console.log(promiseA);
// Promise {<fulfilled>: "OK"}


let promiseB = promiseA.finally(() => {
  console.log('finally...');
  return 'finally';
});
// finally...

console.log(promiseB);
// Promise {<fulfilled>: "OK"}


let promiseC = promiseB.then(value => {
  console.log(value);
});
// OK

console.log(promiseC);
// Promise {<fulfilled>: "undefined"}

promise rejected 的情況也一樣,你可以試著將上面的 Promise.resolve('OK') 改成 Promise.reject('Oops') 觀察看看。

Promise.prototype.finally() vs. finally clause

先來看兩者的寫法。

下面是 Promise.prototype.finally() 的用法:

Promise.resolve('OK')
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.log('error');
  })
  .finally(() => {
    console.log('finally');
  });

// OK
// finally

而下面是 try 陳述句中 finally clause 的用法:

try {
  console.log('OK');
} catch (error) {
  console.log('error');
} finally {
  console.log('finally');
}

// OK
// finally

兩者有些地方很相識,但用法和行為都不同,下面會提出它們的不同之處。

return

Promise.prototype.finally() 會回傳 Promise,該 Promise 可能會 fulfilled 或 rejected (前面有說明)。

finally 只是 try 陳述句中的 clause,若在 finally clause 內 return 某個值會成為 function 的回傳值。

例如:在 func() 函數中,finally clause 內 returnfunc 就成為此函數的回傳值:

function func() {
  try {
    console.log('try');
  } catch (error) {
    console.log('catch');
  } finally {
    console.log('finally');
    return 'func';
  }
}

let result = func();
// try
// finally

console.log(result);
// "func"

throw

若在 finally clause 內使用 throw,需要讓另一個 try-catch 來捕捉錯誤:

function func() {
  try {
    console.log('try');
  } finally {
    console.log('finally');
    throw new Error('Oops');
  }
}

try {
  func();
} catch(error) {
  console.log(error);
}

// try
// finally
// Error: Oops
//     at func (<anonymous>:6:11)
//     at <anonymous>:11:3

而在 Promise.prototype.finally() 的 callback 中使用 throw,會讓回傳的 Promise rejected:

let promiseA = Promise.resolve('OK')
  .then(result => {
    console.log(result);
  });

// OK

console.log(promiseA);
// Promise {<fulfilled>: undefined}


let promiseB = promiseA.finally(() => {
    console.log('finally');
    throw new Error('Oops');
  });

// finally
// Uncaught (in promise) Error: Oops
//     at <anonymous>:3:11
//     at <anonymous>

console.log(promiseB);
// Promise {<rejected>: Error: Oops
//     at <anonymous>:3:11
//     at <anonymous>}

一定會執行的 finally

Promise.prototype.finally()finally clause 的其中一個共通點就是一定會執行。

finally clause 一定會在最後執行

先來說明 finally clause。

在函數內的 try clause 或 catch clause 裡面 return 某個值,函數會在回傳該值之前,先執行 finally clause 內的程式碼 (所以 finally 就如其名,真的是「最後」)。

例如:在 try clause 內 return 值,不會在回傳後直接結束此函數的執行,而是會在回傳之前先執行 finally clause 內的程式碼:

function func() {
  try {
    console.log('try');
    return 'func';
  } catch (error) {
    console.log('catch');
  } finally {
    console.log('finally');
  }
}

console.log(func());
// try
// finally
// "func"

另一個範例:在 catch clause 內 return 值,不會在回傳後直接結束此函數的執行,而是會在回傳之前先執行 finally clause 內的程式碼:

function func() {
  try {
    console.log(data);
  } catch (error) {
    console.log('catch');
    return 'func';
  } finally {
    console.log('finally');
  }
}

console.log(func());
// catch
// finally
// "func"

不管是 fulfilled 或 rejected,Promise.prototype.finally() 的 callback 都會執行

不管 Promise 是 fulfilled 或 rejected 都會執行 Promise.prototype.finally() 內的 callback。

例如:Promise.resolve('OK') 會回傳的 promise 立即 fulfilled 後,會執行 .finally() 的 callback:

let promiseA = Promise.resolve('OK')
  .finally(() => {
    console.log('finally');
  });

// finally

console.log(promiseA);
// Promise {<fulfilled>: "OK"}

另一個例子:Promise.reject('Oops') 會回傳的 promise 立即 rejected 後,會執行 .finally() 的 callback:

let promiseB = Promise.reject('Oops')
  .finally(() => {
    console.log('finally');
  });

console.log(promiseB);
// finally

情境範例

前面提到一些情境,就拿其中一個作為範例。

假設進入某頁面時,會立即發 AJAX request,在拿到 response 之前都會顯示「正在載入...」的訊息,不管是拿到 response,還是發生錯誤,都會隱藏「正在載入...」。

範例程式碼如下:

let isLoading = true;
let JSON_API = 'https://jsonplaceholder.typicode.com/posts/1';
let HTML_API = 'https://developer.mozilla.org/en-US/docs/Web';

function fetchData(url) {
  return fetch(url)
    .then(response => {
      console.log('isLoading:', isLoading);

      const contentType = response.headers.get('Content-Type');
      if (contentType?.includes('application/json')) {
        return response.json();
      } else {
        throw new TypeError(`Oops, we haven't got JSON!`);
      }
    })
    .then(json => {
      console.log('Success');
      return json;
    })
    .catch(error => {
      console.log(error);
    })
    .finally(() => {
      isLoading = false;
      console.log('isLoading:', isLoading);
    });
}

// 試著換成 fetchData(HTML_API)
fetchData(JSON_API).then(data => {
  console.log(data);
});

之後會提到 ?. Optional Chaining 運算子。

若 API 的 Content-Typeapplication/json (即 fetch(JSON_API) 這個 AJAX response),promise 就會 fulfilled 列印出 API 資料,並且在 finally 時將 isLoading 設為 false,所以輸出如下:

fetchData(JSON_API).then(data => {
  console.log(data);
});

// isLoading: true
// Success
// isLoading: false
// {userId: 1, id: 1, title: "..."}

若 API 的 Content-Type 不是 application/json (即 fetch(HTML_API) 這個 AJAX response),promise 就會 rejected 列印出錯誤訊息,並且在 finally 時將 isLoading 設為 false,所以輸出如下:

fetchData(HTML_API).then(data => {
  console.log(data);
});

// isLoading: true
// TypeError: Oops, we haven't got JSON!
// isLoading: false
// undefined

因為不管是 .then().catch() 都要執行 isLoading = false,那更好的作法就是統一在 .finally() 執行 isLoading = false,這樣就不用寫重複的邏輯了。

若上面的範例改用 async / await 的寫法也許會像這樣:

let isLoading = true;
let JSON_API = 'https://jsonplaceholder.typicode.com/posts/1';
let HTML_API = 'https://developer.mozilla.org/en-US/docs/Web';

async function fetchData(url) {
  try {
    const response = await fetch(url);
    console.log('isLoading:', isLoading);

    const contentType = response.headers.get('Content-Type');
    if (contentType?.includes('application/json')) {
      console.log('Success');
      return response.json();
    } else {
      throw new TypeError(`Oops, we haven't got JSON!`);
    }
  } catch(error) {
    console.log(error);
  } finally {
    isLoading = false;
    console.log('isLoading:', isLoading);
  }
}

資料來源


上一篇
JavaScript 之旅 (7):Async Functions & await (2)
下一篇
JavaScript 之旅 (9):RegExp 的 s (dotAll) flag
系列文
JavaScript 之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言