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

Jake Archibald
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, và thực sự thì chúng rất tuyệt vời. Chúng cho phép bạn viết mã dựa trên lời hứa dưới dạng nếu có tính đồng bộ nhưng không chặn luồng chính. Chúng giúp mã không đồng bộ bớt "thông minh" và dễ đọc hơn.

Các 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 sẽ tạm dừng theo cách không chặn luồng thực thi cho đến khi lời hứa thực hiện được. Nếu lời hứa thành công, bạn lấy lại giá trị. Nếu lời hứa bị từ chối, thì giá trị bị từ chối sẽ được gửi.

Hỗ trợ trình duyệt

Hỗ trợ trình duyệt

  • Chrome: 55.
  • Cạnh: 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 URL và ghi nhật ký phản hồi dưới dạng văn bản. Dưới đây là giao diện của ứng dụng 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à dưới đây là 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);
  }
}

Vẫn là số lượng dòng, nhưng tất cả lệnh gọi lại đều biến mất. Bằng cách này dễ đọc hơn, đặc biệt là đối với người không quen thuộc với 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 hứa hẹn, cho dù bạn có sử dụng await hay không. Đó hứa hẹn sẽ phân giải bằng bất kỳ nội dung nào mà hàm không đồng bộ trả về hoặc từ chối bằng bất kỳ điều gì mà hàm không đồng bộ gửi ra. 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';
}

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

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

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

Ví dụ: tạo câu trả lời theo thời gian thực

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

Ở đây là phần cam kết:

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 khám phá tôi nhé, Jake, "người sử dụng những lời hứa" Archibald. Xem cách tôi đang gọi processResult() bên trong chính nó để thiết lập vòng lặp không đồng bộ? Tác phẩm sáng tạo tôi cảm thấy rất thông minh. Nhưng giống như hầu hết các ứng dụng "thông minh" mã của bạn, bạn phải nhìn vào nó để tìm ra tác vụ của công cụ đó, giống như một trong các bức ảnh hiệu ứng mắt kỳ diệu của thập niên 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ả những người "thông minh" đã biến mất. Vòng lặp không đồng bộ khiến tôi cảm thấy tự hào được thay thế bằng một vòng lặp đáng tin cậy, nhàm chán. Tốt hơn nhiều. Trong tương lai, bạn sẽ nhận được trình lặp không đồng bộ, điều này sẽ thay thế vòng lặp while bằng vòng lặp for-of để 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 từ khoá async có thể được sử dụng với cú pháp hàm khác:

Cá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 của 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 sắp xếp quá theo tuần tự

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

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

Thao tá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!';
}

Thao tác trên mất 500 mili giây để hoàn tất vì cả hai lần chờ đều xảy ra cùng một 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 chúng vào nhật ký sớm nhất có thể, trong thứ tự chính xác.

Hít thở sâu – đây là cách trông giống với 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 như vậy thông minh. Tuy nhiên, đây là một cách lập trình thông minh để bạn có thể thực hiện tốt hơn mà không cần.

Tuy nhiên, khi chuyển đổi hàm trên thành hàm không đồng bộ, muốn đi quá tuần tự:

Không nên dùng vì quá tuần tự
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Trông gọn gàng hơn nhiều, nhưng lần tìm nạp thứ hai của tôi không bắt đầu cho đến khi lần tìm nạp đầu tiên của tôi đã được đọc đầy đủ, v.v. Điều này chậm hơn nhiều so với ví dụ hứa hẹn rằng thực hiện các tìm nạp song song. Rất may là có một trung tâm lý tưởng.
Được đề xuất – tốt 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 URL "thông minh" Bit reduce được thay thế bằng một vòng lặp chuẩn, nhàm chán và có thể đọc được.

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

Nếu bạn đang nhắm mục tiêu đến các trình duyệt 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ẽ làm việc này cho bạn, sau đây là một ví dụ về chương trình tái tiếp thị của adb

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

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

...bạn sẽ thêm đoạn mã polyfill và ghi:

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

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

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

Nếu bạn đang nhắm mục tiêu đến các trình duyệt cũ hơn, Squarespace cũng có thể chuyển đổi trình tạo mã 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 Chế độ cài đặt sẵn cho es2017 của Babel giá trị đặt trước es2015.

Kết quả không đẹp, vì vậy, hãy để ý mã quá tải.

Không đồng bộ tất cả mọi thứ!

Sau khi các hàm không đồng bộ có mặt trên tất cả trình duyệt, hãy sử dụng chúng trên mọi trình duyệt hàm trả về hứa hẹn! Những phần này không chỉ giúp mã gọn gàng hơn mà còn giúp đảm bảo rằng hàm đó sẽ luôn trả về một hứa hẹn.

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