JavaScript 異步編程的演進

一、 爲什麼需要異步編程?

先說說爲什麼要有異步這回事. JavaScript 是單線程的, 也就是說它一次只能做一件事. 如果所有操作都同步執行, 遇到網絡請求或者文件讀取這種耗時的操作, 頁面就會卡住不動, 用戶體驗直接爆炸. 

// 同步代碼的災難現場
const data = fetchDataFromServer(); // 假設這個請求要3秒
console.log(data); // 這行代碼要傻等3秒才能執行
doSomethingElse(); // 更要再等

所以 JavaScript 採用異步非阻塞的方式處理這類操作: 發起請求後不幹等着, 而是繼續執行後面的代碼, 等請求完成後再來處理結果. 這就引出了我們最原始的處理方式——回調函數. 

二、回調函數的時代

  1. 基礎回調: 
// 最簡單的回調示例
function fetchData(callback) {
  setTimeout(() ={
    callback('數據到手啦!');
  }, 1000);
}
fetchData((data) ={
  console.log(data); // 1秒後輸出"數據到手啦!"
});
  1. 回調地獄: 

當多個異步操作需要順序執行時, 代碼就會變成這樣: 

// 經典回調地獄
getUserInfo(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      getProductInfo(details.productId, function(product) {
        console.log('最終結果:', product);
      });
    });
  });
});

這種代碼的問題: 

  1. 難以閱讀和維護 (金字塔形狀)

  2. 錯誤處理非常麻煩 (要在每個回調裏處理錯誤)

  3. 無法使用 return 和 throw

  4. 無法使用 for 循環和 try/catch

  1. 回調的改進嘗試

2.1 命名函數, 把匿名回調變成命名函數:

getUserInfo(userId, handleUser);
function handleUser(user) {
  getOrders(user.id, handleOrders);
}
function handleOrders(orders) {
  getOrderDetails(orders[0].id, handleDetails);
}
// ...雖然可讀性好點了,但代碼跳來跳去更暈了

2.2 Async.js 庫, 提供 series/parallel/waterfall 等流程控制: 

async.waterfall([
  function getUser(callback) {
    getUserInfo(userId, callback);
  },
  function getOrders(user, callback) {
    getOrders(user.id, callback);
  }
  // ...
]function finalHandler(err, result) {
  // 最終處理
});

這些方案雖然有一定效果, 但都不夠優雅. 直到 Promise 的出現. 

三、Promise

3.1 Promise 直譯就是 "承諾", 它表示一個異步操作的最終完成或失敗. Promise 有三種狀態: 

  1. pending(進行中)

  2. fulfilled(已成功)

  3. rejected(已失敗)

一旦狀態改變就不會再變 (要麼成功要麼失敗).

const promise = new Promise((resolve, reject) ={
  // 異步操作
  setTimeout(() ={
    if (Math.random() > 0.5) {
      resolve('成功數據');
    } else {
      reject(new Error('失敗原因'));
    }
  }, 1000);
});
promise
  .then(data ={
    console.log('成功:', data);
  })
  .catch(err ={
    console.error('失敗:', err);
  });

3.2 Promise 鏈式調用, 這是 Promise 最強大的地方: 

// 用Promise改造回調地獄
getUserInfo(userId)
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetails(orders[0].id))
  .then(details => getProductInfo(details.productId))
  .then(product ={
    console.log('最終結果:', product);
  })
  .catch(err ={
    console.error('出錯啦:', err);
  });

對比之前的回調地獄, 這種鏈式調用: 

  1. 扁平化, 可讀性極佳

  2. 錯誤處理統一 (一個 catch 處理所有錯誤)

  3. 可以使用 return 傳遞數據

  4. 可以在 then 裏 throw error

3.3 Promise 的靜態方法: 

Promise.all: 等所有 Promise 都成功. 

Promise.all([promise1, promise2, promise3])
  .then(([result1, result2, result3]) ={
    // 所有都成功纔會到這裏
  })
  .catch(err ={
    // 任意一個失敗就到這裏
  });

