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ã được đồng bộ.

Jake Archibald
J Jake Archibald

Các chức năng không đồng bộ được bật theo mặc định trong Chrome, Edge, Firefox và Safari. Các chức năng này thực sự 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ộ mà không chặn luồng chính. Chúng làm cho mã không đồng bộ trở nên kém "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 một đị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 này sẽ bị tạm dừng theo cách không chặn cho đến khi lời hứa đó được giải quyết. Nếu lời hứa 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.

Hỗ trợ trình duyệt

Hỗ trợ trình duyệt

  • 55
  • 15
  • 52
  • 10.1

Nguồn

Ví dụ: ghi nhật ký 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à giao diện sử dụng hứa hẹn:

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

Và đây 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);
  }
}

Cùng là số dòng, nhưng tất cả các lệnh gọi lại đều biến mất. Điều này giúp thông tin trở nên dễ đọc hơn, đặc biệt đối với những người chưa quen thuộc với các lời hứa.

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

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ẽ được phân giải nếu hàm không đồng bộ trả về hoặc từ chối bất kể hàm không đồng bộ nào trả về. Do đó, với:

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

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

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

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

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

Ví dụ: truyền trực tuyến một câu trả lời

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

Đây là những lời hứa hẹn:

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);
    });
  });
}

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

Hãy thử lại với 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ả những gì "thông minh" đều biến mất. Vòng lặp không đồng bộ khiến tôi cảm thấy rất tự hào được thay thế bằng một vòng lặp đáng tin cậy, nhàm chán và đáng tin cậy. Tốt hơn nhiều. Trong tương lai, bạn sẽ nhận được trình lặp không đồng bộ, thay thế vòng lặp while bằng vòng lặp for-of, giúp vòng lặp trở nên 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(…);

Cẩn thận! Tránh diễn ra quá tuần tự

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

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

Việc trên mất 1000 mili giây để hoàn thành, 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 thời gian chờ đều xảy ra cùng lúc. Hãy xem một ví dụ thực tế.

Ví dụ: xuất các lần 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 đúng thứ tự.

Cảm ơn bạn – đây là cách thực hiện kèm theo hứa hẹn:

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());
}

Đúng vậy, tôi đang sử dụng reduce để xâu chuỗi một 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 thông minh đến vậy.

Tuy nhiên, khi chuyển đổi nội dung trên thành một hàm không đồng bộ, bạn 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());
  }
}
Có vẻ gọn gàng hơn nhiều, nhưng lần tìm nạp thứ hai của tôi chỉ bắt đầu cho đến khi lần tìm nạp đầu tiên được đọc đầy đủ, và cứ tiếp tục như vậy. Việc này chậm hơn nhiều so với ví dụ hứa hẹn thực hiện các lần tìm nạp song song. Rất may là có một nền tảng lý tưởng.
Được đề xuất - phù hợ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 "thông minh" reduce được thay thế bằng một vòng lặp for chuẩn, nhàm chán, dễ đọc.

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

Nếu đang nhắm mục tiêu các trình duyệt có hỗ trợ trình tạo (bao gồm 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, sau đây là một ví dụ thông qua CameraX REPL

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

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

...bạn sẽ đưa vào 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ạo

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

Kết quả không đẹp, vì vậy, hãy chú ý đến hiện tượng cồng kềnh.

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

Sau khi hàm không đồng bộ truy cập trên mọi trình duyệt, hãy sử dụng các hàm đó trên mọi hàm trả về hứa hẹn! Chúng 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 thực sự quan tâm đến 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 truy cập thực tế trong các trình duyệt. Ôi!