Làm cách nào chúng ta có thể đạt được tính không đồng bộ trong JavaScript?

Đây là bài #71 của loạt bài, chuyên khám phá JavaScript và các thành phần xây dựng của nó. Trong quá trình xác định và mô tả các yếu tố cốt lõi, chúng tôi cũng chia sẻ một số quy tắc chung mà chúng tôi sử dụng khi xây dựng SessionStack, một ứng dụng JavaScript cần mạnh mẽ và có hiệu suất cao để giúp các công ty tối ưu hóa trải nghiệm kỹ thuật số của người dùng của họ

Giới thiệu

JavaScript không đồng bộ là một trong những phần thiết yếu của ngôn ngữ vì nó chi phối cách chúng tôi xử lý các tác vụ chạy dài — chẳng hạn như tìm nạp dữ liệu từ máy chủ hoặc API

Nói một cách đơn giản, chúng ta có thể xem mã không đồng bộ là mã bắt đầu một tác vụ ngay bây giờ và hoàn thành nó sau. Chúng tôi sẽ giải thích chi tiết về vấn đề này khi chúng tôi tiếp tục trong bài viết nhưng trước đó, hãy cùng tìm hiểu về mã đồng bộ — bản sao của mã không đồng bộ

JavaScript, về bản chất, là một ngôn ngữ đồng bộ. Và điều này có nghĩa là JavaScript chỉ có thể thực thi một mã tại một thời điểm — từ trên xuống dưới

Hãy xem xét mã dưới đây

console.log(“logging line 1”);

console.log(“logging line 2”);

console.log(“logging line 3”);

Theo mặc định, JavaScript thực thi mã ở trên một cách đồng bộ. Và điều này có nghĩa là từng dòng một. Vì vậy, dòng 1 không thể được thực hiện trước dòng 2 và dòng 2 không thể được thực hiện trước dòng 3

Ngoài ra, JavaScript được gọi là ngôn ngữ đơn luồng. Và điều này về cơ bản có nghĩa giống như JavaScript là một ngôn ngữ đồng bộ — về bản chất

Một luồng giống như một chuỗi các câu lệnh được sắp xếp như trong hình bên dưới

Trong một luồng, chỉ một trong những câu lệnh đó có thể chạy tại một thời điểm nhất định. Và đây là mấu chốt của mã đồng bộ. một luồng đơn và một câu lệnh được thực thi tại một thời điểm

Bạn có thể tìm hiểu thêm về chủ đề trong bài viết trước của chúng tôi trong loạt bài này

Vì vậy, trong mã đồng bộ, mỗi lần chỉ có thể chạy một câu lệnh, mã đồng bộ được gọi là mã chặn

Để giải thích về điều này, chúng ta hãy giả sử câu lệnh 2 trong hình trên là một tác vụ dài hạn, chẳng hạn như yêu cầu mạng tới máy chủ. Kết quả của việc này là câu lệnh 3 và 4 không thể thực hiện được cho đến khi thực hiện xong câu lệnh 2. Do đó mã đồng bộ được gọi là “mã chặn”

Bây giờ, từ hiểu biết của chúng ta về mã đồng bộ, chúng ta thấy rằng nếu chúng ta có nhiều câu lệnh — các hàm trong một chuỗi thực hiện các tác vụ chạy dài, thì phần mã còn lại bên dưới các hàm này sẽ bị chặn chạy cho đến khi các hàm này hoàn thành tác vụ của chúng

Mẫu này có thể ảnh hưởng tiêu cực đến hiệu suất của chương trình của chúng tôi. Và đây là lúc mã không đồng bộ xuất hiện

