Chức năng không đồng bộ: đưa ra lời hứa thân thiện

Hàm không đồng bộ cho phép bạn viết mã dựa trên lời hứa như thể mã đó là đồng bộ.

Jake Archibald
Jake Archibald

Các hàm không đồng bộ được bật theo mặc định trong Chrome, Edge, Firefox và Safari, và thật sự là rất tuyệt vời. Các phương thức này cho phép bạn viết mã dựa trên lời hứa như thể mã đó đồng bộ, nhưng không chặn luồng chính. Các hàm này giúp mã không đồng bộ của bạn ít "thông minh" hơn và dễ đọc hơn.

Hàm không đồng bộ hoạt động như sau:

async function myFirstAsyncFunction() {
  try {
    const fulfilledValue = await promise;
  } catch (rejectedValue) {
    // …
  }
}

Nếu sử dụng từ khoá async trước định nghĩa hàm, thì bạn có thể sử dụng await trong hàm. Khi bạn await một lời hứa, hàm sẽ tạm dừng theo cách không chặn cho đến khi lời hứa được thực hiện. Nếu lời hứa được thực hiện, bạn sẽ nhận lại giá trị. Nếu lời hứa từ chối, giá trị bị từ chối sẽ được gửi đi.

Hỗ trợ trình duyệt

Hỗ trợ trình duyệt

  • Chrome: 55.
  • Edge: 15.
  • Firefox: 52.
  • Safari: 10.1.

Nguồn

Ví dụ: ghi nhật ký một lần tìm nạp

Giả sử bạn muốn tìm nạp một URL và ghi lại phản hồi dưới dạng văn bản. Dưới đây là cách sử dụng lời hứa:

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

Và sau đây cũng là cách tương tự khi sử dụng các hàm không đồng bộ:

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log('fetch failed', err);
  }
}

Số dòng vẫn như cũ, nhưng tất cả lệnh gọi lại đều biến mất. Điều này giúp bạn dễ đọc hơn, đặc biệt là những người ít quen thuộc với các lời hứa.

Giá trị trả về không đồng bộ

Các hàm không đồng bộ luôn trả về một lời hứa, cho dù bạn có sử dụng await hay không. Lời hứa đó sẽ phân giải bằng bất kỳ giá trị nào mà hàm không đồng bộ trả về hoặc từ chối bằng bất kỳ giá trị nào mà hàm không đồng bộ gửi. Vì vậy, với:

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

…lệnh gọi hello() trả về một lời hứa thực hiện bằng "world".

async function foo() {
  await wait(500);
  throw Error('bar');
}

…lệnh gọi foo() trả về một lời hứa từ chối bằng Error('bar').

Ví dụ: truyền trực tuyến phản hồi

Lợi ích của các hàm không đồng bộ sẽ tăng lên trong các ví dụ phức tạp hơn. Giả sử bạn muốn truyền trực tuyến một phản hồi trong khi ghi lại các đoạn và trả về kích thước cuối cùng.

Dưới đây là mã nguồn có sử dụng promise:

function getResponseSize(url) {
  return fetch(url).then((response) => {
    const reader = response.body.getReader();
    let total = 0;

    return reader.read().then(function processResult(result) {
      if (result.done) return total;

      const value = result.value;
      total += value.length;
      console.log('Received chunk', value);

      return reader.read().then(processResult);
    });
  });
}

Hãy xem tôi, Jake "người thực hiện lời hứa" Archibald. Bạn có thấy cách tôi gọi processResult() bên trong chính nó để thiết lập một vòng lặp không đồng bộ không? Việc viết mã đó khiến tôi cảm thấy rất thông minh. Nhưng giống như hầu hết mã "thông minh", bạn phải nhìn chằm chằm vào mã đó trong một thời gian dài để tìm hiểu xem mã đó đang làm gì, giống như một trong những bức ảnh mắt thần từ những năm 90.

Hãy thử lại với các hàm không đồng bộ:

async function getResponseSize(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  let result = await reader.read();
  let total = 0;

  while (!result.done) {
    const value = result.value;
    total += value.length;
    console.log('Received chunk', value);
    // get the next result
    result = await reader.read();
  }

  return total;
}

Tất cả tính năng "thông minh" đều biến mất. Vòng lặp không đồng bộ khiến tôi cảm thấy tự mãn đã được thay thế bằng một vòng lặp while đáng tin cậy và nhàm chán. Tốt hơn nhiều. Trong tương lai, bạn sẽ có các trình duyệt tuần tự không đồng bộ, và các trình duyệt này sẽ thay thế vòng lặp while bằng vòng lặp for-of, giúp vòng lặp này gọn gàng hơn.

Cú pháp hàm không đồng bộ khác

Tôi đã cho bạn thấy async function() {}, nhưng bạn có thể sử dụng từ khoá async với cú pháp hàm khác:

