Xem thêm

Đồng bộ và bất đồng bộ trong Javascript: Cách xử lý hiệu quả và mạnh mẽ

Huy Erick
Việc xử lý đồng bộ và bất đồng bộ trong Javascript là một khía cạnh quan trọng mà bạn cần biết khi lập trình. Trong bài viết này, chúng ta sẽ tìm hiểu về khái...

Việc xử lý đồng bộ và bất đồng bộ trong Javascript là một khía cạnh quan trọng mà bạn cần biết khi lập trình. Trong bài viết này, chúng ta sẽ tìm hiểu về khái niệm đồng bộ và bất đồng bộ, cùng với cách áp dụng chú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)

Xử lý đồng bộ có nghĩa là mã chương trình sẽ thực thi theo thứ tự từ trên xuống dưới. Khi một lệnh hoàn thành, lệnh tiếp theo mới được thực hiện. Đây là cách viết mã phổ biến và dễ hiểu.

Ưu điểm của xử lý đồng bộ là quá trình xử lý dễ kiểm soát. Nó cũng dễ dàng kiểm soát các lỗi phát sinh. Tuy nhiên, một điểm không lợi của xử lý đồng bộ là nếu một lệnh chạy quá lâu, nó sẽ ảnh hưởng đến các lệnh phía sau.

Ví dụ:

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 */
// ... thuchienNauCom();
// thuchienChienTrung();
// thuchienAnCom();

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

Xử lý bất đồng bộ có nghĩa là mã chương trình không thực thi tuần tự, nhiều lệnh có thể được thực hiện cùng một lúc. Có thể lệnh phía dưới thực hiện xong và cung cấp kết quả trước lệnh phía trên.

Ưu điểm của xử lý bất đồng bộ là nó có thể tối ưu hóa sức mạnh của hệ thống và giúp giảm thời gian chờ. Tuy nhiên, không phải hệ thống nào cũng hỗ trợ xử lý bất đồng bộ và việc làm quen và kiểm soát lỗi phát sinh có thể gặp phải một số khó khăn.

Trong Javascript, các hàm setTimeout, setInterval, fetch là những ví dụ tiêu biểu cho việc xử lý bất đồng bộ. Dưới đây là một ví dụ về xử lý bất đồng bộ:

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 */
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");

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 một hàm khác. Trong Javascript, bạn có thể tạo ra một hàm mới sử dụng callback. Bạn cũng có thể sử dụng callback trong các hàm có sẵn.

1. Định nghĩa hàm mới có sử dụng callback

Để tạo một hàm mới sử dụng callback, bạn có thể nhận một hàm làm tham số và thực thi hàm đó trong nội dung của hàm.

Ví dụ 1: Định nghĩa hàm xử lý chuỗi, sau đó hiển thị chuỗi đã xử lý.

function hienchuoi(str) {
    str = str.toUpperCase();
    console.log(str);
}

function xulychuoi(str, callback) {
    str = str.replace(/[*?)($!&;]/g, "");
    str = str.replace(/s+/g, " ");
    str = str.trim();
    if (callback) callback(str);
}

str = " Vạn * ( sự tùy ? ! &duyên$ ";
xulychuoi(str, hienchuoi);
// VẠN SỰ TÙY DUYÊN

Ví dụ 2: Viết hàm nhập điểm, sau đó gọi hàm callback để hiển thị học lực.

function xuly(tenmon, callback) {
    alert("Môn đang học: " + tenmon);
    var diem = prompt("Điểm bao nhiêu?????");
    callback(diem, 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á");
}

xuly("Javascript Nâng cao", thongbao);

2. Sử dụng callback trong các hàm Javascript

Nhiều hàm đã được định nghĩa sẵn trong Javascript đã hỗ trợ callback. Bạn có thể sử dụng chúng một cách dễ dàng.

Ví dụ:

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]
function hsT(v, i) {
    if (v.substr(0, 1) == "T") return true;
    else return false;
}

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

Ưu điểm của callback là nó là một cách triển khai phổ biến và dễ hiểu. Tuy nhiên, việc lồng nhau quá nhiều callback có thể dẫn đến khó khăn khi sửa lỗi và bảo trì.

2. Callback hell

Nếu sử dụng nhiều cấp độ callback, có thể xảy ra tình huống lồng nhau quá nhiều callback (gọi là "callback hell").

Callback hell là tình trạng mà code chứa quá nhiều callback nằm lồng nhau, tạo thành một cấu trúc code phức tạp, khó hiểu, khó debug và khó bảo trì.

Ví dụ:

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
*/

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

Có nhiều cách để xử lý callback hell:

  • Đặt tên cho hàm callback: Đặt tên cho hàm callback để dễ hiểu và dễ debug.
  • Thiết kế ứng dụng theo dạng module.
  • Sử dụng Promises (ES6).
  • Sử dụng Async/Await (ES8).

C. Promise trong Javascript

1. Khái niệm promise

Promise là một kỹ thuật được sử dụng trong việc xử lý bất đồng bộ. Mỗi promise đại diện cho một tác vụ chưa hoàn thành. Khi tác vụ hoàn thành, promise sẽ gửi một thông báo thành công hoặc thất bại.

Bằng cách sử dụng Promise, bạn có thể kết hợp các tác vụ xử lý và sử dụng kết quả khi xử lý bất đồng bộ hoàn tất. Điều này giúp làm cho việc lập trình bất đồng bộ trở nên giống với việc lập trình đồng bộ - chờ đợi việc xử lý bất đồng bộ hoàn tất trước khi thực hiện các thao tác yêu cầu kết quả của nó.

2. Cách tạo Promise

Để tạo một Promise, bạn sử dụng cú pháp new Promise với hai hàm resolve và reject để gọi khi tác vụ trong promise hoàn thành thành công hoặc thất bại.

var promise = new Promise(function (resolve, reject) {
    // Thực hiện xử lý...
    if (đánh giá thành công) {
        resolve(value1); // value1 được truyền vào hàm resolve
    } else {
        reject(value2); // value2 được truyền vào hàm reject
    }
});

3. Ví dụ tạo promise

diem = 8;
var p = new Promise(function (resolve, reject) {
    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é");
});

p.then(
    function (v1) {
        console.log(v1);
    },
    function (err) {
        console.log(err);
    }
);
console.log("Hi");
/* Kết quả: Hi Chúc mừng bạn */

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

Có thể sử dụng promise đã tạo bằng cách sử dụng lệnh then và catch để xử lý kết quả.

promise
    .then(function (v1) {
        console.log(v1);
    })
    .catch(function (err) {
        alert(err);
    });

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

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

  • Pending (đang xử lý): promise đã được tạo nhưng chưa được thực thi.
  • Fulfilled (đã hoàn thành): promise đã được thực thi thành công và trả về kết quả từ hàm resolve(). Khi hàm resolve() được gọi, promise sẽ chuyển sang trạng thái fulfilled, và các lệnh .then() sẽ được thực hiện.
  • Rejected (đã bị từ chối): promise đã thực thi và trả về kết quả từ hàm reject(). Khi hàm reject() được gọi, promise sẽ chuyển sang trạng thái rejected, và các lệnh .catch() sẽ được thực hiện.

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

Sử dụng Promise giúp tránh Callback Hell và làm cho code dễ đọc và dễ hiểu hơn.

Ví dụ:

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

// Promise
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))
    .then(() => xuly("green", 3000))
    .then(() => xuly("pink", 4000))
    .catch(function (err) {
        alert("Lỗi " + err);
    });

7. 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, trong đó kết quả của then trên sẽ được truyền vào then dưới nếu bạn sử dụng Promise trong các then(). Điều này giúp thực hiện các tác vụ tuần tự.

Ví dụ:

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

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);
        });
    });

7. Sử dụng Promise.All

Bạn có thể thực hiện nhiều promise cùng một lúc và sử dụng kết quả của chúng cho một tác vụ sau cùng bằng cách sử dụng Promise.all.

Ví dụ:

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

function diemly() {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            diemly = prompt("Nhập điểm lý");
            return resolve(parseInt(diemly));
        }, 8000);
    });
}

Promise.all([diemtoan(), diemly()])
    .then(function (result) {
        console.log(result); // Mảng chứa kết quả của các promise
        dtb = (result[0] + result[1]) / 2;
        alert("Điểm trung bình là " + dtb);
    });

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

Từ khóa async

Từ khóa async được sử dụng để bật chế độ bất đồng bộ cho một hàm. Khi một hàm được đánh dấu là async, nó sẽ trả về một promise.

Ví dụ:

async function chao() {
    return "Chào bạn";
}

v = chao();
console.log(v);
// Promise {<fulfilled>: "Chào bạn"}

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

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

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

Hoặc bạn có thể sử dụng hàm mũi tên:

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

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

Để lấy giá trị trả về của một hàm async, bạn cần đợi promise được hoàn thành bằng cách sử dụng lệnh then():

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 */

Từ khóa await

Từ khóa await được sử dụng trong các hàm đã được khai báo async. Nó được sử dụng để gọi các hàm trả về một Promise, bao gồm cả các hàm API của web.

async function helo() {
    return str = await Promise.resolve("Chào quý khách");
}

helo().then(function(v) {
    alert(v);
});
async function chaonhau() {
    let p = new Promise(function(r1, r2) {
        r1("Chúc an lành");
    });
    document.getElementById("kq").innerHTML = await p;
}

chaonhau();
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 = "Hello";

Async và await giúp việc xử lý các promise trở nên dễ hiểu và giống với việc xử lý đồng bộ.

1