Đâ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ệuJavaScript 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 đầuTrong 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 setTimeout
2 và setTimeout
3. 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ự
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 setTimeout
2 và setTimeout
3 để 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, setTimeout
8 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 setTimeout
9 và setTimeout
0 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 setTimeout
9 vào trước nó. Sau đó, chúng ta có thể sử dụng từ khóa setTimeout
0 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 setTimeout
0 ngăn JavaScript gán giá trị cho các biến setTimeout
7 và setTimeout
8 cho đến khi lời hứa được giải quyết
Sức mạnh của từ khóa setTimeout
0 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 setTimeout
2
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 setTimeout
3. 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 XMLHTTPRequest
5 như bên dưới
Vì vậy, trong đoạn mã trên, JavaScript thực thi mã trong khối XMLHTTPRequest
6 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
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