Tiếng Việt

Khám phá JavaScript Async Iterator Helpers để cách mạng hóa việc xử lý luồng. Học cách xử lý hiệu quả các luồng dữ liệu bất đồng bộ với map, filter, take, drop, v.v.

JavaScript Async Iterator Helpers: Xử lý Luồng Mạnh mẽ cho các Ứng dụng Hiện đại

Trong phát triển JavaScript hiện đại, việc xử lý các luồng dữ liệu bất đồng bộ là một yêu cầu phổ biến. Dù bạn đang lấy dữ liệu từ API, xử lý các tệp lớn hay quản lý các sự kiện thời gian thực, việc quản lý dữ liệu bất đồng bộ một cách hiệu quả là rất quan trọng. Async Iterator Helpers của JavaScript cung cấp một cách mạnh mẽ và thanh lịch để xử lý các luồng này, mang lại một phương pháp tiếp cận chức năng và có thể kết hợp để thao tác dữ liệu.

Async Iterators và Async Iterables là gì?

Trước khi đi sâu vào Async Iterator Helpers, hãy cùng tìm hiểu các khái niệm cơ bản: Async Iterators và Async Iterables.

Một Async Iterable là một đối tượng định nghĩa cách lặp qua các giá trị của nó một cách bất đồng bộ. Nó thực hiện điều này bằng cách triển khai phương thức @@asyncIterator, phương thức này trả về một Async Iterator.

Một Async Iterator là một đối tượng cung cấp một phương thức next(). Phương thức này trả về một promise, khi được giải quyết, sẽ trả về một đối tượng có hai thuộc tính:

Đây là một ví dụ đơn giản:


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 500)); // Mô phỏng một hoạt động bất đồng bộ
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  for await (const value of asyncIterable) {
    console.log(value); // Đầu ra: 1, 2, 3, 4, 5 (với độ trễ 500ms giữa mỗi số)
  }
})();

Trong ví dụ này, generateSequence là một hàm generator bất đồng bộ tạo ra một chuỗi số một cách không đồng bộ. Vòng lặp for await...of được sử dụng để duyệt qua các giá trị từ async iterable.

Giới thiệu về Async Iterator Helpers

Async Iterator Helpers mở rộng chức năng của Async Iterators, cung cấp một bộ phương thức để biến đổi, lọc và thao tác các luồng dữ liệu bất đồng bộ. Chúng cho phép một phong cách lập trình chức năng và có thể kết hợp, giúp việc xây dựng các chuỗi xử lý dữ liệu phức tạp trở nên dễ dàng hơn.

Các Async Iterator Helpers cốt lõi bao gồm:

Hãy cùng khám phá từng trình trợ giúp với các ví dụ.

map()

Trình trợ giúp map() biến đổi mỗi phần tử của async iterable bằng một hàm được cung cấp. Nó trả về một async iterable mới với các giá trị đã được biến đổi.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const doubledIterable = asyncIterable.map(x => x * 2);

(async () => {
  for await (const value of doubledIterable) {
    console.log(value); // Đầu ra: 2, 4, 6, 8, 10 (với độ trễ 100ms)
  }
})();

Trong ví dụ này, map(x => x * 2) nhân đôi mỗi số trong chuỗi.

filter()

Trình trợ giúp filter() chọn các phần tử từ async iterable dựa trên một điều kiện được cung cấp (hàm vị từ). Nó trả về một async iterable mới chỉ chứa các phần tử thỏa mãn điều kiện.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);

const evenNumbersIterable = asyncIterable.filter(x => x % 2 === 0);

(async () => {
  for await (const value of evenNumbersIterable) {
    console.log(value); // Đầu ra: 2, 4, 6, 8, 10 (với độ trễ 100ms)
  }
})();

Trong ví dụ này, filter(x => x % 2 === 0) chỉ chọn các số chẵn từ chuỗi.

take()

Trình trợ giúp take() trả về N phần tử đầu tiên từ async iterable. Nó trả về một async iterable mới chỉ chứa số lượng phần tử được chỉ định.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const firstThreeIterable = asyncIterable.take(3);