Như đã lưu ý ở trên, mã không đồng bộ là mã bắt đầu một tác vụ ngay bây giờ và kết thúc sau. Và điều này có nghĩa là khi một chức năng không đồng bộ xử lý một tác vụ dài hạn được thực thi trong một luồng, trình duyệt sẽ di chuyển tác vụ dài hạn đó ra khỏi luồng đó và tiếp tục xử lý nó. Ngoài ra, trình duyệt đồng thời tiếp tục thực hiện các chức năng khác trong luồng đó nhưng thêm chức năng gọi lại vào luồng. Do đó, mã không đồng bộ không chặn luồng thực thi — vì vậy chúng được gọi là mã không chặn

Khi tác vụ chạy dài hoàn thành, hàm gọi lại được gọi khi các hàm khác trong luồng chính thực thi xong. Và hàm gọi lại này xử lý dữ liệu được trả về từ quá trình tính toán lâu dài

Do đó, mẫu lập trình không đồng bộ cho phép chương trình của chúng ta bắt đầu một tác vụ chạy dài và vẫn tiếp tục thực hiện các tác vụ khác trong luồng. Vì vậy, chúng tôi không phải đợi cho đến khi nhiệm vụ dài hạn đó kết thúc

Hãy giải thích về điều này với một số ví dụ mã

Hãy xem xét mã đồng bộ dưới đây

Xem xét ví dụ về mã không đồng bộ bên dưới

Trong đoạn mã trên, mã đồng bộ đã thực hiện tuần tự từng câu lệnh. Nhưng trong ví dụ mã không đồng bộ, việc thực thi mã không tuần tự

Trong ví dụ về mã không đồng bộ, chúng tôi đã sử dụng hàm setTimeout để mô phỏng một tác vụ dài hạn mất hai giây để hoàn thành. Do đó, câu lệnh 2 được in cuối cùng ra bàn điều khiển vì luồng thực thi không bị chặn. Do đó, các tuyên bố khác đã được thực hiện

Sau phần giới thiệu này, chúng ta sẽ đi sâu vào lập trình không đồng bộ trong JavaScript

Hãy bắt đầu trong phần tiếp theo

Bắt đầu

Trong phần giới thiệu, chúng tôi đã làm việc với một ví dụ nhỏ về mã không đồng bộ. Nhưng trong phần này, chúng ta sẽ đi sâu hơn bằng cách sử dụng các yêu cầu mạng thay cho các hàm setTimeout. Và để làm được điều này, chúng ta cần hiểu một số khái niệm như yêu cầu HTTP

Yêu cầu HTTP

Đôi khi, chúng tôi muốn hiển thị dữ liệu như bài đăng trên blog, nhận xét, danh sách video hoặc dữ liệu người dùng được lưu trữ trên cơ sở dữ liệu hoặc máy chủ từ xa trên trang web của chúng tôi. Và để có được dữ liệu này, chúng tôi thực hiện các yêu cầu HTTP tới máy chủ hoặc cơ sở dữ liệu bên ngoài

Các yêu cầu HTTP được thực hiện cho các điểm cuối API — URL được hiển thị bởi API. Và chúng tôi tương tác với các điểm cuối này để thực hiện các thao tác CRUD - đọc, tạo, cập nhật hoặc xóa dữ liệu

Trong bài viết này, chúng tôi sẽ làm việc với các điểm cuối từ JSONPlaceholder. Và trong phần tiếp theo, chúng ta sẽ tìm hiểu về các mẫu lập trình không đồng bộ được sử dụng để xử lý các yêu cầu mạng trong JavaScript

Các mẫu lập trình không đồng bộ

Các mẫu lập trình không đồng bộ trong JavaScript đã phát triển cùng với ngôn ngữ. Và trong phần này, chúng ta sẽ tìm hiểu cách các chức năng không đồng bộ đã được triển khai trong lịch sử trong JavaScript. Chúng ta sẽ tìm hiểu về các mẫu lập trình không đồng bộ như gọi lại, Lời hứa và Async-await

Ngoài ra, chúng ta sẽ tìm hiểu về cách tạo yêu cầu mạng với đối tượng XMLHTTPRequest và API tìm nạp