Promise.race:競速, 第一個完成的 Promise 決定結果. 

Promise.race([fetchFromA(), fetchFromB()])
  .then(firstResult ={
    // 使用最先返回的結果
  });

**Promise.allSettled: **等所有 Promise 都完成 (無論成功失敗).

Promise.allSettled([promise1, promise2])
  .then(results ={
    results.forEach(result ={
      if (result.status === 'fulfilled') {
        console.log('成功:', result.value);
      } else {
        console.log('失敗:', result.reason);
      }
    });
  });

3.4 Promise 的侷限性

雖然 Promise 很棒, 但還是有不足: 

  1. 無法取消 Promise.

  2. 錯誤處理雖然比回調好, 但還是要用 catch.

  3. 多個 Promise 之間的數據共享不太方便.

  4. 調試時堆棧信息不友好.

四、aysnc/await

async/await 是現代 JavaScript 異步編程的終極解決方案, 它讓異步代碼看起來和同步代碼一樣直觀, 用起來更簡單: 

async function fetchData() {
  try {
    const user = await getUserInfo(userId);
    const orders = await getOrders(user.id);
    const details = await getOrderDetails(orders[0].id);
    console.log(details);
  } catch (err) {
    console.error(err);
  }
}

這代碼看起來完全是同步的風格, 但實際上是異步執行的!

4.1 async/await 函數的特徵

總是返回 Promise:

  1. 如果返回非 Promise 值, 會自動包裝成 Promise.resolve(值)

  2. 如果拋出異常, 返回 Promise.reject(異常)

await 只能在 async 函數中使用:

  1. 普通函數中使用會報語法錯誤

  2. 現代瀏覽器和 Node.js 的模塊系統中支持頂層 await

4.2 await 解析

4.2.1 await 的行爲

async function example() {
  const result = await somePromise;
  console.log(result);
}

這段代碼實際上做了以下幾件事:

  1. 暫停 example 函數的執行

  2. 等待 somePromise 完成

  3. 如果 Promise 成功, 恢復執行並將結果賦值給 result

  4. 如果 Promise 失敗, 拋出拒絕原因 (可以用 try/catch 捕獲)

4.2.2 await 不一定要等 Promise

雖然設計初衷是處理 Promise, 但 await 可以等待任何值:

async function foo() {
  const a = await 42; // 等價於 await Promise.resolve(42)
  const b = await someFunction(); // 如果someFunction返回非Promise,也會自動包裝
}

4.2.3 錯誤處理機制

await 的錯誤處理非常符合直覺, 可以用傳統的 try/catch: 

async function fetchData() {
  try {
    const data = await fetch('/api');
    const json = await data.json();
    console.log(json);
  } catch (err) {
    console.error('請求失敗:', err);
  }
}

對比 Promise 的. catch(), 這種寫法更接近同步代碼的思維模式. 

五、常見使用方式

5.1 順序執行: 

async function sequential() {
  const user = await getUser();
  const posts = await getPosts(user.id);
  const comments = await getComments(posts[0].id);
  return comments;
}

5.2 並行執行: 

async function parallel() {
  const [user, posts] = await Promise.all([
    getUser(),
    getPosts()
  ]);
  return { user, posts };
}

5.3 循環中的 await: 

// 順序處理數組
async function processArray(array) {
  for (const item of array) {
    await processItem(item);
  }
}
// 並行處理數組
async function processArrayParallel(array) {
  await Promise.all(array.map(item => processItem(item)));
}

5.4 await 可以等待任何 thenable 對象: 

async function example() {
  const result = await {
    then(resolve) {
      setTimeout(() => resolve('done'), 1000);
    }
  };
  console.log(result); // 1秒後輸出'done'
}

5.5 混合使用 Promise 和 async/await: 

async function getUserPosts(userId) {
  return fetchUser(userId)  // 返回Promise
    .then(user => fetchPosts(user.id))
    .then(posts ={
      return asyncFunction(posts); // 在then中調用async函數
    });
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/u1TboKTShXb3v_Zy2j9OzQ