(async () => {
  for await (const value of firstThreeIterable) {
    console.log(value); // Đầu ra: 1, 2, 3 (với độ trễ 100ms)
  }
})();

Trong ví dụ này, take(3) chọn ba số đầu tiên từ chuỗi.

drop()

Trình trợ giúp drop() bỏ qua N phần tử đầu tiên từ async iterable và trả về phần còn lại. Nó trả về một async iterable mới chứa các phần tử còn lại.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

const afterFirstTwoIterable = asyncIterable.drop(2);

(async () => {
  for await (const value of afterFirstTwoIterable) {
    console.log(value); // Đầu ra: 3, 4, 5 (với độ trễ 100ms)
  }
})();

Trong ví dụ này, drop(2) bỏ qua hai số đầu tiên từ chuỗi.

toArray()

Trình trợ giúp toArray() duyệt qua toàn bộ async iterable và thu thập tất cả các phần tử vào một mảng. Nó trả về một promise, khi được giải quyết, sẽ trả về một mảng chứa tất cả các phần tử.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const numbersArray = await asyncIterable.toArray();
  console.log(numbersArray); // Đầu ra: [1, 2, 3, 4, 5]
})();

Trong ví dụ này, toArray() thu thập tất cả các số từ chuỗi vào một mảng.

forEach()

Trình trợ giúp forEach() thực thi một hàm được cung cấp một lần cho mỗi phần tử trong async iterable. Nó *không* trả về một async iterable mới, mà thực thi hàm với hiệu ứng phụ. Điều này có thể hữu ích cho việc thực hiện các hoạt động như ghi log hoặc cập nhật giao diện người dùng.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(3);

(async () => {
  await asyncIterable.forEach(value => {
    console.log("Value:", value);
  });
  console.log("forEach completed");
})();
// Đầu ra: Value: 1, Value: 2, Value: 3, forEach completed

some()

Trình trợ giúp some() kiểm tra xem có ít nhất một phần tử trong async iterable vượt qua bài kiểm tra được triển khai bởi hàm được cung cấp hay không. Nó trả về một promise, khi được giải quyết, sẽ trả về một giá trị boolean (true nếu có ít nhất một phần tử thỏa mãn điều kiện, ngược lại là false).


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const hasEvenNumber = await asyncIterable.some(x => x % 2 === 0);
  console.log("Has even number:", hasEvenNumber); // Đầu ra: Has even number: true
})();

every()

Trình trợ giúp every() kiểm tra xem tất cả các phần tử trong async iterable có vượt qua bài kiểm tra được triển khai bởi hàm được cung cấp hay không. Nó trả về một promise, khi được giải quyết, sẽ trả về một giá trị boolean (true nếu tất cả các phần tử thỏa mãn điều kiện, ngược lại là false).


async function* generateSequence(end) {
  for (let i = 2; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(4);

(async () => {
  const areAllEven = await asyncIterable.every(x => x % 2 === 0);
  console.log("Are all even:", areAllEven); // Đầu ra: Are all even: true
})();

find()

Trình trợ giúp find() trả về phần tử đầu tiên trong async iterable thỏa mãn hàm kiểm tra được cung cấp. Nếu không có giá trị nào thỏa mãn hàm kiểm tra, undefined sẽ được trả về. Nó trả về một promise, khi được giải quyết, sẽ trả về phần tử được tìm thấy hoặc undefined.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const firstEven = await asyncIterable.find(x => x % 2 === 0);
  console.log("First even number:", firstEven); // Đầu ra: First even number: 2
})();

reduce()

Trình trợ giúp reduce() thực thi một hàm callback "reducer" do người dùng cung cấp trên mỗi phần tử của async iterable, theo thứ tự, truyền vào giá trị trả về từ phép tính trên phần tử trước đó. Kết quả cuối cùng của việc chạy reducer trên tất cả các phần tử là một giá trị duy nhất. Nó trả về một promise, khi được giải quyết, sẽ trả về giá trị tích lũy cuối cùng.


async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(5);

(async () => {
  const sum = await asyncIterable.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
  console.log("Sum:", sum); // Đầu ra: Sum: 15
})();

