Đây là nỗ lực của tôi để giải thích cách sử dụng code không đồng bộ trong Javascript ở mức rất cao.
Lưu ý: Tôi sẽ sử dụng các chức năng mũi tên trong bài đăng trên blog này. Về cơ bản, tôi sẽ thay thế các chức năng ẩn danh như vậy:
function() {} function(a) {} function(a,b) {}
có thể được thay thế bằng:
() => {} a => {} (a,b) => {}
Giả sử, bạn có một function
sẽ in ra một chuỗi trong khoảng thời gian ngẫu nhiên:
function printString(string){ setTimeout( () => { console.log(string) }, Math.floor(Math.random() * 100) + 1 ) }
Giờ hãy thử in ra các chữ A, B, C theo thứ tự:
function printAll(){ printString("A") printString("B") printString("C") } printAll()
Mở Console Chrome debug thử xem:
Bạn sẽ nhận thấy rằng A, B và C in theo thứ tự khác nhau và ngẫu nhiên mỗi lần bạn gọi printAll()
.
Điều này là do các chức năng này không đồng bộ. Mỗi hàm được thực hiện theo thứ tự, nhưng mỗi hàm độc lập với setTimeout
của riêng nó. Nó khá là khó chịu, vì vậy hãy để sửa lỗi với một callback.
Callbacks
Callbacks là một chức năng được truyền cho một chức năng khác. Khi chức năng đầu tiên được thực hiện, nó sẽ chạy chức năng thứ hai.
function printString(string, callback){ setTimeout( () => { console.log(string) callback() }, Math.floor(Math.random() * 100) + 1 ) }
Bạn có thể thấy đó là nó cực dễ dàng để sửa đổi chức năng ban đầu để làm việc với các callbacks.
Và giờ tiếp tục thử in ra chuỗi A, B, C lúc nãy một lần nữa:
function printAll(){ printString("A", () => { printString("B", () => { printString("C", () => {}) }) }) } printAll()
Như bạn thấy thì bây giờ mã xấu hơn rất nhiều, nhưng ít nhất nó cũng hoạt động! Mỗi lần bạn gọi printAll, bạn sẽ nhận được kết quả tương tự.
Vấn đề với các cuộc gọi lại là nó tạo ra một thứ gọi là “Callback Hell.” Về cơ bản, bạn bắt đầu lồng các hàm trong các hàm và nó bắt đầu rất khó đọc code.
Promises
Promises sẽ cố gắng khắc phục vấn đề này. Hãy để thay đổi chức năng ở trên để sử dụng Promises.
function printString(string){ return new Promise((resolve, reject) => { setTimeout( () => { console.log(string) resolve() }, Math.floor(Math.random() * 100) + 1 ) }) }
Bạn có thể thấy rằng nó vẫn trông khá giống nhau. Bạn bọc toàn bộ chức năng trong một Promise và thay vì gọi lại cuộc gọi, bạn gọi giải quyết(resolve) hoặc từ chối(reject) nếu có lỗi. Hàm trả về một đối tượng Promise.
Giờ hãy tiếp tục thử lại với ví dụ trên, hãy in các chữ cái A, B, C theo thứ tự đó:
function printAll(){ printString("A") .then(() => { return printString("B") }) .then(() => { return printString("C") }) } printAll()
Đây được gọi là Promise Chain. Bạn có thể thấy rằng kết quả trả về của hàm (sẽ là một Promise), và điều này được gửi đến chức năng tiếp theo.
Code không còn được lồng nhưng nó vẫn trông lộn xộn!
Bằng cách sử dụng các tính năng của các function
mũi tên, chúng ta có thể loại bỏ “wrapper” của function. Code trở nên sạch hơn, nhưng vẫn có nhiều dấu ngoặc đơn không cần thiết:
function printAll(){ printString("A") .then(() => printString("B")) .then(() => printString("C")) } printAll()
Await
Await về cơ bản là cú pháp tuyệt vời cho Promises. Nó làm cho code không đồng bộ của bạn trông giống như code đồng bộ / code thủ tục, dễ hiểu hơn cho con người.
Chức năng printString hoàn toàn không thay đổi từ Promises. Một lần nữa, hãy để in các chữ cái A, B, C theo thứ tự đó:
async function printAll(){ await printString("A") await printString("B") await printString("C") } printAll()
Vâng…Tốt hơn nhiều!
Bạn có thể nhận thấy rằng chúng tôi sử dụng từ khóa “async” cho function
printAll(). Điều này cho phép JavaScript biết rằng chúng tôi đang sử dụng cú pháp async/await và là cần thiết nếu bạn muốn sử dụng Await. Điều này có nghĩa là bạn có thể sử dụng Await ở cấp độ toàn cầu; nó luôn cần một hàm bao bọc. Hầu hết các mã JavaScript chạy bên trong một hàm, vì vậy đây không phải là vấn đề lớn.
Xin chờ một chút
Hàm printString không trả về bất cứ thứ gì và độc lập, tất cả những gì chúng tôi quan tâm là thứ tự. Nhưng nếu bạn muốn lấy đầu ra của hàm thứ nhất, hãy làm gì đó với hàm thứ hai, rồi chuyển nó sang hàm thứ ba?
Thay vì in chuỗi mỗi lần, hãy tạo ra một hàm sẽ nối chuỗi và truyền lại.
Callbacks
Đây là kiểu Callbacks:
function addString(previous, current, callback){ setTimeout( () => { callback((previous + ' ' + current)) }, Math.floor(Math.random() * 100) + 1 ) }
Và để gọi nó:
function addAll(){ addString('', 'A', result => { addString(result, 'B', result => { addString(result, 'C', result => { console.log(result) // Prints out " A B C" }) }) }) } addAll()
Không tốt lắm.
Promises
Với Promises thì sao:
function addString(previous, current){ return new Promise((resolve, reject) => { setTimeout( () => { resolve(previous + ' ' + current) }, Math.floor(Math.random() * 100) + 1 ) }) }
Gọi nó ra:
function addAll(){ addString('', 'A') .then(result => { return addString(result, 'B') }) .then(result => { return addString(result, 'C') }) .then(result => { console.log(result) // Prints out " A B C" }) } addAll()
Sử dụng các hàm mũi tên có nghĩa là chúng ta có thể làm cho mã đẹp hơn một chút:
function addAll(){ addString('', 'A') .then(result => addString(result, 'B')) .then(result => addString(result, 'C')) .then(result => { console.log(result) // Prints out " A B C" }) } addAll()
Điều này chắc chắn dễ đọc hơn, đặc biệt nếu bạn thêm nhiều hơn vào chuỗi, nhưng vẫn còn một mớ hỗn độn của dấu ngoặc đơn.
Await
Function vẫn giống như phiên bản Promise. Và để gọi nó:
async function addAll(){ let toPrint = '' toPrint = await addString(toPrint, 'A') toPrint = await addString(toPrint, 'B') toPrint = await addString(toPrint, 'C') console.log(toPrint) // Prints out " A B C" } addAll()
Có thể nói là tuyệt vời.. 😀
Tổng kết
Qua bài viết này hy vọng bạn có thể hiểu thêm về Callbacks, Promises và Async/Await trong JavaScript.
Bài viết được dịch từ FrontEnd Weekly của Medium, mình thấy bài viết khá là hay nên mạn phép dịch lại, câu từ văn chữ còn lủng củng mong các bạn thông cảm :P. Bạn có thể xem qua bài viết gốc ở link bên dưới, xin cảm ơn!
Nguồn: https://medium.com/front-end-weekly/callbacks-promises-and-async-await-ad4756e01d90