Bất đồng bộ trong javascript
Nguyễn Dương 20-06-2024- Phần lớn các code JS đều là đồng bộ
- Đồng bộ có nghĩa là code được thực thi từng dòng một theo thứ tự từ trên xuống dưới
- Mỗi dòng code luôn đợi dòng trước thực thi xong rồi mới thực thi
Điều này dẫn đến vấn đề đối với một dòng code mất nhiều thời gian để chạy, việc thực thi dòng code đó sẽ chặn những dòng code khác thực thi
- Bất đồng bộ là code sẽ được thực thi sau khi một task chạy ở background kết thúc. (Hàm setTimeout là bất đồng bộ vì nó sẽ được thực thi sau khi timer chạy trong BACKGROUND kết thúc)
- Code ở luồng chính không bị chặn
- Việc thực thi không phải đợi một task bất đồng bộ phải thực hiện xong
- Lập trình bất đồng bộ là việc điều chỉnh hành vi của chương trình trong một khoảng thời gian nhất định - câu lệnh nào thực thi trước, câu lệnh nào thực thi sau
- Một hàm callback đứng một mình không làm cho code trở nên bất đồng bộ
- EventHandler đứng một mình cũng không làm code bất đồng bộ
- Ajax (Asynchronous Javascript And XML) cho phép chúng ta giao tiếp với các máy chủ web từ xa theo cách không đồng bộ
- Với Ajax chúng ta có thể yêu cầu dữ liệu từ web server một cách linh động
- Giả sử ứng dụng JS đang chạy trong trình duyệt được gọi là client. Chúng ta muốn ứng dụng web nhận được một số dữ liệu từ một web server, sử dụng ajax chúng ta có thể thực hiện một request đến server, server sẽ phản hồi và cung cấp dữ liệu mà chúng ta yêu cầu. Điều này diễn ra qua lại giữa client và server - tất cả diễn ra bất đồng bộ trong background.
API là gì
- API (Application Programming Interface) là một phần của phần mềm có thể được sử dụng bởi một phần mềm khác, cho phép các ứng dụng nói chuyện với nhau và trao đổi thông tin
- Trong JS và lập trình web có nhiều loại API: DOM Api, Geolocation Api, Online Api, ... và chúng ta cũng có thể tự triển khai các API của mình
- API như một phần mềm độc lập cho phép các phần mềm khác tương tác với chúng
Online API
- Online API là một ứng dụng chạy trên web server, nơi nhận yêu cầu dữ liệu, sau đó truy xuất dữ liệu từ database và gửi lại cho client
- Trong thực tế các Online Api được gọi là API
- Chúng ta có thể tự xây dựng các API ở phía backend hoặc sử dụng API từ bên thứ 3
API data format với JSON
- XML là một định dạng dữ liệu được sử dụng rộng rãi để truyền dữ liệu lên web, nhưng ngày nay không còn ai sử dụng XML (mặc dù chữ X trong AJAX là XML) mà thay vào đó mọi người sử dụng định dạng JSON
- JSON là một đối tượng dạng JS nhưng được chuyển thành một chuỗi string
Promises và Fetch API
Sử dụng fetch API
- Với promises, chúng ta sẽ triển khai Ajax theo cách mới, không dùng XMLHttpRequest mà dùng Fetch API
- Fetch Api sẽ trả về một Promises
Định nghĩa promises
- Promises là một object được sử dụng như một placeholder cho kết quả trong tương lai của hành động bất đồng bộ.
=> Promises giống như một vùng chứa cho một giá trị được phân phối là bất đồng bộ.
Ngắn gọn hơn là một vùng chứa cho các giá trị trong tương lai.
Lợi ích của việc sử dụng promises
- Chúng ta không còn cần phải dựa vào các event và các hàm callback để xử lý các kết quả không đồng bộ, event và các hàm callback có thể đôi khi gây ra những sai sót khó lường
- Chúng ta có thể xâu chuỗi Promises cho một chuỗi hoạt động không đồng bộ thay vì hàm callback hell
Vòng đời Promises
- PENDING - trước khi nhận được kết quả của tác vụ bất đồng bộ. Trong thời gian này, các task bất đồng bộ vẫn thực thi trong background
- SETTLED - khi task kết thúc, promises đã nhận được xử lý và nhận 1 trong 2 giá trị: FULFILLED và REJECTED
2 trạng thái hoàn thành hoặc bị từ chối
- Fulfilled promises là promises thu về giá trị như chúng ta mong đợi
- Rejected promises nghĩa là đã xảy ra lỗi trong quá trình thực thi bất đồng bộ
Chúng ta có thể xử lý riêng đối với 2 trạng thái này.
Promise sẽ được xử lý và từ chối một lần và không thể thay đổi trạng thái đó.
Sử dụng và xây dựng Promises
- Chúng ta sử dụng promises khi đã có promises, hàm fetch sẽ xây dựng promises và trả lại dữ liệu để chúng ta sử dụng
Phương thức then
- Việc gọi hàm fetch sẽ trả về một promises, promises này vẫn đang chờ xử lý vì task bất đồng bộ vẫn đang chạy ở background
- Tại một thời điểm nhất định, promises sẽ được thực hiện và ở trạng thái Fulfilled hoặc Rejected
- Giả sử rằng promises ở trạng thái fulfilled và chúng ta có một giá trị trả về để sử dụng
- Để xử lý trạng thái fulfilled này, chúng ta có thể sử dụng phương thức then có sẵn trên promises
- Trong phương thức then, phải truyền vào một hàm callback mà chúng ta muốn thực thi ngay sau trạng thái fulfilled
- Hàm callback sẽ nhận được 1 đối số là giá trị kết quả của fulfilled promises - thường gọi là response
Xử lý response
- Dữ liệu chính nằm trong response là response.body - ở dạng ReadableStream và chưa thực sự đọc được
- Để lấy ra được body, ta phải sử dụng phương thức có sẵn trên các response của phương thức fetch là json()
- Phương thức then có thể được gọi liên tiếp để xử lý các chuỗi callback bất đồng bộ
JS Runtime
- JS Runtime là một container bao gồm tất cả những thứ cần thiết để thực thi code JS.
- Trọng tâm của JS Runtime là JS Engine, đây là nơi code được thực thi (callstack) và là nơi các object được lưu trong memory (heap).
JS chỉ có một luồng thực thi nên tại một thời điểm nó chỉ thực hiện được 1 tác vụ.
- Web APIs cũng là một thành phần trong JS Runtime, chứa một số API được cung cấp cho engine (Dom timer, fetch api, geolocation api, ...).
- Callback Queue là cấu trúc dữ liệu chứa tất cả các hàm callback sẵn sàng để thực thi, mỗi khi callstack trống, event loop nhận các lệnh callback từ callback queue và đặt chúng vào callstack để thực thi.
- Event loop là điều cần thiết để khiến các hành vi bất đồng bộ có thể thực thi trong JS.
Từ đó ta cũng có khái niệm non-blocking concurrency model, mô hình đồng thời là cách mà một ngôn ngữ xử lý nhiều thứ xảy ra cùng một lúc.
Event Loop
- Event loop có nhiệm vụ vô cùng quan trọng, đó là việc thực hiện điều phối giữa callstack và callback trong callback queue.
- Event loop là yếu tố quyết định chính xác thời điểm mỗi callback được thực thi.
- Event loop còn có vai trò điều phối toàn bộ JS Runtime.
- Ngôn ngữ JS không có khái niệm thời gian vì mọi thứ bất đồng bộ không xảy ra trong engine, runtime mới là nơi quản lý tất cả các hành vi bất đồng bộ, event loop quyết định code nào sẽ được thực thi tiếp theo. Engine chỉ đơn giản là thực thi code mà nó được nhận.
- Hình ảnh nhận tác vụ load bất đồng bộ trong môi trường WEB APIs thay vì trong call stack.
- Thêm sự kiện callback trong sự kiện load hình ảnh, callback này cũng là code bất đồng bộ vì chúng ta chỉ muốn thực thi nó khi ảnh đã được tải.
- addEventListener không đặt hàm callback trong CallBack Queue mà tiếp tục chờ trong WEB APIs cho đến khi sự kiện load được hoàn thành.
- Khi sự kiện load hoàn thành, môi trường WEB APIs đưa callback vào trong Callback Queue, callback tiếp tục chờ tới lượt.
- Khi callback đứng ở đầu và callstack đang rỗng, event loop chọn nó và đặt nó vào Call Stack để thực thi.
Tóm lại, môi trường WEB APIs, Callback Queue và Event loop giúp cho code bất đồng bộ được thực thi theo cách non-blocking.
Microtask queue
- Khi sử dung promises, mọi thứ hoạt động hơi khác, giả sử dữ liệu đã được lấy - quá trình fetch đã hoàn thành, các callback liên quan đến promises không đi vào Callback queue mà đi vào một hàng đợi đặc biệt là Microtasks Queue.
- Hàng đợi microtask queue có quyền ưu tiên hơn hàng đợi callback, event loop sẽ ưu tiên check callback trong microtask queue trước và thực hiện hết các microtask rồi mới check xuống callback queue thông thường.
Trạng thái await
- Trong một hàm async, từ khóa await được sử dụng để chờ đợi kết quả trả về của mệnh đề sau nó, await sẽ ngừng việc thực thi code tại câu lệnh hiện tại cho đến khi câu lệnh hoàn thành.
- Việc ngừng thực thi code trong hàm async không phải là blocking vì hàm này đang chạy bất đồng bộ trong background và nó không chặn luồng thực thi chính, không chặn call stack. Await làm chúng ta thấy giống như lập trình đồng bộ thường thấy nhưng về mặt cơ chế chúng là code bất đồng bộ.
- Với async/await chúng ta không còn gặp vấn đề với callback hell, có thể thay thế promises với phương thức then thành code trông giống như code đồng bộ bình thường.
- Async/await chỉ đơn giản là cú pháp trên phương thức then của promises.
Vai trò của await
- Async/await là giải pháp thay thế cho consuming promises, nó che giấu bản chất thực sự của mọi hoạt động đằng sau cơ chế này.
- async/await thật ra chỉ là một sự chuyển đổi code mà vẫn ngầm sử dụng Promises phía sau
Chạy song song các Promises
Chuyển từ chạy theo thứ tự sang chạy song song với promise.all()
- Nếu một promises bị từ chối thì promise.all cũng bị từ chối.
- Promise.all được gọi là một hàm tổ hợp vì nó cho phép chúng ta kết hợp nhiều promises.