Ví dụ Thực tế và Các Trường hợp Sử dụng

Async Iterator Helpers rất có giá trị trong nhiều tình huống khác nhau. Hãy cùng khám phá một số ví dụ thực tế:

1. Xử lý Dữ liệu từ một Streaming API

Hãy tưởng tượng bạn đang xây dựng một bảng điều khiển trực quan hóa dữ liệu thời gian thực nhận dữ liệu từ một API streaming. API gửi các bản cập nhật liên tục, và bạn cần xử lý những bản cập nhật này để hiển thị thông tin mới nhất.


async function* fetchDataFromAPI(url) {
  let response = await fetch(url);

  if (!response.body) {
    throw new Error("ReadableStream không được hỗ trợ trong môi trường này");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        break;
      }
      const chunk = decoder.decode(value);
      // Giả sử API gửi các đối tượng JSON được phân tách bằng dòng mới
      const lines = chunk.split('\n');
      for (const line of lines) {
        if (line.trim() !== '') {
          yield JSON.parse(line);
        }
      }
    }
  } finally {
    reader.releaseLock();
  }
}

const apiURL = 'https://example.com/streaming-api'; // Thay thế bằng URL API của bạn
const dataStream = fetchDataFromAPI(apiURL);

// Xử lý luồng dữ liệu
(async () => {
  for await (const data of dataStream.filter(item => item.type === 'metric').map(item => ({ timestamp: item.timestamp, value: item.value }))) {
    console.log('Processed Data:', data);
    // Cập nhật bảng điều khiển với dữ liệu đã xử lý
  }
})();

Trong ví dụ này, fetchDataFromAPI lấy dữ liệu từ một API streaming, phân tích các đối tượng JSON và trả về chúng dưới dạng một async iterable. Trình trợ giúp filter chỉ chọn các số liệu, và trình trợ giúp map biến đổi dữ liệu thành định dạng mong muốn trước khi cập nhật bảng điều khiển.

2. Đọc và Xử lý các Tệp Lớn

Giả sử bạn cần xử lý một tệp CSV lớn chứa dữ liệu khách hàng. Thay vì tải toàn bộ tệp vào bộ nhớ, bạn có thể sử dụng Async Iterator Helpers để xử lý từng phần một.


async function* readLinesFromFile(filePath) {
  const file = await fsPromises.open(filePath, 'r');

  try {
    let buffer = Buffer.alloc(1024);
    let fileOffset = 0;
    let remainder = '';

    while (true) {
      const { bytesRead } = await file.read(buffer, 0, buffer.length, fileOffset);
      if (bytesRead === 0) {
        if (remainder) {
          yield remainder;
        }
        break;
      }

      fileOffset += bytesRead;
      const chunk = buffer.toString('utf8', 0, bytesRead);
      const lines = chunk.split('\n');

      lines[0] = remainder + lines[0];
      remainder = lines.pop() || '';

      for (const line of lines) {
        yield line;
      }
    }
  } finally {
    await file.close();
  }
}

const filePath = './customer_data.csv'; // Thay thế bằng đường dẫn tệp của bạn
const lines = readLinesFromFile(filePath);

// Xử lý các dòng
(async () => {
  for await (const customerData of lines.drop(1).map(line => line.split(',')).filter(data => data[2] === 'USA')) {
    console.log('Customer from USA:', customerData);
    // Xử lý dữ liệu khách hàng từ Hoa Kỳ
  }
})();

Trong ví dụ này, readLinesFromFile đọc tệp từng dòng và trả về mỗi dòng dưới dạng một async iterable. Trình trợ giúp drop(1) bỏ qua hàng tiêu đề, trình trợ giúp map chia dòng thành các cột, và trình trợ giúp filter chỉ chọn khách hàng từ Hoa Kỳ.

3. Xử lý Sự kiện Thời gian thực

Async Iterator Helpers cũng có thể được sử dụng để xử lý các sự kiện thời gian thực từ các nguồn như WebSockets. Bạn có thể tạo một async iterable phát ra các sự kiện khi chúng đến và sau đó sử dụng các trình trợ giúp để xử lý các sự kiện này.