Thực hiện các yêu cầu HTTP với đối tượng XMLHttpRequest

Đối tượng XMLHttpRequest là API không đồng bộ cho phép chúng tôi thực hiện yêu cầu mạng tới điểm cuối hoặc cơ sở dữ liệu. API XMLHttpRequest là một mẫu JavaScript không đồng bộ cũ sử dụng các sự kiện

Trình xử lý sự kiện là một dạng lập trình không đồng bộ — trong đó sự kiện là tác vụ không đồng bộ hoặc tác vụ dài hạn và trình xử lý sự kiện là hàm được gọi khi sự kiện xảy ra

Hãy xem xét mã dưới đây

in một danh sách các bài viết như trong hình bên dưới

Lưu ý, để sử dụng mã ở trên trong môi trường Nodejs, bạn sẽ cần cài đặt một gói chẳng hạn như nút-XMLHttpRequest

Trong ví dụ của chúng tôi ở trên, đối tượng XMLHttpRequest sử dụng trình lắng nghe sự kiện lắng nghe sự kiện console.log(“logging line 2”);0. Và khi sự kiện này kích hoạt, trình xử lý sự kiện được gọi để xử lý sự kiện. Bạn có thể tìm hiểu tất cả những gì bạn cần biết về sự kiện và trình xử lý sự kiện bằng cách đọc bài viết trước của chúng tôi trong loạt bài này tại đây

Lập trình không đồng bộ Với Callbacks

Trong đoạn mã trên, bất cứ khi nào chúng tôi sử dụng lại chức năng console.log(“logging line 2”);1, chúng tôi sẽ in các bài đăng đã tìm nạp vào bảng điều khiển. Tuy nhiên, chúng ta có thể tính toán thêm với kết quả của các hàm console.log(“logging line 2”);1 bằng cách sử dụng một số mẫu lập trình không đồng bộ. Và pattern đầu tiên chúng ta sẽ tìm hiểu là callback pattern

Hàm gọi lại là một hàm hạng nhất được truyền dưới dạng đối số cho một hàm khác — — với kỳ vọng rằng hàm gọi lại sẽ được gọi khi một tác vụ không đồng bộ được hoàn thành

Trình xử lý sự kiện là một dạng của hàm gọi lại. Và trong phần này, chúng ta sẽ tìm hiểu cách nâng cao mã của mình bằng cách sử dụng lệnh gọi lại

Hãy xem xét mã dưới đây

Trong đoạn mã trên, chúng tôi đã sửa đổi hàm console.log(“logging line 2”);1 để sử dụng hàm gọi lại. Do đó, chúng tôi có thể gọi lại cuộc gọi để xử lý các kết quả khác nhau của yêu cầu mạng - nếu nó thành công hoặc nếu có lỗi

Ngoài ra, bất cứ khi nào chúng tôi sử dụng lại chức năng console.log(“logging line 2”);1, chúng tôi có thể chuyển một cuộc gọi lại khác cho nó. Do đó, chúng tôi đã làm cho mã của mình có thể tái sử dụng nhiều hơn và linh hoạt hơn

địa ngục gọi lại

Vì vậy, chúng tôi đã thấy rằng mẫu gọi lại giúp mã của chúng tôi có thể tái sử dụng và linh hoạt hơn. Nhưng khi chúng ta cần thực hiện một số yêu cầu mạng theo tuần tự, mẫu gọi lại có thể nhanh chóng trở nên lộn xộn và khó bảo trì

Nhưng trước khi chúng tôi giải thích chi tiết về điều này, hãy cấu trúc lại chức năng console.log(“logging line 2”);1 của chúng tôi như bên dưới

Trong đoạn mã trên, chúng tôi đã tạo URL tài nguyên động bằng cách chuyển đối số console.log(“logging line 2”);6 làm tham số đầu tiên cho hàm console.log(“logging line 2”);1. Do đó, khi chúng ta gọi hàm console.log(“logging line 2”);1, chúng ta có thể tự động chuyển bất kỳ URL nào chúng ta muốn

