Giới thiệu Async Iterators trong NodeJS

0

Async Iterators lần đầu được giới thiệu trong bản NodeJS v10.0.0, kể từ đó tới nay, Async iterators ngày càng thu hút được sự chú ý của cộng đồng lập trình NodeJS.

Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu về Async Iterators, xem chúng có tính năng gì hay ho, nó được tạo ra để giải quyết bài toán gì và như thế nào?

Đặt vấn đề

Chắc hẳn nhắc tới Async là có phải bạn nghĩ ngay tới bất đồng bộ đúng không? Nói tới bất đồng bộ thì lại bật ngay tới Callback, Promise hay Async/Await. Nhưng nó chỉ hữu ích khi xử lý một đơn bất đồng bộ mà thôi. Trong trường hợp, bạn cần xử lý nhiều sự kiện bất đồng bộ lặp đi lặp lại thì sao? Kiểu như vòng lặp các sự kiện bất đồng bộ.

Ví dụ như trường hợp bạn cần xử lý sự kiện “click” chuột trên ứng dụng, một Promise không thể xử lý cho một loạt chuỗi sự kiện bất đồng bộ đó được, cũng không thể sử dụng các async/await thông thường được. Đây là lúc bạn cần sử dụng tới vòng lặp bất đồng bộ.

Async Iterators là gì?

Vậy Async Iterators là gì? Thực chất, nó là một phiên bản vòng lặp bất đồng bộ mà mọi người đã biết từ trước.

Async Iterators được sử dụng khi chúng ta không biết các giá trị và trạng thái kết thúc khi thực hiện vòng lặp. Thay vào đó, chúng ta nhận được các Promise mà trong đó khi promise này được resolve thành đối tượng mà bạn có thể sử dụng được { value: any, done: boolean }

const asyncIterable = [1, 2, 3];
asyncIterable[Symbol.asyncIterator] = async function*() {
  for (let i = 0; i < asyncIterable.length; i++) {
    yield { value: asyncIterable[i], done: false }
  }
  yield { done: true };
};

(async function() {
  for await (const part of asyncIterable) {
    console.log(part);
  }
})();

Nói đến vòng lặp bất đồng bộ, chúng ta có hai khái niệm vòng lặp:

  • for-of: là vòng lặp thông thường mà bạn vẫn hay dùng.
  • for-await-of: Vòng lặp bất đồng bộ

Vòng lặp for-await-of sẽ đợi mọi promise mà nó nhận được để resolve trước khi chuyển sang vòng lặp tiếp theo. Điều này ngược với vòng lặp for-of thông thường.

Một số trường hợp thực tế mà sử dụng Async Iterators rất hữu ích.

Streams với Async Iterators

Async Iterators rất hữu ích khi xử lý các luồng Stream, ví dụ luồng stream đọc file. Các stream có thể là đọc, ghi, transform stream…

async function printFileToConsole(path) {
  try {
    const readStream = fs.createReadStream(path, { encoding: 'utf-8' });

    for await (const chunk of readStream) {
      console.log(chunk);
    }

    console.log('EOF');
  } catch(error) {
    console.log(error);
  }
}

Với cách viết code như trên, bạn sẽ không phải lắng nghe các sự kiện “data” và “end”, vì bạn nhận được từng đoạn chunk bằng cách lặp lại và vòng lặp for-await-of tự kết thúc khi luồng kết thúc.

Viết các API có hỗ trợ phân trang

Bạn có hay phải viết các REST API mà phải hỗ trợ phân trang không?

Cá nhân thì mình rất ngại viết mấy cái phân trang này, mặc dù thường xuyên phải viết.

Với Async Iterators, bạn có thể fetch data từ các nguồn dữ liệu (phổ biến nhất là từ Database) rồi hỗ trợ phân trang kết quả trả về cho client một cách dễ dàng.

Để làm điều này, bạn sẽ cần cấu trúc lại body của response từ stream mà Node https request đang cung cấp. Chúng ta cũng có thể sử dụng trình lặp bất đồng bộ tại đây, vì các request và response trong NodeJS đều là các stream.

const https = require('https');

function homebrewFetch(url) {
  return new Promise(async (resolve, reject) => {
    const req = https.get(url, async function(res) {
      if (res.statusCode >= 400) {
        return reject(new Error(`HTTP Status: ${res.statusCode}`));
      }

      try {
        let body = '';

        /*
          Instead of res.on to listen for data on the stream,
          we can use for-await-of, and append the data chunk
          to the rest of the response body
        */
        for await (const chunk of res) {
          body += chunk;
        }
    
        // Handle the case where the response don't have a body
        if (!body) resolve({});
        // We need to parse the body to get the json, as it is a string
        const result = JSON.parse(body);
        resolve(result);
      } catch(error) {
        reject(error)
      }
    });

    await req;
    req.end();
  });
}

Mình sẽ lấy một ví dụ, chúng ta tạo một request tới Cat API để lấy một số ảnh về mèo theo lô từng 10 ảnh một. Thời gian trễ giữa các request là 7s, số page tối đa là 7 page để tránh quá tải cho API server.

function fetchCatPics({ limit, page, done }) {
  return homebrewFetch(`https://api.thecatapi.com/v1/images/search?limit=${limit}&page=${page}&order=DESC`)
    .then(body => ({ value: body, done }));
}

function catPics({ limit }) {
  return {
    [Symbol.asyncIterator]: async function*() {
      let currentPage = 0;
      // Stop after 5 pages
      while(currentPage < 5) {
        try {
          const cats = await fetchCatPics({ currentPage, limit, done: false });
          console.log(`Fetched ${limit} cats`);
          yield cats;
          currentPage ++;
        } catch(error) {
          console.log('There has been an error fetching all the cats!');
          console.log(error);
        }
      }
    }
  };
}

(async function() {
  try {
    for await (let catPicPage of catPics({ limit: 10 })) {
      console.log(catPicPage);
      // Wait for 7 seconds between requests
      await new Promise(resolve => setTimeout(resolve, 7000));
    }
  } catch(error) {
    console.log(error);
  }
})()

Bằng cách này, chúng ta tự động lấy một lô ảnh mèo sau mỗi 7s.

Ngoài ra, bạn còn một cách tiếp cận khác để phân trang giữa các pages là triển khai phương pháp next/previous thủ công.

function actualCatPics({ limit }) {
  return {
    [Symbol.asyncIterator]: () => {
      let page = 0;
      return {
        next: function() {
          page++;
          return fetchCatPics({ page, limit, done: false });
        },
        previous: function() {
          if (page > 0) {
            page--;
            return fetchCatPics({ page, limit, done: false });
          }
          return fetchCatPics({ page: 0, limit, done: true });
        }
      }
    }
  };
}

try {
    const someCatPics = actualCatPics({ limit: 5 });
    const { next, previous } = someCatPics[Symbol.asyncIterator]();
    next().then(console.log);
    next().then(console.log);
    previous().then(console.log);
} catch(error) {
  console.log(error);
}

Bạn thấy đấy, Async Iterators rất hữu ích trong trường hợp này phải không? Đặt biệt với bài toán như fetch data khi cuộn trang vô hạn trên giao diện người dùng.

Ok, mình tạm kết thúc thảo luận về Async Iterator tại đây. Dự án của bạn có sử dụng  Async Iterator không? Nếu có thì chia sẻ với mọi người nhé.

Nguồn: https://blog.risingstack.com/async-iterators-in-node-js/

Dịch vụ phát triển ứng dụng mobile giá rẻ - chất lượng

Bình luận. Cùng nhau thảo luận nhé!

avatar
  Theo dõi bình luận  
Thông báo