async function* createWebSocketStream(url) {
  const ws = new WebSocket(url);

  yield new Promise((resolve, reject) => {
      ws.onopen = () => {
          resolve();
      };
      ws.onerror = (error) => {
          reject(error);
      };
  });

  try {
    while (ws.readyState === WebSocket.OPEN) {
      yield new Promise((resolve, reject) => {
        ws.onmessage = (event) => {
          resolve(JSON.parse(event.data));
        };
        ws.onerror = (error) => {
          reject(error);
        };
        ws.onclose = () => {
           resolve(null); // Giải quyết với null khi kết nối đóng
        }
      });

    }
  } finally {
    ws.close();
  }
}

const websocketURL = 'wss://example.com/events'; // Thay thế bằng URL WebSocket của bạn
const eventStream = createWebSocketStream(websocketURL);

// Xử lý luồng sự kiện
(async () => {
  for await (const event of eventStream.filter(event => event.type === 'user_login').map(event => ({ userId: event.userId, timestamp: event.timestamp }))) {
    console.log('User Login Event:', event);
    // Xử lý sự kiện đăng nhập của người dùng
  }
})();

Trong ví dụ này, createWebSocketStream tạo một async iterable phát ra các sự kiện nhận được từ WebSocket. Trình trợ giúp filter chỉ chọn các sự kiện đăng nhập của người dùng, và trình trợ giúp map biến đổi dữ liệu thành định dạng mong muốn.

Lợi ích của việc Sử dụng Async Iterator Helpers

Hỗ trợ Trình duyệt và Môi trường Chạy

Async Iterator Helpers vẫn là một tính năng tương đối mới trong JavaScript. Tính đến cuối năm 2024, chúng đang ở Giai đoạn 3 của quy trình tiêu chuẩn hóa TC39, nghĩa là chúng có khả năng sẽ được tiêu chuẩn hóa trong tương lai gần. Tuy nhiên, chúng chưa được hỗ trợ nguyên bản trong tất cả các trình duyệt và phiên bản Node.js.

Hỗ trợ Trình duyệt: Các trình duyệt hiện đại như Chrome, Firefox, Safari và Edge đang dần bổ sung hỗ trợ cho Async Iterator Helpers. Bạn có thể kiểm tra thông tin tương thích trình duyệt mới nhất trên các trang web như Can I use... để xem trình duyệt nào hỗ trợ tính năng này.

Hỗ trợ Node.js: Các phiên bản Node.js gần đây (v18 trở lên) cung cấp hỗ trợ thử nghiệm cho Async Iterator Helpers. Để sử dụng chúng, bạn có thể cần chạy Node.js với cờ --experimental-async-iterator.

Polyfills: Nếu bạn cần sử dụng Async Iterator Helpers trong các môi trường không hỗ trợ chúng nguyên bản, bạn có thể sử dụng một polyfill. Polyfill là một đoạn mã cung cấp chức năng còn thiếu. Có một số thư viện polyfill dành cho Async Iterator Helpers; một lựa chọn phổ biến là thư viện core-js.

Triển khai Async Iterators Tùy chỉnh

Mặc dù Async Iterator Helpers cung cấp một cách tiện lợi để xử lý các async iterables hiện có, đôi khi bạn có thể cần tạo các async iterators tùy chỉnh của riêng mình. Điều này cho phép bạn xử lý dữ liệu từ nhiều nguồn khác nhau, chẳng hạn như cơ sở dữ liệu, API hoặc hệ thống tệp, theo cách streaming.

Để tạo một async iterator tùy chỉnh, bạn cần triển khai phương thức @@asyncIterator trên một đối tượng. Phương thức này nên trả về một đối tượng có phương thức next(). Phương thức next() nên trả về một promise, khi được giải quyết, sẽ trả về một đối tượng có thuộc tính valuedone.

Đây là một ví dụ về một async iterator tùy chỉnh lấy dữ liệu từ một API phân trang:


async function* fetchPaginatedData(baseURL) {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const url = `${baseURL}?page=${page}`;
    const response = await fetch(url);
    const data = await response.json();

    if (data.results.length === 0) {
      hasMore = false;
      break;
    }

    for (const item of data.results) {
      yield item;
    }

    page++;
  }
}

const apiBaseURL = 'https://api.example.com/data'; // Thay thế bằng URL API của bạn
const paginatedData = fetchPaginatedData(apiBaseURL);

// Xử lý dữ liệu phân trang
(async () => {
  for await (const item of paginatedData) {
    console.log('Item:', item);
    // Xử lý mục
  }
})();

Trong ví dụ này, fetchPaginatedData lấy dữ liệu từ một API phân trang, trả về mỗi mục khi nó được truy xuất. Async iterator xử lý logic phân trang, giúp dễ dàng tiêu thụ dữ liệu theo cách streaming.

Những Thách thức và Cân nhắc Tiềm ẩn

Mặc dù Async Iterator Helpers mang lại nhiều lợi ích, điều quan trọng là phải nhận thức được một số thách thức và cân nhắc tiềm ẩn:

Các Thực hành Tốt nhất khi Sử dụng Async Iterator Helpers

Để tận dụng tối đa Async Iterator Helpers, hãy xem xét các thực hành tốt nhất sau đây:

Các Kỹ thuật Nâng cao

Soạn thảo các Trình trợ giúp Tùy chỉnh

Bạn có thể tạo các trình trợ giúp async iterator tùy chỉnh của riêng mình bằng cách kết hợp các trình trợ giúp hiện có hoặc xây dựng những cái mới từ đầu. Điều này cho phép bạn tùy chỉnh chức năng theo nhu cầu cụ thể của mình và tạo ra các thành phần có thể tái sử dụng.


async function* takeWhile(asyncIterable, predicate) {
  for await (const value of asyncIterable) {
    if (!predicate(value)) {
      break;
    }
    yield value;
  }
}

// Ví dụ sử dụng:
async function* generateSequence(end) {
  for (let i = 1; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

const asyncIterable = generateSequence(10);
const firstFive = takeWhile(asyncIterable, x => x <= 5);

(async () => {
  for await (const value of firstFive) {
    console.log(value);
  }
})();

Kết hợp Nhiều Async Iterables

Bạn có thể kết hợp nhiều async iterables thành một async iterable duy nhất bằng các kỹ thuật như zip hoặc merge. Điều này cho phép bạn xử lý dữ liệu từ nhiều nguồn đồng thời.


async function* zip(asyncIterable1, asyncIterable2) {
    const iterator1 = asyncIterable1[Symbol.asyncIterator]();
    const iterator2 = asyncIterable2[Symbol.asyncIterator]();

    while (true) {
        const result1 = await iterator1.next();
        const result2 = await iterator2.next();

        if (result1.done || result2.done) {
            break;
        }

        yield [result1.value, result2.value];
    }
}

// Ví dụ sử dụng:
async function* generateSequence1(end) {
    for (let i = 1; i <= end; i++) {
        yield i;
    }
}

async function* generateSequence2(end) {
    for (let i = 10; i <= end + 9; i++) {
        yield i;
    }
}

const iterable1 = generateSequence1(5);
const iterable2 = generateSequence2(5);

(async () => {
    for await (const [value1, value2] of zip(iterable1, iterable2)) {
        console.log(value1, value2);
    }
})();

Kết luận

JavaScript Async Iterator Helpers cung cấp một cách mạnh mẽ và thanh lịch để xử lý các luồng dữ liệu bất đồng bộ. Chúng mang lại một phương pháp tiếp cận chức năng và có thể kết hợp để thao tác dữ liệu, giúp việc xây dựng các chuỗi xử lý dữ liệu phức tạp trở nên dễ dàng hơn. Bằng cách hiểu các khái niệm cốt lõi của Async Iterators và Async Iterables và thành thạo các phương thức trợ giúp khác nhau, bạn có thể cải thiện đáng kể hiệu quả và khả năng bảo trì của mã JavaScript bất đồng bộ của mình. Khi sự hỗ trợ của trình duyệt và môi trường chạy tiếp tục phát triển, Async Iterator Helpers được dự đoán sẽ trở thành một công cụ thiết yếu cho các nhà phát triển JavaScript hiện đại.