- vừa được xem lúc

Xử lý bất đồng bộ trong JavaScript Phần 1

0 0 10

Người đăng: Robin Huy

Theo Viblo Asia

Lại một năm mới đến, cơ hội kiếm một tấm áo mặc cho mùa hè cũng đến.

Nhân dịp năm mới chúc cho anh chị em trên Viblo luôn có sức khỏe dồi dào, tiền vào như nước tiền ra ào ào (có tiền thì phải tiêu chứ 😂).

Thôi không lan man nữa, mời các anh chị em dành ít phút đọc qua bài viết này để xem nó có giúp ích gì cho công việc của các bạn không, hay ít ra nó cũng giúp bạn giết thời gian sau những giờ code căng thẳng.

Xử lý bất đồng bộ trong JavaScript

Lập trình bất đồng bộ (asynchronous programming) là một trong những vấn đề cơ bản trong JavaScript, nhưng không phải ai cũng hiểu cặn kẽ, đa số chỉ làm theo thói quen. Mình đã làm một số dự án maintain và thấy rất nhiều lỗi khi xử lý bất đồng bộ khiến cho chương trình bị chạy chậm hoặc bị sai logic. Lúc mới phát triển dự án thì sẽ chưa thấy ảnh hưởng gì nhưng sau khi dữ liệu đủ lớn thì sẽ gây giật, lag hoặc một số bug tiềm ẩn.

Mình viết bài viết này để chia sẻ lại những hiểu biết cũng như kinh nghiệm của mình trong việc xử lý bất đồng bộ, hy vọng sẽ giúp các bạn tránh được các lỗi kể trên.

Lý thuyết

Trước tiên chúng ta sẽ ôn lại 1 chút lý thuyết cơ bản về bất đồng bộ trong JavaScript.

Thông thường khi viết ứng dụng, ta sẽ thực thi các hàm một cách tuần tự, từ trên xuống dưới như sau:

func1();
func2();
func3();

Đây gọi là xử lý đồng bộ (synchronous, viết tắt là sync). Code kiểu này đơn giản và dễ đọc, nhưng trong nhiều trường hợp, nếu viết như vậy lại làm chương trình chạy chậm đi.

Chúng ta có thể viết cách khác để cho các hàm này chạy song song (parallel), không theo thứ tự từ trên xuống nữa và chạy luôn cùng lúc. Như vậy tốc độ chương trình sẽ nhanh hơn, các hàm sẽ không cần phải đợi nhau nữa (blocking). Đây được gọi là hàm chạy bất đồng bộ (asynchronous, viết tắt là async).

Lập trình bất đồng bộ sẽ khó hơn vì có thể khi đọc code thì thấy thứ tự gọi hàm là func1, func2, func3 nhưng vì các hàm này chạy cùng 1 lúc nên có thể func3 lại chạy xong trước làm ảnh hưởng đến luồng logic.

Trong JavaScript sẽ có các hàm có sẵn là chạy sync hoặc async. Ví dụ các hàm xử lý chuỗi, số, ... là sync: toUpperCase(), substr(), ... Các hàm viết theo dạng callback thì là async: setTimeout(), fetch(), ...

Hoặc như trong NodeJS các bạn sẽ thấy cùng 1 tác vụ nhưng lại có đến 2 hàm như để ghi file chúng ta có fs.writeFile (async) và fs.writeFileSync (sync). Hàm async sẽ được khuyến khích hơn vì nó không làm chương trình bị block như sync. Ví dụ trường hợp thao tác đọc ghi file bị lỗi hoặc quá lâu thì chương trình sẽ bị block, phải chờ quá trình này hoàn tất mới thực hiện được các tác vụ phía sau.

Ok, lý thuyết chỉ tìm hiểu đến đây thôi, còn về lý thuyết sâu hơn như blocking, non blocking, event loop, ... các bạn hãy tự tìm hiểu thêm nhé. Vì lý thuyết nhiều quá thì sẽ dễ gây buồn ngủ nên chúng ta chuyển qua phần ví dụ thực hành luôn.

Ví dụ

