Async/Non-Blocking trong JS với ES6 (phần 1)
- June 28, 2021
- Posted by: codestar
- Category: Uncategorized
Chúng ta được nghe người khác nói, được đọc nhiều về Non-Blocking rồi Async, Promise các thứ … Liệu những cái đó là cái gì ? Có khó không ? Bài này sẽ giải thích cho các bạn. Thực ra Async function và Non-Blocking là 2 khái niệm độc lập nhau, tuy nhiên với đại đa số Sync function gây ra Blocking thì khái niệm Non-Blocking thường được nhắc đến khi sử dụng Async function.
Sơ lược về Non-Blocking
JavaScript là ngôn ngữ được xây dựng theo mô hình event loop. Nó khác những ngôn ngữ đồng bộ thông thường chạy lần lượt từ trên xuống dưới theo một main thread nhất định. Các hàm trong JS chạy Non-Blocking :
(function(){ console.log('A') const a = () => { console.log('B') } setTimeout(a,1000) console.log('C') })()
NOTE: Cho những ai thấy kỳ lạ về express function: (function(){})()
hoặc có nơi được viết là !function(){}()
thì thực ra, chỉ cần hiểu đơn giản, function(){}
là phần khai báo, (…)() là gọi hàm đó, về bản chất giống như:
const a = function(param){} a(parsing)
Đối với các ngôn ngữ Blocking, câu lệnh setTimeout(a,1000) sẽ chiếm tài nguyên hệ thống dù nó không dùng đến (Block) và sau 1s, thực hiện in ra B
và C
. Tuy nhiên với JS chạy Non-Blocking, việc chờ 1s sẽ được đưa ra tính thời gian đợi ở phía ngoài tài nguyên hệ thống (vì không sử dụng tài nguyên), hệ thống tiếp tục chạy câu lệnh phía sau in ra C
và sau 1s mới in ra B
.
Mục đích mà JS sử dụng Non-Blocking là để tách các event ở mức độ tương đối độc lập và chạy không bị Block giữa các event vì phần lớn JS được sử dụng để xử lý các event xuất hiện nhiều và khá độc lập.
Một lưu ý nữa là về kết quả Non-Blocking thể hiện giống như việc chạy đa luồng (multithread) tuy nhiên về bản chất, Non-Blocking không phải đa luồng mà là Singlethread các tác vụ xoay vòng trong Processing Queue. Cái này nếu có thời gian, mình sẽ viết một bài riêng về nó, nhưng hiện tại chỉ cần hiểu Non-Blocking thể hiện (gần) giống như chạy đa luồng.
Sơ lược về Async
Async là các thao tác được hiện bất ngờ, không được nhìn thấy lời gọi, mà chỉ viết các handler xử lý khi được gọi, không xác định được rõ ràng thời điểm được gọi, không xác định rõ thời điểm thực hiện xong. Event xử lý các sự kiện trong JS là các async function. Các listener thường được định nghĩa sẵn, và chỉ phải viết các handler cho event để xử lý khi được gọi.
<button onclick="onClickHandler()">OK</button>
function onClickHandler(){ //Write something here }
Ở ví dụ này, chúng ta có thể thắc mắc onClickHandler vẫn có lời gọi kia mà. Nhưng thực ra, nó nên được diễn giải thế này:
function onclick(){ onClickHandler() } function onClickHandler(){ //Write something here }
Ở đây, quả thực hàm onClickHandler không phải hàm async (vì nó cần phải được gọi), còn hàm async là hàm onclick(), được gọi khi bấm vào nút đó, DOM listener sẽ xử lý việc này cho chúng ta, chúng ta không cần quan tâm tới nó khi viết handler.
Promise trong ES6
Promise là một tính năng mới được giới thiệu trong ES6, rất hữu ích đối với những trường hợp có hiệu ứng bất lợi do Non-Blocking và Async đem lại.
Promise.then
Promise.then được sử dụng nhiều nhất vì nó loại bỏ Non-Blocking trong những trường hợp không mong muốn hay nói cách khác là nó Block lại theo ý muốn chủ quan của người viết.
function onClickHandler(){ const getData = (url) => { // Get data from given url } var result = 'Get data failed' result = getData('someurl.com') console.log(result) }
Giống như trường hợp đầu tiên, getData là 1 async function tốn thời gian (không thể trả về kết quả ngay), do vậy, trong lúc chờ đợi, hệ thống chạy tiếp câu lệnh console.log(result) và in ra Get data failed
, một lúc sau, result được nhận về, nhưng kết quả in ra lại là Get data failed
. Đây là 1 hệ quả không mong muốn của Non-Blocking, chúng ta mong muốn result được hiện ra sau khi đã chạy xong getData. Ở các phiên bản trước đó, chúng ta dùng như sau:
function onClickHandler(){ const getData = (url, cb) => { // Get data from given url cb(data) } var result = 'Get data failed' result = getData('someurl.com', function(dataResult){ console.log(dataResult) } ) }
Việc này không có vấn đề gì đặc biệt, ngoài việc khi viết nhiều function phụ thuộc vào lượt thực hiện trước đó như vậy sẽ gây khó khăn trong việc theo dõi luồng code (nhảy lung tung callback). Do vậy, ES6 cung cấp Promise.then giúp việc viết Blocking dễ dàng hơn:
function onClickHandler(){ return new Promise((resolve,reject) => {resolve()}) .then( () => { const getData = (url) => { // Get data from given url } var result = 'Get data failed' result = getData('someurl.com') return result }) .then((dataResult)=> { console.log(dataResult) } )}
Promise.all
Promise.all chạy tất cả các Promise thành phần. Nếu mọi Promise đều đã hoàn thành và trả về resolve thì Promise.all sẽ được resolve, ngược lại, khi có bất kỳ Promise nào trả về reject thì Promise.all sẽ chuyển thành trạng thái reject.
function onClickHandler(){ const getData = (url) => new Promise((resolve,reject) => { //getData from url. if(data.OK) { resolve(data) } else { reject('Error geting data') } }) return Promise.all([ getData('url1.com'), getData('url2.com') ]) .then((dataResultSuccess)=> { console.log(dataResult) //Do smt when all data get OK. } ) .catch( (error) => { console.log(error) }) }
Đoạn code sau sẽ hữu ích khi cần chờ lấy dữ liệu từ 2 API khác nhau và tổng hợp lại. Chúng ta cũng có thể sử dụng Promise.all trong trường hợp cần tạo event thực hiện mà người dùng cần thực hiện 1 số thao tác không đồng bộ.
Promise.race
Promise.race khác Promise.all ở chỗ, nó không cần chờ tất cả các Promise đã thực hiện xong, mà thực hiện theo Promise nào thực hiện xong sớm nhất. Promise.race được sử dụng khi cần lấy dữ liệu sớm nhất có thể, hoặc đợi một Promise trong một khoảng thời gian nhất định như trong Debounce hay Throttle.
Async/await trong ES8
Đây là phương án phổ biến và dễ tiếp cận cho những ai chưa quen với Promise trong ES6 và có thể sử dụng khá hiệu quả.
async function a(){ x = getData1() y = getData2() return await x+ await y }
Giả sử getData1() và getData2() là các hàm xử lý tốn thời gian/bất đồng bộ/không xác định thời điểm kết thúc. Cách viết phía trên tương đương với Promise.all() khi getData1 và getData2 được bắt đầu (gần như) cùng lúc và trả về kết quả khi đã nhận được kết quả của cả x và y. Nếu dữ liệu của y phụ thuộc vào x, chúng ta có thể viết thành:
async function a(){ x = await getData1() y = await getData2(x) return x+ y }
Trường hợp này tương đương với Promise.then(). getData2() sẽ được bắt đầu chạy khi getData1() kết thúc.
Generator trong ES6
Generator cũng thường được dùng để thay thế cho Promise trong những trường hợp đơn giản. Tuy nhiên sử dụng generator function yêu cầu cần phải nắm chính xác và rõ ràng luồng xử lý dữ liệu vì việc debug từ generator function khá khó khăn.
var a = (function* doSomething(){ for(let i=0;i<5;i++){ yield i }})() console.log(a.next()) console.log(a.next())
Tại vị trí yield, trả kết quả của hàm ra bên ngoài thông qua a.next() như trên sẽ ra kết quả là 0, và hàm sẽ chờ khi nào được cho phép chạy tiếp, nó sẽ chạy tiếp bằng a.next() và tiếp tục trả ra giá trị 1 và lại chờ tiếp. Khi xử lý bên ngoài hàm, chúng ta cũng có thể gán ngược lại giá trị cho hàm làm thay đổi logic của generator function. Ví dụ:
var a = (function* doSomething(){ for(let i=0;i<5;i++){ i = yield i } })() console.log(a.next()) console.log(a.next(3))
Với đoạn code này, ở lần next()
đầu tiên, tại yield
lần đầu đưa ra giá trị 0, tới lần tiếp theo, thì i được gán giá trị 3 và do vậy kết quả đưa ra là giá trị 4. Lưu ý, nếu ở lượt next thứ 3 không parse giá trị nào thì i ở trong generator function sẽ nhận được undifined. Do vậy khi parse giá trị từ ngoài vào, cần validate phía trong hoặc parse giá trị chính xác để tránh bị lỗi.
Trong ES8
Trong ES*, với async/await, chúng ta cũng có thể viết những function có chức năng tương đương:
var a = async (cb) => { for(let i=0;i<5;i++){ i = await cb(i) console.log(i) } } a((prev) =>{ if (prev==1) return 3; else return prev; } )
Đây chỉ là ví dụ đơn giản, nên 2 phương pháp thực sự không có nhiều khác biệt nhưng khi những luồng xử lý phức tạp hơn thì việc viết theo ES8 sẽ phức tạp hơn do có thể sẽ xuất hiện nhiều những callback độc lập nhau. Ngược lại viết generator function có vẻ đơn giản hơn, tuy nhiên việc trả kết quả ra hàm gọi khá mơ hồ và khó xác định, đồng thời ở hàm gọi phía bên ngoài có thể xảy ra lỗi với những hàm Non-Blocking sử dụng chung biến/hàm khác.
Trên đây là một số khái niệm và cách sử dụng cơ bản của Async function và Non-Blocking có thể hữu ích cho mọi người mà mình biết. Ở bài sau, mình sẽ nói chi tiết hơn về 1 số trường hợp sử dụng async function/Non-Blocking hiệu quả.
Tác giả: Cao Văn Thành