Đồng bộ và bất đồng bộ trong javascript

Đồng bộ và bất đồng bộ trong javascript mang đến cho bạn kiến thức về xử lý đồng bộ,bất động đồng bộ và các cách vận dụng trong lập trình javascript.

A. Đồng bộ và bất đồng bộ trong javascript là gì

1. Xử lý đồng bộ (Synchronous)

Synchronous (đồng bộ / tuần tự) tức là code chương trình sẽ chạy tuần tự từ trên xuống dưới. Khi nào lệnh trên hoàn thành thì lệnh dưới mới được chạy.  Đây là cách viết code rất thường dùng.

Cái hay của nó là dễ kiểm soát quá trình xử lý. Cái hay thứ hai là dễ kiểm soát lỗi phát sinh. Còn điểm không hay của xử lý đồng bộ là gì?  Đó là do chạy theo thứ tự nên sẽ có trạng thái chờ, lệnh trên chạy quá lâu sẽ làm ảnh hướng đến các lệnh dưới.

Trong 2 ví dụ bên dưới, code chạy lần lượt từ trên xuống, lệnh trên chạy dù có lâu thì chỉ chỉ khi thực hiện xong, lệnh dưới mới chạy:

<script>
    console.log("Đời đẹp lắm");
    console.log("Em có biết không");
    /*Kết quả:
         Đời đẹp lắm
         Em có biết không
    */
</script>
<script>
 // ...
 thuchienNauCom();
 thuchienChienTrung();
 thuchienAnCom(); 
</script>

2. Xử lý bất đồng bộ (Asynchronous)

Asynchronous (bất đồng bộ/bất tuần tự): tức là code chương trình không hẳn tuần tự nữa, nhiều lệnh có thể thực hiện cùng lúc. Có khi lệnh dưới cho kết thúc và cho kết quả trước cả lệnh phía trên.

Cái hay của xử lý bất đồng bộ là gì? Đó là nó có thể tối ưu được sức mạnh của hệ thống, thứ hai giúp giảm thời gian chờ, giúp code chạy nhanh hơn.  Còn điểm không tốt của bất đồng bộ?  Không phải hệ thống nào cũng dùng bất đồng bộ được, thứ hai là khó làm quen xử lý và kiểm soát lỗi phát sinh.

Trong javascript, các hàm setTimeout, setInterval, fetch… là tiêu biểu cho các hàm xử lý bất đồng bộ trong javascript . Sau đây là mõt ví dụ xử lý bất đồng bộ:

<script>
    setTimeout( () => {console.log("Đời đẹp lắm");} , 1000);    
    console.log("Em có biết không");
    /*Kết quả:
        Em có biết không
        Đời đẹp lắm
    */
</script>
<script>
url=`https://api.openweathermap.org/data/2.5/weather?id=1566083&appid=YOUR_API_key`;
window.fetch(url).then( res => res.json()).then( data => console.log(data));
alert("Lấy thời tiết ở HCM");
</script>

3. Các phương án xử lý bất đồng bộ

Trong javascript có nhiều phương án xử lý bất đồng bộ : :

  • Dùng hàm Callback
  • Sử dụng Promise
  • Dùng async/await (của es6)

B. Xử lý bất đồng bộ bằng callback

Hàm callback là một hàm được truyền vào như tham số cho 1 hàm khác. Các ngôn ngữ lập trình khác nhau hỗ trợ callback theo cách khác nhau, thường được triển khai dưới dạng chương trình con, hàm nặc danh, chuỗi lệnh hoặc con trỏ hàm.

Rất nhiều hàm trong javascript sử dụng callback cho việc xử lý. Ví dụ:

<script>
//chọn ra các điểm >=8
var arr_diem = [8,2,6,9, 7,3];
arr_diemgioi = arr_diem.filter(function(v,i){
    if (v>=8) return true; else return false;
});
console.log(arr_diemgioi); // [8, 9]
</script>

1. Các cách để sử dụng hàm callback:

a. Sử dụng anonymous functions, tức hàm callback không có tên như ví dụ trên

b. Sử dụng 1 hàm đã được đặt tên ,  như ví dụ sau:

<script>
function hsT(v,i){    
    if (v.substr(0,1)=="T") return true; 
    else return false;
}    
</script>