Dưới đây sẽ là một số hàm sẽ được sử dụng trong các ví dụ:

  • setTimeout: Là một hàm built in chạy bất đồng bộ, dùng để trì hoãn (delay) việc thực thi hàm sau 1 khoảng thời gian.

  • Hàm này thì là hàm tự chế để thay thế cho setTimeout ở trên nhưng là chạy theo kiểu đồng bộ, có tác dụng chờ x giây để giả lập thời gian thực thi của 1 hàm.

function delay(x) { const start = new Date().getTime(); while (new Date().getTime() - start < x * 1000) {}
}

Ok, bây giờ sẽ đến phần ví dụ thực tế. Mình sẽ viết một chương trình mô tả quy trình luộc rau, vì chắc AI ai cũng biết luộc rau rồi 😂.

Ta sẽ có các hàm mô tả hành động như sau:

function soCheRau() { delay(3); console.log('Sơ chế rau.');
} function dunSoiNuoc() { delay(4); console.log('Đun sôi nước.');
} function luocRau() { delay(5); console.log('Luộc rau.');
} function votRau() { delay(3); console.log('Vớt rau, để nguội.');
}

Mỗi hàm mình giả lập chạy mất vài giây (còn trong thực tế thì sẽ là 5-10 phút cho 1 công đoạn).

Thực thi chương trình:

console.time('Total time');
soCheRau();
dunSoiNuoc();
luocRau();
votRau();
console.timeEnd('Total time');

Vậy tổng thời gian sẽ là xấp xỉ 15 giây (3+4+5+3). Các bạn có thể copy các đoạn code ở trên chạy thử ở Console của Browser để thử nghiệm.

Trong thực tế nếu bạn luộc rau theo cách trên thì khả năng cao là sẽ bị bố mẹ mắng vì không biết cách sắp xếp thời gian 😅.

Như vậy để tối ưu thời gian, mình sẽ chuyển qua cách làm đồng thời nhiều việc cùng 1 lúc. Viết lại chương trình trên theo cách asynchronous như sau (dùng setTimeout và hàm callback):

function soCheRau(callback) { setTimeout(() => { console.log('Sơ chế rau.'); if (callback) callback(); }, 3000);
} function dunSoiNuoc(callback) { setTimeout(() => { console.log('Đun sôi nước.'); if (callback) callback(); }, 4000);
} function luocRau(callback) { setTimeout(() => { console.log('Luộc rau.'); if (callback) callback(); }, 5000);
} function votRau(callback) { setTimeout(() => { console.log('Vớt rau, để nguội.'); if (callback) callback(); }, 3000);
}

Các hàm trên được viết lại theo kiểu callback, tức là cho phép truyền vào tham số là 1 hàm, và sẽ gọi lại (callback) hàm đó sau khi thực hiện xong code logic nào đó. Ở trên mình đặt luôn tên hàm là callback, còn thực tế các bạn có thể đặt tùy theo ngữ cảnh.

Bây giờ các hàm này đã chạy bất đồng bộ, nên nếu chúng ta gọi như này:

console.time('run');
soCheRau();
dunSoiNuoc();
luocRau();
votRau();
console.timeEnd('run');

Kết quả sẽ ra như này:

console.timeEnd còn chạy trước các console.log khác và các bước lung tung không theo đúng trình tự, vớt rau trước cả khi nước sôi 😂.

Đó là vì các hàm này chạy đồng thời cùng lúc, không phụ thuộc lẫn nhau, dẫn đến kết quả không như ý muốn. Trong thực tế chúng ta có thể tối ưu quá trình luộc rau bằng cách vừa sơ chế rau vừa đun sôi nước, nhưng cũng phải chờ nước sôi thì mới luộc rau và luộc rau xong thì mới vớt rau.

Do đó mình sẽ viết lại chương trình để gọi đồng thời 2 hàm soCheRau()dunSoiNuoc() cùng lúc. Sau đó khi cả 2 hàm đã chạy xong thì lại gọi tuần tự các hàm luocRau()votRau():