Bây giờ, nếu chúng tôi thực hiện các yêu cầu mạng mà chúng tôi đã đề cập ở trên, chúng tôi sẽ kết thúc với các cuộc gọi lại được lồng sâu như bên dưới

Mọi thứ thậm chí có thể trở nên tồi tệ hơn khi chúng ta lồng nhiều cuộc gọi lại vào trong các cuộc gọi lại. Và điều này được gọi là địa ngục gọi lại. Gọi lại địa ngục là nhược điểm của mẫu gọi lại

Để giải quyết vấn đề gọi lại, chúng tôi sử dụng các mẫu JavaScript không đồng bộ hiện đại như lời hứa hoặc console.log(“logging line 2”);9

Hãy cùng tìm hiểu về Promise trong phần tiếp theo

Lập trình không đồng bộ với Promise

Lời hứa là nền tảng của JavaScript không đồng bộ hiện đại và lời hứa được giải quyết hoặc bị từ chối

Khi một hàm không đồng bộ triển khai Promise API, hàm này sẽ trả về một đối tượng lời hứa — thường là trước khi thao tác kết thúc. Đối tượng lời hứa chứa thông tin về trạng thái hiện tại của hoạt động và các phương thức để xử lý thành công hay thất bại cuối cùng của nó

Để triển khai API lời hứa, chúng tôi sử dụng hàm tạo console.log(“logging line 3”);0 trong hàm không đồng bộ, như bên dưới

Trong ví dụ trên, hàm tạo console.log(“logging line 3”);1 nhận một hàm — nơi yêu cầu mạng được thực hiện, làm đối số. Và chức năng này có hai đối số. hàm console.log(“logging line 3”);2 và hàm console.log(“logging line 3”);3

Hàm console.log(“logging line 3”);2 được gọi để giải quyết lời hứa nếu yêu cầu thành công và hàm console.log(“logging line 3”);3 được gọi nếu yêu cầu không thành công

Bây giờ, khi chúng ta gọi hàm console.log(“logging line 3”);6, nó sẽ trả về một đối tượng lời hứa. Vì vậy, để làm việc với hàm này, chúng ta gọi phương thức console.log(“logging line 3”);7 — để xử lý dữ liệu được trả về nếu lời hứa được giải quyết và phương thức console.log(“logging line 3”);8 để xử lý lỗi nếu lời hứa bị từ chối

Hãy xem xét mã dưới đây

Với kiến ​​thức này, hãy cấu trúc lại chức năng console.log(“logging line 2”);1 của chúng ta để sử dụng API lời hứa

Hãy xem xét mã dưới đây

Đoạn mã trên triển khai Promises API và chúng tôi thấy rằng thay vì gọi các cuộc gọi lại trong trình xử lý sự kiện, chúng tôi đã gọi hàm console.log(“logging line 3”);2 nếu yêu cầu thành công và hàm console.log(“logging line 3”);3 nếu yêu cầu không thành công

Xâu chuỗi lời hứa

Chúng ta đã thấy cách chúng ta xâu chuỗi các lời hứa bằng cách gọi các phương thức setTimeout2 và setTimeout3. Chuỗi lời hứa rất hữu ích, đặc biệt là trong các trường hợp có thể dẫn đến địa ngục gọi lại - nơi chúng tôi cần tìm nạp dữ liệu theo trình tự như đã đề cập trong phần trước

Chuỗi lời hứa cùng nhau cho phép chúng tôi thực hiện lần lượt các tác vụ không đồng bộ một cách rõ ràng. Để giải thích rõ hơn về điều này, chúng tôi sẽ triển khai ví dụ địa ngục gọi lại bằng API Promise

Hãy xem xét mã dưới đây

