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
  • Cảm ơn threads.net của nhà Meta vì nó là nguồn cảm hứng cho mình tạo ra chuyên mục này trên blog. Ban đầu hơi nghi ngờ về việc liệu tạo ra các bài viết ngắn như thế này có thu hút được người dùng, có ai ngày qua ngày quay trở lại đọc không, hay tất cả chỉ như dã tràng xe cát? Như mình đã nói rất nhiều là làm ra một tính năng không khó, nhưng vận hành nó làm sao cho hiệu quả mới là điều cần phải bận tâm.

    Giờ đây thời gian đã chứng minh tất cả. Chuyên mục Bài viết ngắn luôn đứng trong tốp 5 trang có lượt truy cập nhiều nhất trong ngày/tuần/tháng. Điều đó có nghĩa bạn đọc đã có thói quen quay trở lại nhiều hơn. Tại sao mình lại khẳng định như thế? Vì chuyên mục này gần như không hề được SEO trên các công cụ tìm kiếm như Google.

    Lại kể về thời xa xưa một chút. Thời gian đầu mình rất chịu khó đăng bài trên threads.net với hy vọng thu hút được nhiều người theo dõi, để từ đó khéo léo giới thiệu họ trở thành người dùng blog của mình. Nhưng càng về sau càng thấy "đuối" vì thuật toán của Threads ngày càng không phù hợp với định hướng của mình. Hay nói cách khác là nội dung tạo ra không ăn khách.

    Ví dụ các bài viết của mình thường mang khuynh hướng chia sẻ thông tin, tin tức, hoặc kinh nghiệm cá nhân rút ra sau khi học hoặc làm một cái gì đó. Dường như những bài viết như vậy không được đánh giá cao và thường bị chôn vùi chỉ sau hơn... 100 lượt xem. Hmm... Liệu vấn đề có phải là do mình? Biết thế sao không chịu thay đổi nội dung theo hướng phù hợp hơn với nền tảng?

    Mình đã quan sát Threads, các nội dung dễ lan toả nhất là có yếu tố gây tranh cãi hoặc một định kiến về vấn đề gì đó, đôi khi chỉ đơn giản là phát biểu "ngây ngô" một vấn đề gì đó mà họ biết chắc chắn có tương tác. Mà mình thì gần như là không hề thích định hướng người dùng theo nội dung kiểu này. Mọi người có thể bảo mình bảo thủ, mình chấp nhận. Mỗi người có định hướng nội dung và khán giả khác nhau, lựa chọn nằm ở họ.

    Thế là từ đó mình chủ yếu viết trên này. Chỉ thi thoảng có phát hiện hay lắm thì mới lên Threads "khoe". Ở đây hàng ngày vẫn có người vào đọc, dù cho bạn là ai thì mình tin chắc rằng các bạn nhận ra được thông điệp mà mình muốn truyền tải thông qua mỗi bài viết. Ít nhất chúng ta có chung một định hướng về nội dung. Đôi khi điều sợ nhất không phải là viết ra không ai đọc, mà là họ đọc xong rồi lãng quên trong phút chốc. Số lượng là quan trọng, nhưng chất lượng mới là thứ mang chúng ta lại gần nhau hơn.

    Cảm ơn tất cả 🤓

    » Xem thêm
  • Zed chắc là cộng đồng những nhà phát triển chịu khó lắng nghe người dùng nhất quả đất. Mới đây họ thêm tuỳ chọn để tắt tất tần tật tính năng AI có trong Zed. Trong khi nhiều bên khác đang muốn tích hợp sâu hơn và làm nhiều hơn với AI Agent. Quả là một nước đi táo bạo 🤔

    You Can Now Disable All AI Features in Zed

    » Xem thêm
  • Hôm nay mình đã cố gắng đi hẳn 8k bước trong một phiên để đo lường cho các bạn thấy. Quả là không ngoài dự đoán khi thời gian đi lên đến hơn 1 giờ và quãng đường ~6km 🤓

    À vài hôm nữa là hết tháng, tức là cũng tròn 1 tháng mình bắt đầu thói quen đi bộ mỗi ngày với mục tiêu 8k bước. Để đầu tháng sau mình tổng kết lại xem thế nào luôn ha.

    » 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

5 bài học sâu sắc

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào ngay!

Mỗi sản phẩm đi kèm với những câu chuyện. Thành công của người khác là nguồn cảm hứng cho nhiều người theo sau. 5 bài học rút ra được đã thay đổi con người tôi mãi mãi. Còn bạn? Hãy bấm vào 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...