Promise - Lời hứa ngọt ngào trong JavaScript (P.2)
Thế giới trước khi có Promise trông như thế nào? Hãy cùng Bizfly Cloud xem sự xuất hiện của Promise trong javascript có thực sự "ngọt ngào" như tên gọi của nó hay không nhé.
Hàm xử lí tuần tự - Hàm xử lí bất đồng bộ
Trước tiên ta sẽ đưa ra một ví dụ để hình dung rõ hơn hai loại hàm xử lí này, đầu tiên là hàm xử lí tuần tự, nơi mà mọi thao tác được xử lí nối tiếp nhau một trình tự chính xác đã được viết ra:
// add two numbers normally
function add (num1, num2) {
return num1 num2;
}
// call the function, you get result = 3 immediately
var result = add(1, 2);
Nếu bạn cần làm nhiều phép toán cộng, bạn chỉ việc viết chúng nối tiếp nhau, bởi vì hàm sẽ trả về kết quả ngay lập tức nên bạn có thể dùng được kết quả này để tính toán ngay sau đó. Ví dụ: cộng 4 số liên tiếp:
var num1, num2, num3;
// call the function to calculate 1 2 3 4. Result is: 10
num1 = add(1, 2);
num2 = add(num1, 3);
num3 = add(num2, 4);
print("1 2 3 4 = " num3);
Bạn đã quá quen với hàm xử lí tuần tự phải không nào, code bạn viết có thứ tự như thế nào thì kết quả chạy sẽ y như vậy, không có điều gì là quá khó hiểu cả.
Bây giờ, chúng ta sẽ xem xét tới các hàm xử lí bất đồng bộ, giả sử bạn cần truyền input là 2 số nguyên về một hàm xử lí ở server, server sẽ thực hiện tính toán và đẩy kết quả trả về cho bạn, code sẽ trông như sau:
// add two numbers remotely, by calling an API
var result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// print the result, you get result = "undefined"
console.log(result);
Bạn có biết lí do tại sao khi in ra kết quả thì lại xuất hiện giá trị undefined không?
Đó là vì khi bạn gửi dữ liệu về server để tính toán, bạn mất thời gian cho các việc truyền gửi dữ liệu qua mạng, đợi server xử lí, chờ phản hồi, … trong khi đó đoạn code này lại in ra giá trị của biến result ngay khi gọi, lúc này dữ liệu vẫn chưa trả về kịp, do đó mà xuất hiện giá trị undefined.
Thế giới trước ngày Promise xuất hiện: Callback là lựa chọn.
Bạn đã nghe nhắc tới callback chưa? Nếu chưa thì bạn nên tìm hiểu qua trước một chút về callback rồi sau đó quay lại đọc tiếp bài viết này. Trước khi Promise xuất hiện người ta dùng callback để xử lí các lệnh bất đồng bộ. Nói cách khác, callback là một hàm sẽ được kích hoạt sau khi ta lấy được kết quả trả về từ một xử lí định trước.
Xử lí ví dụ ở trên bằng callback sẽ trông như sau:
// add two numbers remotely, by calling an API
function addAsync (num1, num2, callback) {
// use the famous jQuery getJSON callback API
return $.getJSON('http://www.example.com', {
num1: num1,
num2: num2
}, callback);
}
// do the calculation: 1 2
addAsync(1, 2, success => {
// callback, you get result = 3 here
const result = success;
console.log(result);
});
Hàm addAsync() chúng ta khai báo ở trên sẽ thực hiện một tính toán bất đồng bộ và nhận vào một biến là callback, khi tính toán xong thì hàm addAsync() sẽ thực hiện kích hoạt hàm callback. Ở phía dưới khi ta gọi hàm addAsync() để tính toán, ta đã truyền vào một hàm callback để xử lí in giá trị kết quả ra màn hình.
Nếu bạn muốn thực hiện một xử lí cộng 4 số như trên ví dụ ở đầu bài viết thì như thế nào? Có còn giống như khi viết hàm xử lí tuần tự không? Câu trả lời là không. Bởi vì các xử lí tính toán là bất đồng bộ, nên ta phải tiếp tục sử dụng callback, đoạn code xử lí sẽ trông như thế này đây:
// do the calculation: 1 2 3 4
var num1, num2, num3;
addAsync(1, 2, success => {
// callback, you get num1 = 3 here
num1 = success;
console.log("1 2 = " num1);
addAsync(num1, 3, success => {
// callback, you get num2 = 6 here
num2 = success;
console.log("1 2 3 = " num2);
addAsync(num2, 4, success => {
// callback, you get num3 = 10 here
num3 = success;
console.log("1 2 3 4 = " num3);
});
});
});
Về mặt logic, ta hoàn toàn có thể xử lí được các yêu cầu đề ra ban đầu, tuy nhiên code trông có vẻ rất rườm rà và phức tạp. Viết mã kiểu thế này thì các xử lí được lồng vào nhau, hình thành các tầng xử lí riêng biệt, nếu các đoạn code lồng vào nhau quá nhiều thì sẽ hình thành callback hell (địa ngục callback). Cử thử hình dung đoạn code lồng vào nhau 10 cấp, nó thực sự là "địa ngục" đó.
Promise xuất hiện và giải cứu chúng ta
Với callback, rất có thể chúng ta sẽ sa chân vào "địa ngục", chúng ta vùng vẫy đến kiệt sức và mọi thứ vẫn cứ rối tung như vậy. Nhưng rồi, Promise xuất hiện, và giải cứu chúng ta như một vị cứu tinh với lời hứa ngọt ngào đầy sức mạnh.
Với Promise, chúng ta có thể đơn giản đoạn code trên như sau:
let resultA, resultB, resultC;
//simulate a async function
function addAsync(num1, num2) {
return new Promise( function(resolve, reject){
setTimeout(function(){
resolve( num1 num2 );
}, 500);
} );
}
//Execute async functions
addAsync( 1, 2 )
.then( success1 => {
resultA = success1;
console.log('resultA: ' success1);
return addAsync( resultA, 3 );
})
.then( success2 => {
resultB = success2;
console.log('resultB: ' success2);
return addAsync( resultB, 4 );
})
.then( success3 => {
resultC = success3;
console.log('resultC: ' success3);
console.log('total (1 2 3 4): ' success3);
});
Promise xuất hiện và giải cứu chúng ta khỏi rắc rối callback. Nó làm "phẳng" các xử lí bất đồng bộ theo cách mà chúng ta vẫn hay viết các đoạn mã xử lí tuần tự. Từ đó, cho dù chúng ta có phải xử lí bất đồng bộ bao nhiêu lần đi chăng nữa thì code xử lí của chúng ta trông vẫn rất nhẵn nhụi, rất dễ đọc và dễ hiểu.
Lưu ý: Nếu bạn viết Promise không khéo thì code xử lí vẫn sẽ "phân tầng". Nếu điều đó xảy ra, bạn nên biết rằng mình đã sử dụng Promise sai cách.
Bạn thậm chí còn có thể chạy song song các lệnh Async với Promise.
Không chỉ cung cấp cho ta một cách thức chạy bất đồng bộ để thay thế callback, Promise còn cho phép ta chạy một lần nhiều xử lí song song, với một cách thức gọi hàm rất đơn giản - một điều mà trước đây với callback ta rất khó để thực hiện:
//Async function using Promise
function testPromiseNomal(name) {
var time = Math.round(Math.random() * 2000);
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve("Hello " name);
}, time);
});
}
// Test promise
var names = ["John", "Peter", "Michael"];
// if the reject function is detected, the process will be stopped immediately
var names_mapped = names.map(function(name) {
return testPromiseNomal(name);
});
Promise.all(names_mapped).then(function(response) {
console.log(response);
});
Để chạy các xử lí bất đồng bộ một cách song song, chúng ta sử dụng hàm map của Array để trả về một mảng các Promise. Câu lệnh Promise.all() sẽ chờ cho đến khi tất cả các kết quả của Promise được resolved rồi mới thực hiện khối lệnh .then() sau đó.
Kết luận
Có thể thấy, Promise thay đổi hoàn toàn cách chúng ta ứng phó với các xử lí bất đồng bộ ở phía client, làm cho việc xử lí ở phía client trở nên đơn giản hơn rất nhiều so với việc dùng callback.
Nếu bạn chưa biết tới Promise, có lẽ là bạn vẫn còn đang vật lộn với những callback nặng nề và phức tạp, hãy thử sử dụng promise đi, biết đâu là bạn sẽ lại tự "hứa" với lòng mình rẳng: I promise, to not use callback anymore.
==> Xem thêm: Promise - Lời hứa ngọt ngào trong Javascript (P.1)