Lưu ý, phương thức console.log(“logging line 3”);8 trong các lời hứa ở trên sẽ bắt mọi lỗi bất kể số lượng yêu cầu lồng nhau. Ngoài ra, xâu chuỗi các lời hứa, như đã thấy ở trên, mang đến cho chúng ta một cách rõ ràng hơn và dễ bảo trì hơn để thực hiện nhiều yêu cầu mạng một cách tuần tự

API tìm nạp gốc

Fetch API là một API khá hiện đại để thực hiện các yêu cầu HTTP trong JavaScript, nhưng nó có nhiều cải tiến hơn đối tượng XMLHttpRequest. Ngoài ra, API tìm nạp triển khai API hứa hẹn bên trong và cú pháp của nó yêu cầu ít mã hơn nhiều, do đó dễ sử dụng hơn

Fetch API chỉ đơn giản là một chức năng lấy tài nguyên — một điểm cuối làm đối số của nó và trả về một lời hứa. Do đó, chúng ta có thể gọi các phương thức setTimeout2 và setTimeout3 để xử lý các trường hợp mà lời hứa được giải quyết và bị từ chối

Chúng tôi có thể triển khai ví dụ của mình bằng cách sử dụng API Tìm nạp như bên dưới

Lưu ý, trong đoạn mã trên, setTimeout8 trả về một lời hứa, vì vậy chúng tôi tận dụng chuỗi lời hứa để xử lý nó

Ngoài ra, trong môi trường Nodejs, bạn sẽ cần cài đặt một gói chẳng hạn như tìm nạp nút để hoạt động với API Tìm nạp

Lập trình không đồng bộ Với Async Await

Các từ khóa setTimeout9 và setTimeout0 gần đây đã được đưa vào JavaScript. Và chúng cho phép chúng tôi xâu chuỗi các lời hứa với nhau theo cách rõ ràng và dễ đọc hơn nhiều

Mặc dù Promise API có rất nhiều cải tiến so với callback, nhưng nó vẫn có thể trở nên lộn xộn khi chúng ta xâu chuỗi nhiều promise lại với nhau

Nhưng với console.log(“logging line 2”);9, chúng ta có thể tách tất cả mã không đồng bộ thành một hàm không đồng bộ và sử dụng từ khóa đang chờ bên trong để xâu chuỗi các lời hứa với nhau theo cách dễ đọc hơn

Chúng ta có thể tạo một hàm không đồng bộ bằng cách thêm từ khóa setTimeout9 vào trước nó. Sau đó, chúng ta có thể sử dụng từ khóa setTimeout0 bên trong chức năng đó để xâu chuỗi các lời hứa

Hãy xem xét mã dưới đây

Trong đoạn mã trên, chúng tôi đã cấu trúc lại hàm console.log(“logging line 2”);1 từ việc sử dụng API Promise thành console.log(“logging line 2”);9. Và chúng ta có thể thấy rằng nó sạch hơn và dễ đọc hơn

Ngoài ra, từ khóa setTimeout0 ngăn JavaScript gán giá trị cho các biến setTimeout7 và setTimeout8 cho đến khi lời hứa được giải quyết

Sức mạnh của từ khóa setTimeout0 là chúng ta có thể xâu chuỗi nhiều lời hứa một cách tuần tự trong hàm không đồng bộ và mã vẫn không bị chặn. Vì vậy, đây là cách sạch hơn, dễ đọc hơn và dễ bảo trì hơn để xử lý các lời hứa so với việc sử dụng phương pháp setTimeout2

Xử lý lỗi

Khi triển khai Promise API, chúng tôi xử lý lỗi bằng cách gọi phương thức setTimeout3. Tuy nhiên, trong mẫu console.log(“logging line 2”);9, không có phương thức nào như vậy. Vì vậy, để xử lý lỗi khi sử dụng từ khóa console.log(“logging line 2”);9, chúng tôi triển khai console.log(“logging line 2”);9 bên trong khối XMLHTTPRequest5 như bên dưới

