【JavaScript】淺談 Promise 、async/await 與 callback

瀏覽器在網頁中都是使用同一個執行緒執行 JavaScript 語法當一個功能需要花費較長時間時,非同步的寫法就…

callback function

callback function 是 Javascript 傳統的非同步處理方式
意思就是將一個 function 當成參數傳入,等取得資料或運算完成後
再呼叫 callback function處理後續事項
下面程式我們模擬從遠端伺服器取得資料,取得資料後(等待一秒後),呼叫 callback function 印出資料


function fetchData(callback) {
  setTimeout(() => {
    const data = 'Hello, world!';
    // 在異步操作完成後呼叫回呼函式
    callback(null, data); // 傳遞錯誤參數為 null,資料參數為 data
    // 若要模擬錯誤,可以使用以下程式碼
    // callback(new Error('Failed to fetch data'), null);
  }, 1000);
}

function handleData(error, data) {
  if (error) {
    console.error('Error:', error);
  } else {
    console.log('Data:', data);
	console.log('data Fetched');
  }
}

console.log('Fetching data...');

fetchData(handleData);

console.log('end');

上面程式中,最終瀏覽器 console 印出來的結果順序如下
代表執行緒遇到非同步函式時,並不會等待非同步函式執行完畢,而是直接往下執行
因此,若有需要操作資料的部分,都需要寫在 callback 裡面才能確保資料已取得

Fetching data...
callback.html:25 end
callback.html:16 Data: Hello, world!
callback.html:17 data Fetched


實務上,我們也很少特地定義一個 function 來傳入,通常都是把 function 直接寫再參數內

function fetchData(callback) {
  setTimeout(() => {
    const data = 'Hello, world!';
    // 在異步操作完成後呼叫回呼函式
    callback(null, data); // 傳遞錯誤參數為 null,資料參數為 data
    // 若要模擬錯誤,可以使用以下程式碼
    // callback(new Error('Failed to fetch data'), null);
  }, 1000);
}

function handleData(error, data) {
  if (error) {
    console.error('Error:', error);
  } else {
    console.log('Data:', data);
	console.log('data Fetched');
  }
}

console.log('Fetching data...');

fetchData(fetchData((error, data)=>{
  if (error) {
    console.error('Error:', error);
  } else {
    console.log('Data:', data);
  }
}););

console.log('end');

以上是 callback function 簡單介紹,需注意的是若 callback function 層層堆疊時,會讓排版變的非常混亂,造成 callback hell ,這邊我就不放波動拳的梗圖了

Promise

Promise 是一個用來改善 callback function 寫法的物件,使程式碼更容易閱讀和維護
Promise 物件包含以下幾種屬性

  1. State:Promise 有三種狀態,分別是 pendingfulfilledrejected,剛被建立出來的時候狀態是 pending,隨後,若程式執行成功並 resolve 執行結果,則狀態會變更為 fulfilled,相反的,若程式執行失敗,透過 reject 回傳錯誤訊息,則狀態變更為 rejected
  2. resolve 與 reject:Promise 可以透過 resolve 或 reject 來解析並回傳結果或錯誤訊息
  3. .then():當 Promise 物件使用 resolve 來解析並回傳資料時,則可以透過 .then() 來獲取回傳的資料,相當於把上面callback function的內容移到這裡面來
  4. .catch():當 Promise 物件使用 reject 來解析並回傳錯誤訊息時,則會進入這個區塊,用來處理錯誤維護
  5. .finally():無論是 resolve 或 reject 都會進入的區塊,順序在 .then() 或 .catch() 之後

下面我們就來把上面 callback function 的寫法改為 Promise 寫法


function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = 'Hello, world!';
      // 在異步操作完成後解析 Promise
      resolve(data);
      // 若要模擬錯誤,可以使用以下程式碼
      // reject(new Error('Failed to fetch data'));
    }, 1000);
  });
}

console.log('Fetching data...');

const promise = fetchData();

console.log(promise)

promise
  .then(data => {
    console.log('Data:', data);
    console.log('Data Fetched');
    console.log(promise)
  })
  .catch(error => {
    console.error('Error:', error);
    console.log(promise)
  })
  .finally(() => {
    console.log('end');
  });


console.log('main thread end');

可以看到 fetchData() 改為回傳 Promise 物件
若資料取得成功,則會進入 promise.then 區塊內
而若是將第7行註解起來,第9行打開,則會進入 promise.catch 區塊內
不管是進入 .then 或是 .catch,最終都還會執行 .finally 後才結束程式

依照上面的程式,瀏覽器 print 出來的結果如下,依然可以發現程式並不會等待非同步函式執行結束後才往下走,而是先往下執行,待非同步函式完成後,才回頭去跑 .then()、.catch()、.finally()內的程式

Fetching data...
promise.html:18 Promise {<pending>}
promise.html:35 main thread end
promise.html:22 Data: Hello, world!
promise.html:23 Data Fetched
promise.html:24 Promise {<fulfilled>: 'Hello, world!'}
promise.html:31 end

而如果是將第7行註解,第9行打開,則會顯示下面訊息

Fetching data...
promise.html:18 Promise {<pending>}
promise.html:35 main thread end
promise.html:27 Error: Error: Failed to fetch data
    at promise.html:9:14
(anonymous) @ promise.html:27
Promise.catch (async)
(anonymous) @ promise.html:26
promise.html:28 Promise {<rejected>: Error: Failed to fetch data
    at file:///C:/Users/Vic/Desktop/promise.html:9:14}
promise.html:31 end

async/await

從 promise 又延伸出 asyncawait 的兩個新特性,其實本質上是更簡便的語法糖。

  • async:宣告一個函式是非同步函式( async function ),並且這個函式所回傳的一定是 Promise 物件,非同步函式內部可以使用 await 關鍵字,當遇到 await 時,將暫停執行,等待 Promise 回傳結果( resolved 或 rejected ) 後,才繼續執行後續的程式碼。
  • await:用來等待 Promise 物件完成。可以放在非同步函式內,並且可以將 Promise 的結果賦值給一個變數

下面我們來看看將上面程式改寫為 async/await 後的寫法


function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = 'Hello, world!';
      // 在異步操作完成後解析 Promise
      resolve(data);
      // 若要模擬錯誤,可以使用以下程式碼
      // reject(new Error('Failed to fetch data'));
    }, 1000);
  });
}

async function main() {
  try {
    console.log('Fetching data...');
    const data = await fetchData();
    console.log('Data:', data);
    console.log('Data Fetched');
  } catch (error) {
    console.error('Error:', error);
  } finally {
    console.log('end');
  }
}

main();

console.log('main thread end');

下面來看看這個範例,瀏覽器 print 出內容的順序為何

Fetching data...
async.html:29 main thread end
async.html:18 Data: Hello, world!
async.html:19 Data Fetched
async.html:23 end

與上面大同小異,印完 Fetching data… 後,因為函式內需暫停一秒,所以執行緒就先跳出往下執行 main thread end,待取得 17 行的結果後,才執行 18、19 行程式。

需注意的是,因為 main() 被宣告為 async,async function 一定會回傳一個 promise 物件,實務上能夠使用 promise = main() 來承接,接著在透過 .then() 之類的方式來處理後續。

以上是常見的幾種 JavaScript 處理非同步 function 的寫法,記錄下來供未來的自己參考

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *