Từ Callback, Promise đến Async/Await trong xử lý bất đồng bộ của JavaScript/Node.js

Từ Callback, Promise đến Async/Await trong xử lý bất đồng bộ của JavaScript/Node.js

Tin ngắn hàng ngày dành cho bạn
  • Hôm qua đến nay, lượt truy cập tới từ Facebook tăng đột biến. Thường như thế là do ai đó chia sẻ bài viết của blog vào một nhóm nào đó.

    Cơ mà lần này là liên kết trực tiếp đến trang chủ luôn. Tò mò ghê, không biết ai chia sẻ, chia sẻ ở đâu nữa. Muốn biết để tìm hiểu "insight" ghê 🥹

    » Xem thêm
  • Mình mới phát hiện ra thư viện idb-keyval giúp triển khai cơ sở dữ liệu dạng key-value một cách đơn giản. Như đã chia sẻ trong chuỗi bài viết về quá trình làm OpenNotas, mình loay hoay đi tìm một loại cơ sở dữ liệu để lưu trữ mà xem chừng vất vả quá, cuối cùng chốt localForage.

    idb-keyval cũng tương tự như localForage nhưng có vẻ như nó đang làm tốt hơn một chút. Đơn cử là có hàm update để cập nhật dữ liệu, hình dung đơn giản là:

    update('counter', (val) => (val || 0) + 1);

    Chứ không như hàm set là thay thế dữ liệu mất tiêu luôn.

    » Xem thêm
  • Đầu xuân năm mới, xin phép khoe số tiền kiếm được sau 1 tháng đặt quảng cáo tại indieboosting.com 🥳🥳🥳

    » Xem thêm

Vấn đề

JavaScript đã xuất hiện lần đầu tiên cách đây gần 30 năm về trước, là một ngôn ngữ lập trình mới thời đó, chắc hẳn phải trải qua một thời gian nữa thì nó mới thực sự ổn định và được sử dụng rộng rãi. Cho đến bây giờ thì chúng ta không thể phủ nhận được sự thành công của JavaScript, nó xuất hiện ở khắp mọi nơi trong thế giới web. Hơn thế, nó còn "thoát" mình ra khỏi trình duyệt để làm được nhiều điều hơn nữa.

Một sản phẩm của công nghệ thì không thể nào ngừng phát triển. Trải qua rất nhiều năm, JavaScript liên tục lột xác và mang lại nhiều tính năng hữu ích hơn cũng như khắc phục nhiều nhược điểm vốn có của nó. TC39 là một nhóm được tạo ra trong hiệp hội ECMA với một nỗ lực nhằm "chuẩn hóa" JavaScript đồng thời chịu trách nhiệm phát triển nó ngày một lớn mạnh.

Chúng ta thường được biết đến cái tên JavaScript phổ biến hơn so với ECMAScript, trong khi thực tế ECMAScript là một đặc tả, và JavaScript là một thành phẩm thành công của đặc tả này. Phiên bản ổn định lần đầu tiên phải nói đến ECMAScript 2009 (ES5, ES2009) được phát hành vào 2009, mang đến nhiều tính năng góp phần khiến cho nó được phổ biến rộng rãi đến bây giờ. Nhưng sự thay đổi lớn nhất phải nói đến năm 2015, sau khi ES6 được giới thiệu (ES2015) với nhiều cải tiến rất đáng kể. Từ đó cứ sau mỗi một năm, TC39 sẽ cố gắng phát hành một phiên bản ECMAScript tiếp theo để cập nhật thêm tính năng mới cho ngôn ngữ này. Tính đến hiện tại, chúng ta đã có phiên bản mới nhất là ECMAScript 2022 (ES13).

Quay trở lại với JavaScript, môt trong những đặc trưng nổi bật nhất của nó là khả năng xử lý bất đồng bộ, có nghĩa là làm việc với nó không thể không biết xử lý bất đồng bộ. Tuy vậy, trong khoảng thời gian đầu ra mắt, việc xử lý bất đồng bộ có nhiều phần bất tiện. Nắm bắt được điều đó, TC39 đã qua nhiều bản cập nhật để mang đến khả năng xử lý sao cho tối ưu nhất.

Callback, Promise, Async/Await lần lượt là những cách mà TC39 giới thiệu khả năng xử lý bất đồng bộ. Trong bài viết này chúng ta hãy tìm hiểu xem quá trình đó diễn ra như thế nào và có đáng giá hay không nhé!

Bất đồng bộ

Node.js chỉ có một luồng chạy mã JavaScript, điều đó có nghĩa là nó phải có cơ chế thực hiện các tác vụ I/O (vốn không tốn quá nhiều CPU mà phụ thuộc vào tốc độ ổ cứng hoặc mạng...) mà không chặn luồng chính, bằng việc áp dụng xử lý I/O không đồng bộ. Mô hình này cho phép chúng ta thực hiện các tác vụ không đồng bộ mà không cần chờ đợi kết quả trước khi tiếp tục thực hiện các tác vụ khác.

Mô hình I/O không đồng bộ của Node.js mang lại nhiều lợi ích. Đầu tiên, nó tăng cường hiệu suất và sự phản hồi của ứng dụng, đặc biệt là trong các ứng dụng mạng như REST API. Thay vì phải chờ đợi các tác vụ I/O hoàn thành, chúng ta có thể tiếp tục thực hiện các tác vụ khác.

Để hiểu thêm về bất đồng bộ, bạn đọc có thể tham khảo thêm các bài viết:

Vì kết quả sẽ được trả về ở một thời điểm nào đó khi gọi các hàm bất đồng bộ, thế nên phải có cách để xử lý chúng.

Callback

Callback là một hàm được truyền vào một hàm khác để nó được gọi lại sau khi một tác vụ bất đồng bộ hoàn thành. Callback là cách xử lý bất đồng bộ nguyên thủy nhất trong JavaScript. Ví dụ, đọc một tệp tin trong Node.js, chúng ta truyền một callback để xử lý kết quả đọc.

const fs = require('node:fs')

fs.readFile('readme.md', (err, data) => {
  if (err) throw err;
  console.log(data);
});

Tham số thứ hai truyền vào trong readFile là một hàm có hai tham số errdata. Sau khi readFile có kết quả, nó sẽ gọi hàm đó với tham số data là dữ liệu lấy được hoặc err sẽ chứa lỗi nếu xảy ra. Cũng vì lý do đó mà cách xử lý bất đồng bộ này được gọi là callback, nghĩa là hàm sẽ được gọi lại sau khi có kết quả của hành vi I/O trả về.

Tuy callback là một cách tiếp cận phổ biến, nhưng nó có một số điểm yếu. Vấn đề lớn nhất là callback hell, khi mã trở nên lồng nhau và khó đọc. Điều này xảy ra khi chúng ta cần thực hiện nhiều tác vụ bất đồng bộ liên tiếp và sử dụng callback để xử lý kết quả của chúng.

asyncFunc1((err1, data1) => {
  asyncFunc2((err2, data2) => {
    asyncFunc3((err3, data3) => {
      asyncFunc4((err4, data4) => {
        // ...
      });
    });
  });
});

Promise

ES6 (còn được gọi là ES2015) đưa đến một tiến bộ đáng kể trong việc xử lý bất đồng bộ bằng việc giới thiệu Promise. Promise là một cơ chế mạnh mẽ cho phép chúng ta xử lý các tác vụ bất đồng bộ một cách dễ đọc và dễ hiểu hơn.

Thay vì sử dụng callback, chúng ta có thể sử dụng Promise để tạo ra một chuỗi các tác vụ bất đồng bộ và xử lý kết quả của chúng. Promise cung cấp các phương thức như thencatch để xử lý kết quả thành công và thất bại của một tác vụ.

Ví dụ về một hành vi đọc file nhưng không xử lý bất đồng bộ bằng callback nữa, thay vào đó là Promise:

const fs =  require('node:fs/promises');

fs.readFile("readme.md")
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.error(err);
  }
);

Ngoài ra, cái hay của Promise là "chaining function", tức là chúng ta có thể "hứng" kết quả của các Promise trước thông quan then và xử lý nó liên tiếp. Nếu tạo ra được những hàm xử lý logic liên tục, chúng ta có thể khắc phục được "callback hell".

fs.readFile("readme.md")
    .then(handleFile1)
    .then(handleFile2)
    .then(handleFile3)
    ...
    .catch()

Lúc này, cộng đồng đón nhận Promise cũng đưa ra một cách mới để biến một hàm không hỗ trợ Promise thành hỗ trợ, dựa vào new Promise.