<script>
var arr_hs = ["Tèo", "Lượm", "Tý", "Út", "Chanh"];
arr_hsT = arr_hs.filter(hsT);
console.log(arr_hsT);// ["Tèo", "Tý"]
</script>

Chú ý: hàm callback phải định nghĩa phía trước thì mới dùng được.

c. Tự định nghĩa 1 hàm sử dụng callback

Bạn có thể tự tạo 1 hàm sử dụng callback bằng cách nhận tenbien ở tham số sau đó thực thi tenbien() ở nội dung của hàm.

<script>    
function chuhoa(str){ console.log(str.toUpperCase());} 
function hiendanhngon(str){ console.log(str);} 
function xulydanhdanhon(callback){  if (callback) callback(); }
</script>
<script>
var  danhngon ="Vạn sự tùy duyên";
xulydanhdanhon(hiendanhngon(danhngon)); //Vạn sự tùy duyên
xulydanhdanhon(chuhoa(danhngon)); //VẠN SỰ TÙY DUYÊN
</script>

Ưu điểm của callback: Callback function là một cách thức phổ biến, dễ hiểu, dễ triễn khai. Nhược điểm là các callback nếu lồng nhau nhiều quá dễ dẫn đến tình trạng CallbackHell (sẽ được đề cập đến ở bài tới) – gây khó khăn khi sửa lỗi và bảo trì.


Giờ thì thực tập chút chơi: Viết hàm nhập môn học, sau đó alert ra môn đang học, nhập số điểm sau đó gọi hàm callback để hiển thị học lực.

<script>    
function xuly(tenmon, callback){
    alert("Môn đang học: " + tenmon);
    var điểm = prompt("Điểm bao nhiêu?????");
    callback(điểm, tenmon);
}
function thongbao(d, sub){
    if (d<5) alert("Rớt môn " + sub + " rồi bé yêu");
    else if(d<=7.5) alert("Chúc mừng nhé! Qua môn rồi");
    else alert("Bạn giỏi quá");    
}
</script>
<script> xuly("Javascript Nâng cao", thongbao); </script>

2. Callback hell

Với những vấn đề phức tạp, việc xử lý bất đồng bộ bằng callback nếu làm không tốt thì có thể xảy ra tình huống nhiều hàm callback lồng nhau (callback hell)

callback-hell

Callback hell là trạng thái code lồng nhau nhiều cấp dẫn đến hình thành hình kim tự tháp code, nhìn rất phức tạp, khó hiểu, khó debug lỗ,i khó maintain… Mời xem ví dụ:

<script>
function thucday(viec){
    console.log("Thức dậy nhớ rằng đời đẹp lắm");  viec();
}
function danhrang(viec){
    console.log("Đánh răng 50 cái, nhìn gương mỉm cười"); viec();
}
function diansang(viec){
    console.log("Cảm ơn đời, mình vẫn còn có cái mà ăn");viec();
}

function main(){//...
    thucday(function(){ //...        
        danhrang(function(){ //...
            diansang(function(){ //...
                console.log("Đi làm thôi")
            })
        })
    })
}

main();
    /*
    Thức dậy nhớ rằng đời đẹp lắm
    Đánh răng 50 cái, nhìn gương mỉm cười
    Cảm ơn đời, mình vẫn còn có cái mà ăn
    Đi làm thôi
    */
</script>

3. Các cách xử lý callback hell

Có nhiều cách xử lý Callback Hell, đó là:

  • Đặt tên cho hàm callback : các hàm callback cố gắng đặt tên cho chúng, đừng dùng hàm vô danh (không đặt tên), khó debug.
  • Thiết kế ứng dụng theo dạng module
  • Định nghĩa hàm trước khi gọi
  • Sử dụng Promises (ES2015 – ES6)
  • Sử dung Async/Await (ES2017 – ES8)

C. Promise trong Javascript

1. Khái niệm promise

Promise (lời hứa) là một đối tượng dùng trong những lúc xử lý bất đồng bộ. Một promise đại diện cho 1 tác vụ nào đó chưa hoàn thành ngay được. Ở 1 thời điếm trong tương lai, promise sẽ trả về giá trị khi thành công (resolve) hoặc thất bại (reject).

Bằng cách dùng Promise , bạn có thể kết hợp các hàm xử lý để sử dụng kết quả khi xử lý bất đồng bộ hoàn tất. Nhờ vậy mà bạn có thể lập trình bất đồng bộ gần giống với kiểu lập trình đồng bộ – tức là đợi xử lý bất đồng bộ xong mới thực thi các thao tác cần sử dụng kết quả của xử lý đó.

2. Cách tạo Promise

Tạo một Promise bằng cú pháp new Promise. Hàm constructor của Promise nhận 2 tham số là 2 hàm resolve và reject.

var promise = new Promise ( function(resolve, reject) {
     //code xử lý...
     if (đánh giá thành công) {
           resolve(value1); //value trả về khi ok
     } else {
           reject(value2) ; //value trả ra khi fail
     }
});

3. Ví dụ tạo promise

<script> diem =8;
var p = new Promise( function(resolvereject){
    //...
    if (diem>=9) cothuong=true; else cothuong=false;
    if (cothuong) resolve("Chúc mừng bạn");
    else reject("Cố gắng lần sau nhé");
});
</script>

Hai hàm trong Promise có thể đặt tên khác, không nhất thiết là resolve reject. Hàm đầu tiên (resolve) dùng để chạy khi task được đánh giá thành công (không có lỗi), nếu không thì hàm sau (reject) sẽ được gọi. Mỗi hàm chỉ được trả về 1 giá trị (mảng cũng được), nếu trả về nhiều hơn thì các giá trị sau sẽ bị bỏ qua.

4. Sử dụng promise đã tạo

Cách 1: Dùng then với 2 hàm, hàm đầu tiên chạy khi tác vụ trong promise OK, hàm sau chạy khi tác vụ trong promise là fail , 2 hàm cách nhau bởi dấu phẩy.

<script>
    p.then(
        function(v1) { console.log(v1)} ,
        function(err) { console.log(err)}
     );
    console.log("Hi");
</script>

Cách 2: dùng then và catch

<script>
    promise
    .then(function(v1){ console.log(v1)})
    .catch(function(err){ alert(err)})
</script>

5. Các trạng thái của Promise

Mỗi đối tượng promise có các trạng thái như sau:

  • Pending (đang xử lý) : ): là trạng thái mà promise đã tạo ra nhưng chưa được thực thi.
  • Fulfilled (resolve – đã hoàn thành) : là trạng thái mà Promise đã được thực thi và return kết quả resolve(). Khi hàm resolve được trả về – promise gọi đến lệnh .then() – để tiếp tục thực hiện logic tiếp theo.
  • Rejected (đã bị từ chối): Promise đã thực thi và return kết quả reject(). Khi hàm reject được trả về – promise gọi đến lệnh .catch() – để tiếp tục thực hiện logic tiếp theo.

5. Lợi ích khi sử dụng Promise

  • Tránh được Callback Hell
  • Code rõ ràng, dễ đọc , dễ debug hơn

Ví dụ sau dùng callback (chứ không dùng promise) thực hiện chuyển màu màn hình sang màu red sau 5 giây, sau đó 1 giây chuyển sang blue.

<script>
setTimeout(() => {
    document.querySelector("body").style.background="red";
    setTimeout(() => {
       document.querySelector("body").style.background="blue"; 
    }, 1000);
}, 5000);
</script>

Viết theo kiểu dùng callback như trên rối , khó debug. Nếu yêu cầu thêm : sau đó 3s đổi thành green, rồi sau đó 4s đổi thành pink thì bài toán lúc này rối lắm, code sẽ cực kỳ khó nuốt.

Code sau sẽ dùng promise để giải quyết yêu cầu giống như trên: đổi màu nền sang red sau 5s rồi 1s sau đổi sang blue. Tiếp 3s sau đổi sang green, rồi 4s sau đổi sang pink.

<script>
function xuly(color, time){
    return new Promise(function(rOK, rFail){
        setTimeout(() => {
           document.querySelector("body").style.backgroundColor=color;
           rOK("Đã chuyển màu"); 
        }, time);
    })
}

xuly("red", 5000)
.then(() =>xuly("blue",1000) )    //function(){xuly("blue",1000);}
.then(() =>xuly("green",3000) )    
.then(() =>xuly("pink",4000) )    
.catch( function(err) { alert("Lỗi " + err)})
</script>

Nhờ Promise mà code bất đồng bộ được xử lý dễ hiểu hơn, có vẻ đồng bộ hơn.

Ví dụ tiếp: một ngữ cảnh khác để dùng promise: nếu điểm môn js>=9 sẽ được 1 chuyến du lịch. Để biết có được đi hay không thì phải chờ 1 khoảng thời gian đến lúc thi xong. Do đó cần tạo 1 kịch bản và các hành động có thể xảy ra sau thời gian này (tạo ra promise)

<h2>Loading...</h2>
<script>
h2 = document.querySelector("h2");
quatrinhhoc = function (diem, tg){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            if (diem>=9) resolve(diem);
            else reject("Tiêu rồi")
        }, time);
    })
}

var diemTK = prompt("Nhập điểm coi");
var time = prompt("Thời gian");
quatrinhhoc(parseInt(diemTK), parseInt(time))
.then(function(mark){ h2.innerText="Good! welcome Singapore"; })
.catch(function(err){ h2.innerText=err; })
</script>

6. Sử dụng nhiều .then() liên tiếp

Bạn có thể sử dụng nhiều then liên tiếp nhau, kết quả trả về từ .then() trên sẽ trả cho then() dưới một cách tuần tự nếu bạn dùng promise trong các then(). Ví dụ 1:

<script>
var p = new Promise(function(resolve, reject){
    setTimeout(function(){
            console.log(1);
            return resolve();
    },1000)
})
console.log(p);

p.then(function(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log(2);
            return resolve();
        },4000);        
    })
})
.then(function(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log(3);
            return resolve();
        }
        ,3000);
    })
})
.then(function(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log(4);
            return resolve();
        }
        ,2000);
    })
})
</script>

Ví dụ 2 thực hiện các công đoạn giải phương trình bậc hai gồm : nhập dữ liệu, kiểm tra, tính nghiệm , báo lỗi

ptb2 = new Promise( function(r1, r2){
    var a = prompt("Mời nhập số a");
    var b = prompt("Mời nhập số b");
    var c = prompt("Mời nhập số c");
    r1( [ a, b, c]);
});

ptb2
.then(kiemtra)
.then(tinhnghiem)
.then(xuatketqua)
.catch(thongbao)    
document.getElementById("kq").innerHTML="<h2>Giải phương trình bậc 2</h2>";

function kiemtra(arr){
    return new Promise(function(r1, r2){
        if (isNaN(arr[0])==true || isNaN(arr[1])==true || isNaN(arr[2])==true  ){ 
            r2("Các hệ số a ,b , c cần phải nhập số")
        }
        else if (arr[0]>10|| arr[1]>10 || arr[2]>10 ){ 
            r2("Các hệ số a ,b , c  phải <=10")
        }
        else  r1(arr);
    })
}

function tinhnghiem(arr){ //arr = [a,b,c]
    return new Promise( function(r1,r2){
       var a = arr[0] , b = arr[1] , c = arr[2]; 
       var delta = b*b - 4*a*c;
       if (delta<0) {
           r2("Phương trình vô nghiệm");
       }
       if (delta==0){
           x1 = -b/(2*a);
           x2 = -b/(2*a)
       }
       if (delta>0) {
           x1=(-b - Math.sqrt(delta))/(2*a);
           x2=(-b + Math.sqrt(delta))/(2*a);
       } 
       r1( [x1, x2]);
    })
}    

function xuatketqua(data){
    str = `
        <h4>Nghiệm của phương trình là </h4>
        <p>X<sub>1</sub> = ${ data[0].toFixed(2) } </p>
        <p>X<sub>1</sub> = ${ data[1].toFixed(2) } </p>
    `;
    document.querySelector("#kq").innerHTML = str;
}
function thongbao (str){
    document.querySelector("#kq").innerHTML = str;
}

Chuyển kiểu viết callback thành promise

Như vậy, để chuyển kiểu viết callback thành promise, Bạn thực hiện gọi chuỗi hàm bằng lệnh then, mỗi hàm trong then trả về 1 promise

promise()
.then(promise1)
.then(primise2)
.then(promise3)
.catch(funcErr )

7. Sử dụng Promise.All