Hàm mũi tên

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});

Phương thức đối tượng

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then();

Phương thức lớp

class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jaffathecake').then();

Hãy cẩn thận! Tránh quá trình quá tuần tự

Mặc dù bạn đang viết mã có vẻ đồng bộ, nhưng hãy đảm bảo bạn không bỏ lỡ cơ hội để thực hiện các thao tác song song.

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

Quá trình trên mất 1000 mili giây để hoàn tất, trong khi:

async function parallel() {
  const wait1 = wait(500); // Start a 500ms timer asynchronously…
  const wait2 = wait(500); // …meaning this timer happens in parallel.
  await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
  return 'done!';
}

Quá trình trên mất 500 mili giây để hoàn tất, vì cả hai lần chờ đều diễn ra cùng một lúc. Hãy xem một ví dụ thực tế.

Ví dụ: xuất dữ liệu tìm nạp theo thứ tự

Giả sử bạn muốn tìm nạp một loạt URL và ghi lại các URL đó càng sớm càng tốt, theo thứ tự chính xác.

Hít thở sâu – sau đây là cách thực hiện với các lời hứa:

function markHandled(promise) {
  promise.catch(() => {});
  return promise;
}

function logInOrder(urls) {
  // fetch all the URLs
  const textPromises = urls.map((url) => {
    return markHandled(fetch(url).then((response) => response.text()));
  });

  // log them in order
  return textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text));
  }, Promise.resolve());
}

Vâng, đúng vậy, tôi đang sử dụng reduce để tạo chuỗi các lời hứa. Tôi rất thông minh. Tuy nhiên, đây là một cách lập trình quá thông minh mà bạn nên tránh.

Tuy nhiên, khi chuyển đổi mã trên thành một hàm không đồng bộ, bạn có thể sẽ quá tuần tự:

Không nên dùng – quá tuần tự
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Nhìn gọn gàng hơn nhiều, nhưng lần tìm nạp thứ hai sẽ không bắt đầu cho đến khi lần tìm nạp đầu tiên đã được đọc hoàn toàn, v.v. Cách này chậm hơn nhiều so với ví dụ về lời hứa thực hiện các lệnh tìm nạp song song. Rất may, có một giải pháp trung gian lý tưởng.
Nên dùng – đẹp và song song
function markHandled(...promises) {
  Promise.allSettled(promises);
}

async function logInOrder(urls) {
  // fetch all the URLs in parallel
  const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
  });

  markHandled(...textPromises);

  // log them in sequence
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}
Trong ví dụ này, các URL được tìm nạp và đọc song song, nhưng bit reduce "thông minh" được thay thế bằng một vòng lặp for-loop tiêu chuẩn, nhàm chán và dễ đọc.

Giải pháp hỗ trợ trình duyệt: trình tạo

Nếu đang nhắm đến các trình duyệt hỗ trợ trình tạo (bao gồm cả phiên bản mới nhất của mọi trình duyệt chính), bạn có thể sắp xếp các hàm không đồng bộ polyfill.

Babel sẽ thực hiện việc này cho bạn, dưới đây là ví dụ thông qua Babel REPL

Bạn nên sử dụng phương pháp chuyển đổi mã vì bạn chỉ cần tắt phương thức này sau khi trình duyệt mục tiêu hỗ trợ các hàm không đồng bộ. Tuy nhiên, nếu thực sự không muốn sử dụng trình chuyển đổi mã, bạn có thể sử dụng polyfill của Babel và tự sử dụng. Thay vì:

async function slowEcho(val) {
  await wait(1000);
  return val;
}

…bạn sẽ thêm polyfill và viết:

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

Xin lưu ý rằng bạn phải truyền một trình tạo (function*) đến createAsyncFunction và sử dụng yield thay vì await. Ngoài ra, cách hoạt động vẫn như cũ.

Giải pháp: trình tái tạo

Nếu bạn đang nhắm đến các trình duyệt cũ, Babel cũng có thể biên dịch ngược các trình tạo, cho phép bạn sử dụng các hàm không đồng bộ cho đến IE8. Để làm việc này, bạn cần có tập lệnh trước es2017 của Babel tập lệnh trước es2015.

Kết quả không đẹp mắt, vì vậy, hãy cẩn thận với tình trạng mã bị phình to.

Không đồng bộ mọi thứ!

Sau khi các hàm không đồng bộ xuất hiện trên tất cả trình duyệt, hãy sử dụng các hàm này trên mọi hàm trả về lời hứa! Không chỉ giúp mã của bạn gọn gàng hơn, mà còn đảm bảo rằng hàm sẽ luôn trả về một lời hứa.

Tôi đã rất hào hứng với các hàm không đồng bộ vào năm 2014 và thật tuyệt khi thấy các hàm này xuất hiện thực sự trong trình duyệt. Hoan hô!