Ví dụ dưới đây chuyển hàm readFile ở đầu bài thành hàm Promise mà không sử dụng modules sẵn có của Node.

const fs = require('node:fs')

const readFilePromise = (file) => {
  return new Promise((resolve, reject) => {
    fs.readFile(file, (err, data) => {
      if (err) reject(err);
      resolve(data);
    });
  });
}

readFilePromise('readme.md')
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.log(err);
  }
);

Hàm readFilePromise được tạo ra gói lại hàm readFile vốn xử lý bằng callback. Thay vì trả về kết quả trực tiếp trong callback thì dùng hai hàm rejectresolve để trả về kết quả của Promise. Giờ đây chúng ta có thể sử dụng hàm readFilePromise với thencatch như một Promise.

Ngoài ra, Node.js cung cấp thư viện util.promisify để chuyển đổi các hàm xử lý bất đồng bộ từ callback thành Promise. Điều này giúp giảm bớt công việc và tăng tính tương thích khi sử dụng Promise.

const fs = require('node:fs')
const util = require('util')

const readFile = util.promisify(fs.readFile)

readFile("readme.md")
  .then((data) => {
    console.log(data);
  })
  .catch((err) => {
    console.error(err);
  }
);

Tuy nhiên, Promise cũng không hoàn hảo. Một trong những điểm yếu của Promise là quản lý các Promise trở nên khó khăn và dễ gây ra hiện tượng "callback hell" tương tự callback. Xử lý lỗi có phần khó khăn khi áp dụng "chaining function". Nhận ra điều đó, Async/Await tiếp tục được giới thiệu.

Async/Await

Async/Await là một trong những nỗ lực để viết mã bất đồng bộ như là đồng bộ. Với Async/Await, chúng ta có thể viết mã bất đồng bộ giống như mã đồng bộ thông qua việc sử dụng cú pháp đơn giản hơn và dễ hiểu hơn.

Async/Await dựa trên Promise để xử lý các tác vụ bất đồng bộ. Chúng ta có thể đánh dấu một hàm bằng từ khóa async, sau đó sử dụng từ khóa await bên trong hàm đó để đợi kết quả của một Promise.

Ví dụ, có thể viết một hàm bất đồng bộ để đọc một tệp tin như sau:

const fs =  require('node:fs/promises');

async function readFileAsync(filename) {
  try {
    const data = await fs.readFile(filename);
    console.log(data);
  } catch(err) {
    console.log(err);
  }
}

readFileAsync("readme.txt");

Async/Await cải thiện rõ rệt sự đọc và hiểu của mã, che giấu đi sự phức tạp của Promise, đồng thời giúp cho việc debug cũng trở nên dễ dàng hơn.

Tiếp xúc mạnh mẽ với JavaScript từ năm 2017, tôi được học cách xử lý bất đồng bộ bằng Promise và Async/Await nhiều hơn hẳn. Callback là một cú pháp hết sức lạ lẫm và khó hiểu bởi những bất tiện mà nó mang lại, khiến tôi phải nhiều lần đau đầu tại sao vẫn có người hay thư viện cung cấp cách xử lý bất đồng bộ bằng callback. Chỉ khi đào sâu vào quá khứ thì những nghi ngờ đó mới dần được sáng tỏ, callback đang dần được thay thế bằng Promise. Chính "cha đẻ" của Node.js cũng phải lên tiếng thừa nhận rằng việc không hỗ trợ Promise ngay từ đầu đã khiến Node.js lộn xộn như bây giờ.

Tổng kết

Callback, Promise và cuối cùng là Async/Await, JavaScript đã trải qua một sự tiến hóa đáng kể trong việc xử lý bất đồng bộ. Mô hình I/O không đồng bộ của Node.js đã mang lại nhiều lợi ích cho việc phát triển ứng dụng. Callback là cách tiếp cận nguyên thủy, trong khi Promise và Async/Await mang lại cú pháp đơn giản hơn và mã dễ đọc hơn. Mỗi phương pháp có ưu điểm và nhược điểm riêng, thế nên không nhất thiết phải dùng một trong số chúng.

Cao cấp
Hello

Tôi & khao khát "chơi chữ"

Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!

Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!

Xem tất cả

Đăng ký nhận thông báo bài viết mới

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.

Bình luận (0)

Nội dung bình luận...
Bấm hoặc cuộn mạnh để sang bài mới