Giới thiệu
Bạn đã bao giờ tự hỏi JavaScript biết làm gì sau khi hoàn thành một tác vụ như chờ đợi một bộ đếm thời gian, xử lý các cú nhấp chuột của người dùng hoặc lấy dữ liệu từ máy chủ chưa? Đó chính là lúc hàm callback xuất hiện. Chúng là nền tảng để viết mã JavaScript hiệu quả, không chặn và sạch sẽ.
Trong bài viết này, chúng ta sẽ cùng khám phá các khái niệm cơ bản về callback, bao gồm chúng là gì, những cách khác nhau để viết chúng và sự phân biệt quan trọng giữa các callback đồng bộ và không đồng bộ.
Hàm Callback Là Gì?
Trong JavaScript, hàm là công dân hạng nhất, nghĩa là chúng có thể được truyền như các biến.
Một callback đơn giản là một hàm được truyền như một đối số cho một hàm khác, mà sau đó sẽ được thực hiện sau, có thể ngay lập tức hoặc sau một sự kiện hoặc thao tác cụ thể.
Dưới đây là một ví dụ đơn giản:
javascript
function greet(name) {
console.log("Xin chào, " + name);
}
function runCallback(callback) {
const name = "Rowland";
callback(name); // gọi hàm callback
}
runCallback(greet); // Kết quả: Xin chào, Rowland
Trong đoạn mã này, hàm greet là hàm callback, được truyền như một đối số cho hàm runCallback và được thực thi bên trong nó.
Lưu ý Quan Trọng:
Khi truyền hàm như một callback, bạn truyền tham chiếu của nó bằng cách không bao gồm dấu ngoặc như sau:
runCallback(greet).Nếu thêm dấu ngoặc như trong
runCallback(greet())thì hàmgreetsẽ được thực thi ngay lập tức và giá trị trả về của nó (trong trường hợp này sẽ làundefined) sẽ được truyền làm đối số, điều này là không chính xác cho một callback.
Cách Viết Hàm Callback
Các callback có thể được định nghĩa theo một vài cách khác nhau:
Callback Đặt Tên
Một callback đặt tên là một định nghĩa hàm thông thường có tên rõ ràng, như hàm greet trong ví dụ trên.
Lợi ích:
- Tính tái sử dụng: Bạn có thể sử dụng cùng một hàm như một callback ở nhiều nơi khác nhau.
- Độ rõ ràng: Cấu trúc mã rõ ràng hơn khi logic phức tạp.
- Gỡ lỗi: Tên hàm xuất hiện trong ngăn xếp gọi, giúp việc gỡ lỗi dễ dàng hơn.
Callback Vô Danh
Một callback vô danh là một hàm không có tên được truyền trực tiếp như một đối số, thường được định nghĩa trực tiếp.
javascript
function runCallback(callback) {
const name = "Rowland";
callback(name); // gọi hàm callback
}
runCallback(function(name) {
console.log("Xin chào, " + name);
}); // Kết quả: Xin chào, Rowland
Callback Hàm Mũi Tên
Điều này tương tự như các callback vô danh nhưng sử dụng cú pháp hàm mũi tên ES6 hiện đại để có phong cách ngắn gọn và dễ đọc hơn.
javascript
function runCallback(callback) {
const name = "Rowland";
callback(name); // Gọi hàm callback
}
runCallback((name) => {
console.log(`Xin chào, ${name}`);
}); // Kết quả: Xin chào, Rowland
Các Loại Hàm Callback
Các callback chủ yếu chia thành hai loại, dựa trên thời điểm chúng được thực thi:
Callback Đồng Bộ
Một callback đồng bộ được thực thi ngay lập tức trong hàm mà nó được truyền vào, hoàn thành trước khi dòng mã tiếp theo chạy.
Các Trường Hợp Sử Dụng Phổ Biến:
- Các Phương Thức Mảng:
forEach,map,filter, v.v. - Các hàm tiện ích tùy chỉnh
- Các Hàm Bậc Cao (HOF).
Ví dụ với phương thức mảng map:
javascript
const arr = [1, 2, 3];
function doubleNumber(num) {
return num * 2
}
const doubled = arr.map(doubleNumber); // Hàm callback chạy ngay lập tức cho mỗi phần tử.
console.log(doubled); // [2, 4, 6]
Trong trường hợp này, doubleNumber là một callback đặt tên được truyền cho phương thức arr.map().
Callback Không Đồng Bộ
Một callback không đồng bộ được thực thi sau khi một thao tác không đồng bộ hoàn thành, không ngay lập tức. Điều này cho phép động cơ JavaScript tiếp tục thực hiện dòng mã tiếp theo trong khi thao tác (như bộ đếm thời gian hoặc yêu cầu mạng) đang chờ xử lý.
Các Thao Tác Không Đồng Bộ Thông Dụng:
setTimeout- Các Listener Sự Kiện (ví dụ:
document.addEventListener('event', callback)) - Yêu Cầu HTTP (ví dụ: sử dụng
fetch)
Ví dụ với setTimeout:
javascript
setTimeout(function() {
console.log("Chạy sau 2 giây");
}, 2000);
console.log("Điều này sẽ được thực thi trước");
Kết quả trong bảng điều khiển:
javascript
Điều này sẽ được thực thi trước
Chạy sau 2 giây
Động cơ JS không chờ bộ đếm thời gian, vì vậy console.log thứ hai được thực thi ngay lập tức và callback bên trong setTimeout chỉ chạy sau 2000ms (2 giây) trễ.
Những Thực Hành Tốt Nhất
- Tránh Callback Hell: Sử dụng các hàm đặt tên hoặc các kỹ thuật như
Promisehoặcasync/awaitđể giữ cho mã rõ ràng và dễ quản lý. - Sử Dụng Hàm Mũi Tên: Sử dụng hàm mũi tên để giữ ngữ cảnh
thischính xác và để có mã ngắn gọn hơn. - Ghi Chú Rõ Ràng: Đảm bảo rằng bạn ghi chú rõ ràng cho các hàm callback để người khác có thể hiểu được logic của bạn.
Các Cạm Bẫy Thường Gặp
- Gọi Hàm Sai: Như đã đề cập ở trên, nếu thêm dấu ngoặc khi gọi hàm callback, bạn sẽ không truyền tham chiếu đúng cách.
- Quá Nhiều Callback Lồng Nhau: Điều này có thể làm mã trở nên khó đọc và bảo trì.
Mẹo Hiệu Suất
- Tránh Tạo Hàm Mới Trong Vòng Lặp: Điều này có thể dẫn đến hiệu suất kém. Thay vào đó, hãy tạo hàm trước và truyền nó vào.
- Sử Dụng Thư Viện: Sử dụng các thư viện như Lodash có thể giúp quản lý các callback một cách dễ dàng và hiệu quả hơn.
Giải Quyết Vấn Đề
- Callback Không Thực Thi: Kiểm tra xem hàm callback có thực sự được truyền đúng cách không.
- Thời Gian Chờ Không Đúng: Nếu bạn đang sử dụng
setTimeout, hãy đảm bảo thời gian trễ được thiết lập chính xác.
Kết luận
Hàm callback là một khái niệm cốt lõi trong JavaScript, rất quan trọng để hiểu cách mà ngôn ngữ này quản lý việc thực thi hàm, đặc biệt với các tác vụ không đồng bộ.
Tóm tắt lại:
- Callbacks là các hàm được truyền vào các hàm khác để được gọi sau.
- Chúng có thể được viết dưới dạng hàm đặt tên, vô danh hoặc hàm mũi tên.
- Callback đồng bộ chạy ngay lập tức (ví dụ: trong các phương thức mảng như map và forEach).
- Callback không đồng bộ chạy sau khi một thao tác hoàn thành (ví dụ: setTimeout, trình lắng nghe sự kiện, fetch).
Việc hiểu cách và khi nào các callback được thực thi là rất cần thiết và tạo nền tảng vững chắc cho việc học các khái niệm sâu hơn như Promises và async/await. Bạn sẽ thấy các callback xuất hiện ở khắp nơi khi tiếp tục viết nhiều JavaScript hơn!