Vì vậy, trong đoạn mã trên, JavaScript thực thi mã trong khối XMLHTTPRequest6 và gọi hàm console.log(“logging line 2”);1. Và nếu lời hứa được giải quyết, dữ liệu JSON sẽ được ghi vào bảng điều khiển. Nhưng nếu lời hứa bị từ chối, mã trong khối console.log(“logging line 3”);8 sẽ chạy. Khi mã trong khối bắt chạy, hàm bắt sẽ nhận đối tượng lỗi được ném làm đối số và xử lý lỗi

Phần kết luận

Trong bài viết này, chúng ta đã tìm hiểu về JavaScript không đồng bộ. Và các mẫu đã phát triển như thế nào trong lịch sử từ cuộc gọi lại đến Lời hứa đến console.log(“logging line 2”);9. Ngoài ra, chúng tôi đã tìm hiểu về API tìm nạp gốc, đây là API javascript hiện đại để thực hiện yêu cầu mạng

Sau khi xem qua bài viết này, tôi hy vọng rằng bạn hiểu cách thức hoạt động ẩn của JavaScript không đồng bộ — ngay cả khi bạn sử dụng các API cấp cao như API tìm nạp hoặc mẫu console.log(“logging line 2”);9

Vì vậy, mặc dù tất cả chúng ta đều thích áp dụng các công nghệ mới, việc nâng cấp mã của chúng ta — lên các API hiện đại nên được bổ sung bằng thử nghiệm thích hợp. Và ngay cả khi chúng tôi cảm thấy mình đã thử nghiệm mọi thứ trước khi phát hành thì vẫn luôn cần phải xác minh rằng người dùng của chúng tôi có trải nghiệm tuyệt vời với sản phẩm của chúng tôi

Một giải pháp như SessionStack cho phép chúng tôi phát lại hành trình của khách hàng dưới dạng video, cho thấy khách hàng thực sự trải nghiệm sản phẩm của chúng tôi như thế nào. Chúng tôi có thể nhanh chóng xác định liệu sản phẩm của mình có hoạt động theo mong đợi của họ hay không. Trong trường hợp chúng tôi thấy có điều gì đó không ổn, chúng tôi có thể khám phá tất cả các chi tiết kỹ thuật từ trình duyệt của người dùng, chẳng hạn như mạng, thông tin gỡ lỗi và mọi thứ về môi trường của họ để chúng tôi có thể dễ dàng hiểu và giải quyết vấn đề. Chúng tôi có thể đồng duyệt với người dùng, phân khúc họ dựa trên hành vi của họ, phân tích hành trình của người dùng và mở khóa các cơ hội phát triển mới cho các ứng dụng của chúng tôi

JavaScript đạt được mã không đồng bộ như thế nào?

Lập trình không đồng bộ giúp có thể diễn đạt thời gian chờ các hành động chạy dài mà không làm đóng băng chương trình trong các hành động này. Các môi trường JavaScript thường triển khai kiểu lập trình này bằng cách sử dụng hàm gọi lại, các hàm được gọi khi hành động hoàn tất .

Làm cách nào để tạo hàm không đồng bộ trong JavaScript?

async function foo() { const p1 = new Promise((resolve) => setTimeout(() => resolve("1"), 1000));

Làm cách nào để triển khai async trong JavaScript?

Hàm không đồng bộ JavaScript . Từ khóa “async” đi kèm với hàm, cho biết rằng hàm này trả về một lời hứa . Trong chức năng này, từ khóa chờ đợi được áp dụng cho lời hứa được trả lại. Từ khóa await đảm bảo rằng chức năng chờ lời hứa giải quyết.

JavaScript có chạy không đồng bộ không?

Javascript là ngôn ngữ đơn luồng đồng bộ nhưng với sự trợ giúp của vòng lặp sự kiện và lời hứa, JavaScript được sử dụng để lập trình không đồng bộ .