Có thể thực hiện nhiều promise cùng lúc mà hoạt động của chúng độc lập nhau nhưng kết quả của những promise đó cần thiết cho 1 hoạt động sau cùng.

Hàm Promise.all với tham số truyền vào là 1 mảng các promise cần thực hiện. Khi đó, tham số của .then() chính là 1 mảng chứa các kết quả từ các promise.

Ví dụ: điểm toán được nhập sau 4s, điểm lý được nhập sau 8s, sau đó tính điểm trung bình.

<script>
function diemtoan(){
    return new Promise(function(resolve, reject){
        setTimeout(() => {
            diemtoan = prompt("Nhập điểm toán");
            return resolve(parseInt(diemtoan));
        }, 4000);
    })
}
function diemly(){
    return new Promise(function(resolve, reject){
        setTimeout(() => {
            diemly = prompt("Nhập điểm ly");
            return resolve(parseInt(diemly));
        }, 8000);
    })
}

Promise.all([diemtoan(), diemly()])
.then(function(result){    
    console.log(result);// array các giá trị từ các resolve
    dtb = (result[0] + result[1])/2;
    alert("Điểm trung bình là " + dtb);
})
</script>

D. Sử dụng async/await trong javascript

Từ khóa async

Từ async đặt trước 1 function để bật chế độ bất đồng bộ cho nó. Khi có async ở trước , hàm sẽ trả về 1 promise.

<script>
async function chao() { 
     return "Chào bạn" ;
}; 
v = chao();
console.log(v); //trả về như sau:  Promise {<fulfilled>: "Chào bạn"}
</script>

Cũng có thể tạo hàm async như sau:

<script>
var  loichao = async function() { return "Xin chào" }; 

v = loichao();
console.log(v); //trả về:  Promise {<fulfilled>: "Xin chào"}
</script>

Hoặc có thể dùng hàm mũi tên

<script>
var  loichao = async () => { return "Xin chào" }; 

v = loichao();
console.log(v);  //trả về:  Promise {<fulfilled>: "Xin chào"}
</script>

Muốn lấy giá trị trả về của hàm async, bạn phải đợi promise đạr fulfilled bằng cách sử dùng lệnh then() như sau :

<script>
  var  loichao = async () => { return "Xin chào" }; 
  loichao().then( function(v){ console.log(v)} ); 
  console.log("Việt Nam địch vô");
   /* Kết quả: 
    Việt Nam địch vô
    Xin chào
   */
</script>

Từ khóa await

Từ khóa await được dùng trong các hàm đã khai báo async. Sử dụng await khi gọi các hàm trả về 1 Promise, gồm cả các hàm web API.

<script>
async function helo() {   
   return str = await Promise.resolve("Chào quý khách"); 
}; 
helo().then( function(v) { alert(v); });
</script>

<div id="kq"></div>
<script>
async function chaonhau() {
  let p = new Promise(function(r1, r2) {
    r1("Chúc an lành");
  });  
  document.getElementById("kq").innerHTML = await p;
}

chaonhau();
</script>

<div id="kq"></div>
<script>
async function homnaythenao() {
  var p = new Promise(function(r1, r2) {
    setTimeout(function() { r1("Đời đẹp lắm"); }, 3000);
  });
  document.getElementById("kq").innerHTML = await p;
}

homnaythenao();
document.getElementById("kq").innerHTML="Helo";
</script>

Async và await giúp bạn chạy các promise 1 cách tuần tự. code đơn giản đễ đọc:

async function() {   
   var kq1 = await promise1();
   var kq2 = await promise2(kq1);
   var kq3 = await promise3(kq2);
 }

Đồng bộ và bất đồng bộ trong javascript cung cấp nhiều kiến thức với các cách xử lý bất đồng bộ như callback, promise, async/await. Nó giúp bạn nhiều trong việc xử lý lập trình trình tương tác sau này. Bởi vì các chức năng bạn triển khai nhiều khi đòi hỏi cần tận dụng tối đa tài nguyên hệ thống, hoặc phải gọi đến các chức năng từ module khác, từ hệ thống khác… Để hiểu bài này, các kiến thức yêu cầu cần có là lập trình đối tượng trong javascript và các kiến thức cơ sở về lập trình javasript nhé.