Bài trước: Web nâng cao (6) - JavaScript cho React (5)
-----1.1
JavaScript
cho React (6)
Phần này bạn sẽ
tìm hiểu về:
– Lập trình đồng
bộ
– Lập trình bất đồng
bộ
– Các kĩ thuật lập
trình bất đồng bộ trong JavaScript (callback, promise, async/await)
– Fetch API
1.1.1 Lập trình kiểu đồng bộ và bất đồng bộ
Lập trình kiểu đồng bộ
Đồng bộ (tiếng
Anh là synchronous) là một tính từ, có nghĩa là (những chuyển động) có cùng chu
kì hoặc cùng tốc độ, được tiến hành trong cùng một thời gian, tạo ra một sự phối
hợp nhịp nhàng, ăn khớp với nhau; có sự ăn khớp giữa tất cả các bộ phận hoặc
các khâu, tạo nên một sự hoạt động nhịp nhàng của chỉnh thể. [theo wiktionary].
Lập trình theo kiểu
đồng bộ được hiểu là quá trình xử lý đoạn mã sẽ được thực hiện tuần tự, từ trên
xuống dưới, xử lý hết lệnh trước rồi đến lệnh sau, nếu lệnh trước chưa xử lý
xong thì lệnh sau sẽ phải chờ.
Bạn cùng xem xét
ví dụ sau:
const name = "cu Teo";
const greeting = `Xin chao, ten toi la ${name}!`;
console.log(greeting);
// Xin chao, ten
toi la cu Teo!
Đoạn mã trên gồm
3 việc là:
1.
Khai
báo một chuỗi có tên là name để chứa
một chuỗi kí tự
2.
Khai
báo một chuỗi khác có tên là greeting,
chuỗi này sẽ sử dụng chuỗi name
3. Xuất
chuỗi greeting ra cửa sổ console
Khi thực thi đoạn mã trên, trình duyệt sẽ thực thi tuần tự từng
lệnh một, từ trên xuống dưới theo đúng thứ tự của mã nguồn. Do lệnh sau cần sử
dụng tới kết quả của lệnh trước, nên các lệnh phía sau phải chờ cho các lệnh
phía trước thực hiện xong thì nó mới được thực hiện. Đoạn mã trên chính là một
chương trình chạy theo kiểu đồng bộ.
Nếu bạn viết lại đoạn mã trên có dùng tới lời gọi hàm thì
chương trình vẫn chạy theo kiểu đồng bộ. Ví dụ:
function makeGreeting(name) {
return `Xin chao, ten toi
la ${name}`;
}
const name = "cu Teo";
const greeting = makeGreeting(name);
console.log(greeting);
// Xin chao, ten
toi la cu Teo!
Hàm makeGreeting() là một hàm xử lý theo kiểu
đồng bộ, vì vậy khi gọi nó để thực thi bạn cần phải đợi cho tới khi nào nó thực
thi xong và trả về kết quả thì chương trình mới có thể thực hiện các lệnh kế tiếp.
Ưu điểm của lập
trình theo kiểu đồng bộ là đơn giản, dễ hiểu, dễ kiểm soát logic xử lý; chạy mã
nguồn là cho ra kết quả ngay.
Nếu một hàm đồng
bộ thực thi trong thời gian hữu hạn cho phép thì ổn, tuy nhiên nếu nó cần nhiều
thời gian để thực thi thì sẽ làm cho chương trình bị ngưng lại, ảnh hưởng tới
trải nghiệm người dùng, tới quá trình xử lý các tác vụ. Chúng ta cùng xem xét
ví dụ sau:
Ví dụ sau sẽ xuất
ra ngẫu nhiên một số lượng lớn các số nguyên tố (prime number).
<label for="quota">So luong so nguyen to:</label>
<input type="text" id="quota" name="quota" value="1000000">
<button id="generate">Sinh so nguyen to</button>
<button id="reload">Lam lai</button>
<div id="output"></div>
<script>
// tao cac so nguyen to
function generatePrimes(quota){
// kiem tra mot so co la nguyen to?
function isPrime(n){
for(let c = 2; c <= Math.sqrt(n); ++c){
if(n % c === 0) {
return false;
}
return true;
}
}
// tao ra quota so nguyen to luu vao mang primes
const primes = [];
const maximum = 1000000;
while(primes.length < quota){
const candidate = Math.floor(Math.random() * (maximum + 1));
if(isPrime(candidate)){
primes.push(candidate);
}
}
return primes;
}
// doan ma xu ly
giao dien
document.querySelector('#generate').addEventListener('click', () => {
const quota
=
document.querySelector('#quota').value;
const
primes
=
generatePrimes(quota);
console.log(primes);
document.querySelector('#output').textContent
=
`Da
tao duoc ${quota}
so nguyen to!`;
});
document.querySelector('#reload').addEventListener('click',
()
=>
{
document.location.reload();
});
Khi chạy đoạn mã
trên, sau khi bấm vào nút "Sinh so nguyen to", tùy theo tốc độ xử lý
của máy tính, có thể bạn phải chờ một vài giây trước khi trình duyệt xuất hết
các số nguyên tố ra cửa sổ console và hiển thị thông báo "Da tao duoc … so
nguyen to !".
Hạn chế của các hàm xử lý đồng
bộ
Đối với các hàm xử
lý theo kiểu đồng bộ mà cần nhiều thời gian xử lý sẽ tạo ra trải nghiệm người
dùng không tốt. Để chứng minh, bạn sẽ thêm một ô nhập liệu (textarea) vào đoạn
chương trình Sinh số nguyên tố.
<button id="reload">Lam lai</button>
<br><br>
<textarea name="" id="" cols="70" rows="10" placeholder="Nhap van ban ..."></textarea>
<div id="output"></div>
Bạn hãy nhập số nguyên tố cần sinh ra là 10 000 000, ngay khi bấm nút "Sinh so nguyen to", bạn sẽ không thể nhập liệu vào ô "Nhap van ban"; mà phải chờ cho việc sinh số nguyên tố thực hiện xong thì mới có thể nhập liệu được. Có hiện tượng này là do JavaScript là ngôn ngữ xử lý đơn luồng (single threaded) nên khi một lệnh đang được xử lý thì không thể chen ngang một lệnh khác.
Tuy nhiên, trong lập trình thực tế có những công việc cần nhiều thời gian để xử lý (như tải dữ liệu/hình ảnh từ server về, gọi dịch vụ từ xa, đọc/ghi tập tin), nếu cứ phải chờ cho lệnh đó xử lý xong thì mới thực hiện các lệnh kế tiếp sẽ làm cho ứng dụng bị “treo”, hoặc bị lỗi, không đáp ứng được nhu cầu của người dùng. Xem hình minh họa.
Vậy, để chương trình chạy mượt mà, cần có giải pháp để:
– Các công việc cần
nhiều thời gian xử lý sẽ được viết trong một hàm (ví dụ hàm xyz), để khi muốn
thực hiện chỉ việc gọi hàm
– Có cơ chế để ngầm gọi hàm xyz thực thi, trong khi vẫn có
thể xử lý và đáp ứng các xử lý khác của chương trình chính
– Khi nào hàm xyz thực hiện xong sẽ tự động hiển thị kết quả
Lập trình bất đồng bộ chính là giải pháp cho yêu cầu này.
Lập trình kiểu bất đồng bộ
Bất đồng bộ (tiếng Anh là asynchronous) là tính từ, có nghĩa
là hai cái gì đó xảy ra không đồng thời. Trong trường hợp này chính là việc thực
thi đoạn mã và cho ra kết quả, có thể đoạn mã được thực thi nhưng không cho ra
kết quả ngay.
Lập trình theo kiểu bất đồng bộ cũng được hiểu theo một khía
cạnh khác là quá trình xử lý mã nguồn không nhất thiết phải thực hiện tuần tự,
một lệnh/đoạn mã phía sau vẫn có thể được thực thi khi một lệnh/đoạn mã phía
trước chưa thực thi xong, chưa cho ra kết quả.
Để dễ hiểu về lập trình bất đồng bộ, bạn cùng viết và chạy
thử đoạn mã sau:
console.log("Câu lệnh 1");
console.log("Câu lệnh 2");
setTimeout(() => console.log("Câu lệnh 3"), 2000);
console.log("Câu lệnh 4");
// Câu lệnh 1
// Câu lệnh 2
// Câu lệnh 4
// Câu lệnh 3
Hàm setTimeout() là một hàm có sẵn của JavaScript, có thể sử
dụng hàm này để minh họa cho kiểu chạy bất đồng bộ. Hàm setTimeout(function, milliseconds) sẽ gọi một hàm khác (tham số function) để thực thi, sau một khoảng thời
gian nhất định (tham số milliseconds).
Theo đoạn mã nguồn ở trên, nếu thực thi theo tuần tự, đoạn
mã sẽ xuất chuỗi “Câu lệnh 1”, “Câu lệnh 2”, hàm setTimeout() sẽ xuất chuỗi “Câu
lệnh 3”, cuối cùng là xuất chuỗi “Câu
lệnh 4”. Tuy nhiên, khi thực thi đoạn mã thì thứ tự các chuỗi được xuất ra
là: “Câu lệnh 1”, “Câu lệnh 2”, “Câu lệnh 4”, “Câu lệnh 3”.
Nghĩa là, chương trình không được thực thi theo thứ tự từ trên xuống dưới mà có
sự xáo trộn trong thứ tự thực thi các câu lệnh, đó là tính bất đồng bộ trong việc
thực thi mã nguồn.
Lập trình theo kiểu bất đồng bộ giúp chương trình chạy hợp
lý hơn trong một số tình huống, chạy mượt mà hơn, đem lại trải nghiệm người
dùng tốt hơn; tuy nhiên nó làm cho lập trình viên khó kiểm soát logic xử lý.
Một số tình huống cần sử dụng kĩ thuật lập trình bất đồng bộ
như: thực hiện lời gọi hàm fetch(), truy cập camera hoặc microphone getUserMedia(), yêu cần người dùng lựa
chọn một tập tin showOpenFilePicker().
JavaScript là ngôn ngữ lập trình đơn luồng (single threaded)
do vậy tại một thời điểm chỉ có một luồng (một lệnh/đoạn mã) được xử lý. Để có
thể lập trình theo kiểu bất đồng bộ, JavaScript cung cấp một số kỹ thuật như:
callback, promise và async/await.
1.1.2
Callback
Trong JavaScript, callback là một kỹ thuật lập trình. Nó được
sử dụng trong nhiều tình huống khác nhau chứ không chỉ trong lập trình bất đồng
bộ. Vì vậy, trước hết chúng ta nên tìm hiểu callback là gì?
Callback là gì?
Trong tình huống giao tiếp thông thường, callback nghĩa là gọi
lại, ví dụ khi bạn đang bận làm việc, bạn để điện thoại ở chế độ im lặng nên
không biết có cuộc gọi tới, khi xong việc bạn mở danh sách các cuộc gọi nhỡ để
gọi lại. Có thể hiểu, lập trình theo kiểu callback là hành động tạo ra danh
sách các cuộc gọi nhỡ giúp người sử dụng điện thoại có đầu mối để biết cần phải
gọi lại cho ai.
Chúng ta cùng làm
ví dụ sau:
function
lamBaiTap(tenBaiTap){
console.log("Toi
dang lam bai tap: " +
tenBaiTap);
}
lamBaiTap("Bai
tap 1");//
Toi dang lam bai tap: Bai tap 1
Ở ví dụ trên, hàm
lamBaiTap() sẽ nhận một tham số là tenBaiTap khi thực thi. Vì hàm trong
JavaScript cũng là object, nên bạn hoàn toàn có thể truyền hàm cho một hàm khác
như là một tham số, như đoạn mã sau:
function uongNuoc(){
console.log("Toi uong nuoc");
}
function
lamBaiTap(tenBaiTap,
tenHam){
console.log("Toi
dang lam bai tap: " +
tenBaiTap);
tenHam();
}
lamBaiTap("Bai tap 1", uongNuoc);
// Toi dang lam bai tap: Bai
tap 1
// Toi uong nuoc
Bạn hoàn toàn có
thể gọi trực tiếp hàm uongNuoc()
trong hàm lamBaiTap() mà không nhất thiết
phải truyền hàm uongNuoc() vào hàm lamBaiTap() theo kiểu truyền tham số:
function
uongNuoc(){
console.log("Toi
uong nuoc");
}
function
lamBaiTap(tenBaiTap){
console.log("Toi
dang lam bai tap: " +
tenBaiTap);
uongNuoc();
}
lamBaiTap("Bai
tap 1");
// Toi dang lam
bai tap: Bai tap 1
// Toi uong nuoc
Tuy nhiên, bạn
hãy cứ trải nghiệm với kiểu truyền hàm cho hàm và luôn suy nghĩ là "tại
sao người ta lại tạo ra kiểu lập trình này"? kiểu lập trình này có ưu điểm
gì? dùng được trong những tình huống nào?
Giờ bạn sẽ đổi
tên hàm được gọi là callback cho trực quan hơn:
function
uongNuoc(){
console.log("Toi
uong nuoc");
}
function
lamBaiTap(tenBaiTap,
callback){
console.log("Toi
dang lam bai tap: " +
tenBaiTap);
callback();
}
lamBaiTap("Bai tap 1", uongNuoc);
// Toi dang lam bai tap: Bai
tap 1
// Toi uong nuoc
Tóm lại, kĩ thuật
lập trình cho phép truyền hàm cho một hàm khác dưới dạng một tham số được gọi
là lập trình kiểu callback. Hàm truyền cho hàm khác được gọi là hàm callback
(ví dụ uongNuoc()). Nhờ hàm callback
đã được khai báo là tham số, nên nó sẽ được "hàm cha" gọi lại để thực
thi ở một thời điểm trong tương lai.
Quay trở lại tình
huống "cuộc gọi nhỡ ", thì đây là quá trình tạo danh sách cuộc gọi nhỡ:
function
uongNuoc(){
console.log("Toi
uong nuoc");
}
function
lamBaiTap(tenBaiTap,
callback){
console.log("Toi
dang lam bai tap: " +
tenBaiTap);
callback();
}
Còn đây là thực
hiện gọi lại:
lamBaiTap("Bai
tap 1", uongNuoc);
// Toi dang lam
bai tap: Bai tap 1
// Toi uong nuoc
Tham số truyền
cho hàm có thể là một dữ liệu thông thường, có thể là một hàm? Làm sao biết được
tham số nào là dữ liệu thông thường, tham số nào là một hàm? Bạn cứ yên tâm,
JavaScript sẽ có cách để kiểm tra điều này cho bạn. Nếu là dữ liệu, JavaScript
sẽ thực hiện các phép toán, nếu là hàm, JavaScript sẽ thực thi nội dung của
hàm. Bạn có thể tự kiểm tra bằng đoạn mã sau:
const number = 123;
function uongNuoc(){
console.log("Toi uong nuoc");
}
function lamBaiTap(tenBaiTap, callback){
console.log("Toi
dang lam bai tap: " +
tenBaiTap);
if(typeof(callback) === 'function') {
callback();
}
}
lamBaiTap("Bai
tap 1", number);
// Toi dang lam
bai tap: Bai tap 1
Đoạn mã trên sử dụng
typeof() để kiểm tra tham số truyền
vào, nếu nó là function thì mới gọi thực thi. Khi gọi hàm, bạn đã truyền vào một
giá trị số (biến number), nên hàm
callback không được thực thi.
Bạn có thể định
nghĩa trước hàm callback, sau đó truyền nó vào một hàm khác như ví dụ ở trên.
Hoặc bạn có thể định nghĩa trực tiếp hàm callback trong quá trình "hàm cha"
thực thi. Ví dụ dùng từ khóa function để định nghĩa hàm callback:
function lamBaiTap(tenBaiTap,
callback){
console.log("Toi
dang lam bai tap: " +
tenBaiTap);
callback();
}
lamBaiTap("Bai
tap 1", function()
{
console.log("Toi
uong nuoc");
});
Bạn cũng có thể dùng
hàm nặc danh (anonymous function, arrow function) để định nghĩa hàm callback:
function lamBaiTap(tenBaiTap,
callback){
console.log("Toi
dang lam bai tap: " +
tenBaiTap);
callback();
}
lamBaiTap("Bai
tap 1", ()
=>
{
console.log("Toi
uong nuoc");
});
Bạn có thể gọi một
lúc nhiều hàm callback:
function lamBaiTap(tenBaiTap, callback1, callback2){
console.log("Toi
dang lam bai tap: " +
tenBaiTap);
callback2();
callback1();
}
lamBaiTap("Bai tap 1",
() => {
console.log("Toi uong nuoc");
},
() => {
console.log("Toi an banh");
});
Tiếp theo, chúng ta sẽ trải nghiệm một số ví dụ sử dụng
callback trong JavaScript.
Sắp xếp mảng với callback
Bạn chạy ví dụ sau sẽ thấy, kết quả sắp xếp mảng theo chiều
tăng không đúng, vì JavaScript đã đổi dữ liệu của mảng thành kiểu chuỗi để sắp
xếp theo thứ tự bảng chữ cái:
const arr = [9, 4, 7, 2, 13, 45];
console.log(arr); // [9, 4, 7, 2, 13, 45]
// sắp xếp mảng arr bằng hàm sort()
arr.sort();
console.log(arr);
//[13, 2, 4, 45, 7, 9]
Vì vậy bạn cần phải
định nghĩa một hàm callback để thực hiện so sánh hai số a và b. Hàm callback sẽ
trả về giá trị như sau:
– Giá trị âm, nếu
a nhỏ hơn b (đồng nghĩa với không đổi chỗ a và b)
– Giá trị 0, nếu
a bằng b (đồng nghĩa với không đổi chỗ a và b)
– Giá trị dương,
nếu a lớn hơn b (đồng nghĩa với có đổi chỗ a và b)
function soSanhSo(a, b){
return a - b;
}
const arr = [9, 4, 7, 2, 13, 45];
arr.sort(soSanhSo);
console.log(arr); //[2, 4, 7, 9, 13, 45]
Nếu muốn sắp xếp mảng theo chiều giảm thì sửa lại hàm
callback như sau:
function soSanhSo(a, b){
return b - a;
}
forEach() với callback
Thông thường bạn hay duyệt mảng như sau:
const mau = ['xanh', 'do', 'tim', 'vang'];
for(let i = 0; i < mau.length; i++) {
console.log(`Mau
tai vi tri ${ i
}
la ${ mau[i]
}`);
/*
Mau tai vi tri 0 la
xanh
Mau tai vi tri 1 la
do
Mau tai vi tri 2 la
tim
Mau tai vi tri 3 la
vang
*/
}
Với callback bạn
có thể duyệt mảng theo kiểu khác, với kết quả xuất ra màn hình là như nhau:
const mau = ['xanh', 'do', 'tim', 'vang'];
mau.forEach((color, index) => {
console.log(`Mau tai vi tri ${ index } la ${ color }`);
});
map() với callback
const arr = [1, 2, 3];
let result = arr.map( (x) => x * 2 );
console.log(result); // [2, 4, 6]
Sự kiện DOM với callback
<body>
<button id = "nut-bam">Nut bam</button>
<script>
const btn = document.getElementById('nut-bam');
btn.addEventListener('click', function(){
alert('Ban vua bam nut');
});
</script>
</body>
-----
Cập nhật: 30/3/2022
-----
Bài tiếp: Web nâng cao (8) - JavaScript cho React (7)