console.time('run'); // Thêm một biến đếm để kiểm tra số hàm callback đã thực hiện
let count = 0; // Hàm này để kiểm tra khi có 2 hàm callback đã thực hiện (soCheRau() và dunSoiNuoc())
function checkCallback() { count++; // Nếu đã có 2 hàm callback được gọi thì tiếp tục thực hiện các hàm tiếp theo if (count === 2) { // Gọi hàm luocRau() và truyền callback là hàm votRau() để khi luộc rau xong thì mới vớt rau luocRau(() => { votRau(() => { // Vớt rau xong thì kết thúc chương trình console.timeEnd('run'); }); }); }
} // Thực thi 2 hàm soCheRau và dunSoiNuoc cùng lúc, với callback là hàm checkCallBack ở trên
soCheRau(checkCallback);
dunSoiNuoc(checkCallback);

Kết quả:

Như vậy các hàm vẫn chạy đúng trình tự mong muốn và chúng ta đã rút ngắn tổng thời gian xuống còn khoảng 12 giây, tiết kiệm được 3 giây.

Trong thực tế nếu số lượng các hàm lớn thì hiệu suất (performance) sẽ được cải thiện đáng kể so với cách gọi tuần tự. Ví dụ như trong một màn hình cần gọi rất nhiều API, nếu cứ viết theo thói quen dùng async await và cứ await lần lượt từng lệnh call API thì đó lại chính là lập trình theo kiểu đồng bộ làm trang web load rất lâu.

Lập trình bất đồng bộ cũng sẽ có nhược điểm là khó xử lý hơn, đặc biệt nếu chúng ta chỉ dùng callback như ví dụ ở trên, sẽ dẫn đến callback hell khiến code vừa khó đọc vừa khó bảo trì. Các bạn thử tăng độ khó của ví dụ trên lên sẽ thấy code khó hơn và callback hell rõ ràng hơn:

Viết lại chương trình mô tả quy trình luộc rau ở trên nhưng bước sơ chế rau sẽ tách ra thành vặt rau và rửa rau. Khi đó chúng ta cần cho vatRau() + ruaRau() + dunSoiNuoc() chạy cùng lúc, nhưng vẫn phải đảm bảo vatRau() chạy xong rồi mới đến ruaRau().

Giờ đến lúc các bạn nên tự thực hành để hiểu rõ hơn về lập trình bất đồng bộ với callback trong JavaScript. Phần tiếp theo mình sẽ hướng dẫn tiếp về lập trình bất đồng bộ với Promise, async await và các cách "bắt lỗi" (handling error) để chương trình chạy chuẩn hơn.

See you again!

Bình luận

Bài viết tương tự

- vừa được xem lúc

Giới thiệu Typescript - Sự khác nhau giữa Typescript và Javascript

Typescript là gì. TypeScript là một ngôn ngữ giúp cung cấp quy mô lớn hơn so với JavaScript.

0 0 518

- vừa được xem lúc

Bạn đã biết các tips này khi làm việc với chuỗi trong JavaScript chưa ?

Hi xin chào các bạn, tiếp tục chuỗi chủ đề về cái thằng JavaScript này, hôm nay mình sẽ giới thiệu cho các bạn một số thủ thuật hay ho khi làm việc với chuỗi trong JavaScript có thể bạn đã hoặc chưa từng dùng. Cụ thể như nào thì hãy cùng mình tìm hiểu trong bài viết này nhé (go).

0 0 429

- vừa được xem lúc

Một số phương thức với object trong Javascript

Trong Javascript có hỗ trợ các loại dữ liệu cơ bản là giống với hầu hết những ngôn ngữ lập trình khác. Bài viết này mình sẽ giới thiệu về Object và một số phương thức thường dùng với nó.

0 0 147

- vừa được xem lúc

Tìm hiểu về thư viện axios

Giới thiệu. Axios là gì? Axios là một thư viện HTTP Client dựa trên Promise.

0 0 135

- vừa được xem lúc

Imports và Exports trong JavaScript ES6

. Giới thiệu. ES6 cung cấp cho chúng ta import (nhập), export (xuất) các functions, biến từ module này sang module khác và sử dụng nó trong các file khác.

0 0 105

- vừa được xem lúc

Bài toán đọc số thành chữ (phần 2) - Hoàn chỉnh chương trình dưới 100 dòng code

Tiếp tục bài viết còn dang dở ở phần trước Phân tích bài toán đọc số thành chữ (phần 1) - Phân tích đề và những mảnh ghép đầu tiên. Bạn nào chưa đọc thì có thể xem ở link trên trước nhé.

0 0 241