Hướng dẫn is javascript non blocking
JavaScript seriesHello, lại là tớ đây. Dạo này đang bị sml với các môn cuối kì, lại gặp một chủ đề khá là khó trong JavaScript nữa nên đăng bài hơi lâu Show
Ok, chương này sẽ nói về bất đồng bộ trong JavaScript - một chủ đề "tuy không dễ nhưng mà lại rất khó mang về". Bài viết này là một phần của series JavaScript dành cho người không mới, giúp các bạn đã có kinh nghiệm code trong các ngôn ngữ khác nhanh chóng làm quen với JS. Nếu được rất mong nhận được sự ủng hộ và đóng góp ý kiến của mọi người để hoàn thiện series. A. Introduction1. Synchronous & IO blockinga. Synchronous programmingLập trình đồng bộ (synchronous programming) là cách lập trình mà các câu lệnh chạy tuần tự nhau, lệnh trước chạy xong thì lệnh sau mới có thể chạy.
Như vậy, khi xử lý các tác vụ IO như đọc file, query database, chờ network,... nói chung là những công việc tốn thời gian, thì trong synchronous chương trình phải chờ đợi những việc kia hoàn thành xong thì mới chạy tiếp, do đó dẫn tới hiện tượng IO blocking. b. IO blockingLà hiện tượng xảy ra với code đồng bộ, khi thực hiện một tác vụ quá lâu (như ví dụ trên). Do đó, các câu lệnh sau phải chờ đợi một thời gian dài, và trình duyệt có thể không phản ứng với các sự kiện UI. Nếu ứng dụng hay trang web không phản hồi những sự kiện UI thì hệ điều hành sẽ nghĩ rằng ứng dụng đang bị treo. Khi đó con trỏ chuột có thể thành hình tròn xoay xoay, hoặc tệ hơn là ứng dụng bị lỗi "Not responding" (trên Windows). Đối với web thì nó có thể bị crash, người dùng không thể tương tác, không thể làm gì khác ngoài chờ đợi. 2. Asynchronous & non-blockinga. Asynchronous programmingĐể khắc phục vấn đề blocking của synchronous, người ta đưa ra khái niệm lập trình bất đồng bộ (không đồng bộ - asynchronous programming). Về cơ bản, bất đồng bộ là các câu lệnh chạy có thể không theo thứ tự, lệnh chạy trước có thể kết thúc sau câu lệnh chạy sau.
Bất đồng bộ không làm chương trình chạy nhanh hơn, đúng ra nó chỉ tránh lãng phí thời gian chờ đợi vô ích các tác vụ IO, network,... trong khi những tác vụ đấy thực hiện thì làm việc khác, khi nào chúng xong sẽ có cơ chế để thông báo lại cho mình. Bất đồng bộ có hai dạng:
Đối với bất đồng bộ đa luồng, thì có thể thực thi nhiều công việc cùng lúc (mỗi việc thuộc một thread). Đối với ngôn ngữ đơn luồng như JS thì bất đồng bộ có một số điểm đặc biệt, sẽ được trình bày sau. b. Non-blockingBất đồng bộ giải quyết được vấn đề blocking của đồng bộ, được gọi là non-blocking. Nghĩa là khi chạy một tác vụ nặng (IO, network,...) thì những lệnh tiếp theo được phép chạy ngay mà không cần chờ tác vụ kia hoàn thành. 3. JavaScript asynchronous?Người ta thường nói JavaScript là ngôn ngữ bất đồng bộ, nhưng thực tế không hẳn như vậy. a. JS luôn đồng bộ & blockingĐúng là JS có những cơ chế hỗ trợ lập trình bất đồng bộ (callback, promise, async/await), nhưng bản thân JS runtime hoàn toàn không phải. Bạn không thể nào viết một hàm với callback, promise hay async/await và hi vọng nó hoạt động asynchronous và non-blocking.
Code JS là đơn luồng và hoàn toàn đồng bộ. Chỉ có những tác vụ sử dụng WebAPIs (do browser) như AJAX, timeout,... thì JS mới thực thi chúng dạng bất đồng bộ. (hoặc một số API như AJAX của jquery cho phép cả hai chế độ). Do việc gọi và dùng các hàm WebAPIs tương tự như code bình thường, nên có thể bạn hiểu nhầm code JS cũng là bất đồng bộ.
Thú thực tôi đã tìm hiểu async trong JS khá lâu rồi, và vì không biết được câu trên nên tôi đã lãng phí khá nhiều thời gian của mình vì một việc vô ích. b. Callback, promise,... là gì?Ok, chúng ta vừa biết được chỉ khi JS thực hiện các hàm của WebAPIs thì nó mới bất đồng bộ. Vậy những kĩ thuật như callback hay promise dùng để làm gì?
Hỗ trợ ở đây có nghĩa, khi bạn thực hiện một hàm bất đồng bộ, đôi lúc bạn sẽ cần đợi nó hoàn thành để làm thêm việc gì đó. Nhưng nếu đợi nó bằng cách cho nó chạy dạng đồng bộ thì bị blocking. Do đó, các kĩ thuật như callback hay promise được sinh ra để thực hiện các công việc mà yêu cầu hàm bất đồng bộ hoàn thành. Ví dụ, bạn cần đợi hàm AJAX (bất đồng bộ) thực hiện xong để lấy dữ liệu, nhưng không muốn web bị blocking. Bình thường, nếu lấy dữ liệu ngay thì sẽ không lấy được do AJAX chưa thực hiện xong. Do đó, bạn dùng callback (hoặc promise) để giúp bạn thực hiện lấy dữ liệu ngay sau khi AJAX hoàn thành. Nghĩa là một callback dùng lấy dữ liệu nhận được sẽ được gọi. c. Code quá chậm thì sao?Tìm hiểu về WebWorker, về cơ bản nó giúp tạo ra một thread mới để thực thi code JS, và những tác vụ tiếp theo trong thread hiện tại sẽ không bị ảnh hưởng. Nhược điểm là worker thread không thể truy cập vào DOM và một số object của JS như window, document,... B. How does it work?1. Conceptsa. Execution contextHiểu đơn giản, execution context là môi trường mà hàm thực thi. Một execution context chứa code của một hàm và tất cả những thứ liên quan mà hàm cần dùng (biến cục bộ, inner function,...). Trong hình vẽ, thì có thể tưởng tượng execution context là một cái hộp chứa bên trong là một function. Execution context có 3 loại:
b. Call stackStack là cấu trúc dữ liệu dạng LIFO (Last In First Out), là thứ gì đưa vào stack sau sẽ được lấy ra đầu tiên. Có thể tưởng tượng nó như một chồng đĩa, đĩa nào nằm trên cùng (được đặt vào sau cuối) thì sẽ được lấy ra đầu tiên. Call stack cũng là một stack, và nó chứa tất cả execution context trong chương trình. Như ví dụ trên thì chồng đĩa là call stack, thì những cái đĩa là các execution context. Và chỉ có context nằm trên cùng (trên đỉnh) của stack thì mới được thực thi. Quy trình hoạt động của call stack:
Vì JS là đơn luồng, nên nó chỉ có một callstack. Điều đó đồng nghĩa với việc tại một thời điểm, chỉ có một lệnh trên cùng (top) của call stack được thực thi. c. Task queueQueue là cấu trúc dữ liệu hơi khác với stack, có dạng FIFO (First In First Out), vào trước ra trước. Tưởng tượng giống như khi xếp hàng vậy. Task queue (hay còn gọi là callback queue) là nơi chứa các callback đi kèm với các hàm bất đồng bộ. Quy trình hoạt động:
Ngoài task queue, còn có job queue, là một queue thứ 2 nhưng dùng xử lý các callback của promise. Và job queue có độ ưu tiên cao hơn callback queue, do đó các job sẽ được chạy trước callback. d. Web APIs & C++ APIsWebAPIs (hoặc C++ API bên Node.js) là những API được trình duyệt hoặc platform cung cấp cho JavaScript sử dụng. Các API có thể kể tới như:
JS sử dụng các hàm này tương tự như việc gọi hàm bình thường, nhưng thường thì các hàm này sẽ gọi theo kiểu bất đồng bộ (mặc dù một số API cho phép dùng cả 2 kiểu). Và nên dùng bất đồng bộ vì thường những thao tác với WebAPIs đều khá tốn thời gian. Quy trình hoạt động:
Có hai chú ý như sau:
e. Event loopVề cơ bản event loop là một vòng lặp vô tận của V8 engine, chỉ để thực hiện hai việc:
Khi hết stack, hết tất cả queue, thì event loop vẫn không dừng lại. 2. Work flowOk, phần lý thuyết ở trên hơi nhiều, nên bây giờ hãy đi vào thực tế cách hoạt động của chúng. Một trang siêu hay về phần này là trang http://latentflip.com/loupe/ như ví dụ bên dưới. Bạn viết code, và xem luồng chạy của nó, luồng đi của từng context trong từng call stack, queue, WebAPIs. nên nó bị thiếu mất một bên. Phần này mình sẽ nói sơ qua về nguyên tắc hoạt động của nó (trong đây mình dùng từ hàm cho ngắn gọn, đúng ra phải là "execution context của hàm đó")
3. Problemsa. Stack overflowVấn đề đầu tiên cần tránh là stack overflow. Đây là tình trạng stack bị đầy do chứa quá nhiều execution context. Như code ví dụ sau.
Hàm Khi đó, trình duyệt sẽ báo lỗi Range Error. Cách tránh thì đơn giản thôi, tránh việc gọi hàm vô tận, thường xảy ra khi đệ quy không có điều kiện dừng như trên. C. Async techniques1. Callbacka. Callback?Callback là kĩ thuật truyền một function (nó là callback) vào một function khác làm tham số, và khi function kia thực thi xong nó sẽ gọi lại thằng callback được truyền vào. Có hai dạng callback:
Callback đảm bảo một function (callback là nó) phải được thực thi sau khi một function khác hoàn thành (function nhận callback). Callback bình thường có thể thay thế bằng một đoạn code đồng bộ ngay phía sau, nên nó không cần thiết. Do đó, thường thì nhắc tới callback là nói tới async callback, được truyền cho hàm bất đồng bộ.
Chú ý đoạn code trên, sử dụng b. Error handlingThường thì các WebAPIs hay C++ API bất đồng bộ khi sử dụng với callback, thì chúng thường tự mình xử lý lỗi và báo lỗi cho callback bằng một hoặc hai tham số. Ví dụ như method
Do đó, cần kiểm tra request có thành công hay không, chỉ khi không có lỗi thì callback mới thực hiện xử lý data nhận được. c. Callback hellKhi bạn muốn thực hiện nhiều công việc bất đồng bộ liên tiếp nhau, thì sẽ dẫn tới callback hell. Về cơ bản, callback hell chỉ là những callback lồng vào nhau, nhưng vì quá nhiều nên sẽ làm code khó đọc, khó hiểu. Ví dụ sau sẽ tải liên tiếp 3 file theo thứ tự
Thử tưởng tượng cần phải tải nhiều file hơn nữa thì sẽ dẫn tới một đống callback lồng nhau. Do đó, nó sẽ sinh ra vấn đề là code siêu khó đọc,đó là chưa kể tới việc bắt lỗi cho từng callback. Từ đó, người ta tạo ra Promise để giải quyết vấn đề callback hell như trên. 2. Promisea. Promise?What is promise? Promise là đối tượng đại diện cho kết quả của hành động nào đó sẽ hoàn thành trong tương lai, kết quả trả về sẽ là Chúng ta sẽ thực hiện một hành động bất đồng bộ (trong hàm gọi là executor), và gắn thêm callback vào từng kết quả, từng trường hợp thành công hay thất bại.
Ví dụ khi thành công, thì những callback gắn với trường hợp
Bên trên là sơ đồ siêu đơn giản mô tả các promise hoạt động. Promise states Một promise từ khi tạo ra tới khi kết thúc sẽ có các trạng thái (state) sau:
Ngoài ra còn một trạng thái nữa là settled, là chỉ chung cho full filled và rejected. Khi đó promise thực hiện xong, không quan tâm kết quả. b. Promise workflowExecutor Promise về bản chất là một object. Constructor của nó nhận vào một hàm gọi là executor (thường là hàm ẩn danh), có cấu trúc như sau.
Executor function gồm 2 tham số là
Thường thì phần gọi Promise object Executor sẽ được truyền vào cho Promise constructor khi tạo một promise object.
Lúc này, một
promise object được tạo ra. Khi nó vừa tạo ra, nó thực thi lệnh ngay lập tức. Các lệnh trong executor sẽ lần lượt được thực hiện cho tới phần gọi Callback ở trong job queue sẽ chờ .Then & catch Khi lệnh bất đồng bộ thực hiện, luồng chạy sẽ tiếp tục các lệnh sau khi tạo object promise. Lúc này promise object có thể thuộc hai trường hợp:
Dù trường hợp nào đi nữa, thì chắc chắn callback trong job queue chưa thực hiện ngay, hoặc là nó chưa có, hoặc job queue đang chờ chưa được thực thi (vì job queue phải chờ stack rỗng, là không còn lệnh nào sẽ chạy nữa). Do đó, có thể nói rằng callback trong job queue luôn thực thi sau cùng. Luồng chạy vẫn tiếp tục các lệnh tiếp theo. Luồng chạy đi qua phần tạo object promise và tiến tới phần
Method Ending Cuối cùng, trong call stack không còn gì nữa, thì callback trong job queue được event loop đẩy vào trong call stack để chạy. Thằng callback này tùy vào kết quả promise, nó sẽ gọi các callback tương ứng với kết quả đó. Nếu trước đó:
Do đó, nhờ job queue luôn thực hiện sau cùng mà:
Từ đó, chức năng của promise được thực thi. c. Sending dataKhi gọi Như bên dưới là ví dụ cơ bản nhất.
Hãy thử xem lại workflow của promise và áp dụng vào ví dụ trên thử xem. d. Return promiseDo promise được gọi ngay sau khi tạo object, nên người ta cho nó vào một function. Function này sẽ return một promise khi được gọi.
Và rồi khi nào cần dùng thì gọi như
hàm bình thường, gán nó vào một tham chiếu và dùng
e. Promise chainingPromise được xem như phiên bản nâng cấp cho callback, bởi vì nó khắc phục được tình trạng callback hell khi phải lồng quá nhiều callback để thực hiện nhiều công việc bất đồng bộ liên tiếp. Promise thực hiện điều đó như thế nào. Đó là do method
Chú ý có thể chain nhiều Ok, đoạn code trên nhiều bạn sẽ bảo là sao không viết gọn lại thành như vầy cho dễ.
Đúng là ví dụ trên quá đơn giản, cả hai lệnh
Thêm một cách chữa khác. Cách này có vẻ tốt hơn cách 1, tuy nhiên nó lại bị vấn đề tương tự callback hell.
Cả 2 cách vừa nêu đều có nhược điểm, và đây là cách tốt nhất.
Code như trên lợi dụng tính năng Code như trên sẽ tránh được callback hell,
vì các 3. Promise methodsa. Then, catch, finally methodsThen & catch methods Method Method
Finally method Ngoài ra, còn một method khác của promise cũng dùng để gắn callback là
Ba method trên đều nhận vào tham số là callback, và thường được viết dạng arrow function cho nó ngắn gọn. b. Promise constructor methodsPromise.resolve & Promise.reject Hai method này khá là hay, dùng để return value bình thường thành dạng promise.
Vậy tại sao phải dùng chúng? Ok lý do siêu đơn giản, là để pass nhanh value từ mắt xích Mặc định nếu một mắt xích không return promise, thì
Cách 1 thì nếu return một function đã return promise , kiểu nhưreturn downloadFile('1.txt') thì dễ rồi, nếu như phải return một
thứ gì đó không phải function như trên sẽ khá khó. Do đó dùng cách 2.Promise.all & Promise.any Promise.allSettled & Promise.race 4. Async/awaita. Async & awaitHai từ khóa async/await được bổ sung vào ECMAScript 2017 để hỗ trợ sử dụng promise được tốt hơn. Như những ví dụ về promise ở trên thì nó khá là dài dòng và rắc rối. Async/await được sinh ra để giải quyết vấn đề đó, giúp code gọn gàng hơn. Async/await làm cho code bất đồng bộ trở nên có vẻ giống đồng bộ, do đó dễ đọc hơn. Tuy nhiên, chỉ nên dùng khi biết rõ về nó, có những trường hợp không thể dùng async/await được. Async keyword Từ khóa async được thêm vào đầu khai báo function, dùng để biến function đó thành function sẽ return một promise. Và câu lệnh return trong function nếu có sẽ trở thành return data cho promise đó. Ví dụ như đoạn code sau.
Function Await keyword Từ khóa await được đặt ở trước lời gọi một async function hoặc một promise, để tạm dừng luồng chạy cho tới khi promise trả về kết quả (async function cũng là promise).
Từ khóa await chỉ được sử dụng bên trong một function có đánh dấu async. b. ExampleNhớ lại ví dụ về hàm
Well, một vấn đề gần giống callback hell xuất hiện, với tên gọi promise hell. Vấn đề xuất hiện khi có quá nhiều promise liên tiếp nhau do promise chaining. Hãy cùng viết lại code trên với async/await để xem cách nó giải quyết promise hell như thế nào.
Code đã rõ ràng và sáng sủa hơn rất nhiều rồi, và trông nó giống code đồng bộ hơn là bất đồng bộ. Vì lẽ đó, nó làm code dễ đọc và phù hợp với suy nghĩ logic hơn. c. Error handlingNếu như đối với promise, chúng ta sử dụng catch callback để bắt trường hợp lỗi (thất bại), thì với async/await, chỉ cần dùng cấu trúc Ví dụ trên về
Xong, tất cả chỉ có vậy. Cấu trúc trên vừa bắt lỗi đồng bộ, vừa có thể bắt được khối catch của promise khi promise nào đó thất bại. Rất dễ dàng và tiện dụng. d. Why async/awaitAsync/await có khá nhiều lợi ích, như việc giúp code rõ ràng hơn, ngắn gọn hơn, và xử lý lỗi đơn giản. Tuy nhiên, nó cũng có một số hạn chế như sau. Thứ nhất là vì async/await thực thi code bất đồng bộ như code đồng bộ, nên await sẽ chặn các lệnh phía sau để chờ một promise nào đó hoàn thành. Vì vậy, chỉ khi bạn thực sự biết await sẽ làm gì thì mới nên dùng, vì nó có thể gây ra blocking. Thứ hai là tính tương thích trình duyệt, những trình duyệt cũ không hỗ trợ async/await, nên nếu bạn muốn dùng bạn cần sử dụng thêm công cụ khác bên thứ ba như Babel để chuyển code async/await thành dạng promise. |