Callback hell trong Javascript là gì và cách phòng trách

Xin chào các bạn, khái niệm callback chắc hẳn đã không còn xa lạ gì đối với các anh em coder JavaScript, đặc biệt là trong việc xử lý các hàm JavaScript bất đồng bộ (như trong NodeJS chẳng hạn). Tuy nhiên, nếu lạm dụng quá nhiều các hàm callback mà không có phương pháp code đúng đắn sẽ dẫn đến tình trạng code của chúng ta cực kì phức tạp, cực kì khó đọc. Hình dạng code khi ấy như sau:

B4UaJfMCQAE67QB.png

Chỉ mới nhìn vào thôi đã bị hoa mắt rồi đúng không nào? Như vậy bài viết ngày hôm nay, mình sẽ nhắc lại một số khái niệm cơ bản về callback, mô tả thêm về callback hell cũng như những phương pháp để phòng tránh nó.

**1. Callback là gì? ** Về cơ bản thì callback chẳng phải là một khái niệm cao siêu gì, nó chỉ là một convention cho coder khi viết code JavaScript mà thôi. Trong khi các hàm JavaScript thông thường sẽ trả về kết quả ngay khi kết thúc thì các hàm callback lại trì hoãn việc này. Do đó, callback hay được sử dụng trong trường hợp thực thi các lệnh liên quan tới nhập xuất dữ liệu như download, đọc file và giao tiếp với cơ sở dữ liệu,...

Khi khai báo và gọi một hàm, thông thường chúng ta thường làm như sau:

var result = multiplyTwoNumbers(5, 10);
console.log(result);
// Kết quả được in ra là 50

Tuy nhiên, đối với những hàm bất đồng bộ thì sẽ không trả về kết quả nào ngay tại thời điểm khai báo và được gọi

var photo = downloadPhoto('http://framgia.com/abc.gif')
// Biến photo vẫn chưa được xác định

Trong trường hợp trên, việc download bức ảnh gif có thể mất nhiều thời gian, chương trình thì không thể bị dừng lại chỉ vì bức ảnh vẫn chưa được download xong.

Để giải quyết vấn đề này, chúng ta có thể sử dụng callback (có thể được hiểu là call you back later - gọi chạy sau), cụ thể chúng ta sẽ chỉnh sửa lại đoạn code trên một chút, sao cho khi việc tải về bức ảnh hoàn thành thì hàm sẽ trả về kết quả.

downloadPhoto('http://coolcats.com/cat.gif', handlePhoto)

function handlePhoto (error, photo) {
  if (error) console.error('Download error!', error)
  else console.log('Download finished', photo)
}

console.log('Download started')

Trong đoạn code đã chỉnh sửa ở trên, cần lưu ý 3 điểm chính về thứ tự gọi chạy.

  • Thứ nhất, hàm handlePhoto được khai báo
  • Sau đó, hàm downloadPhoto được gọi chạy, lấy vào tham số là hàm handlePhoto (được gọi là callback)
  • Cuối cùng là dòng chữ "Download started" được in ra Cần lưu ý rằng, ở thời điểm hàm handlePhoto được truyền vào hàm downloadPhoto như một callback thì nó vẫn chưa được chạy, chúng ta mới chỉ khai báo và truyền thôi. Chỉ cho tới khi hàm downloadPhoto chạy xong thì nó mới chính thức được chạy.

Như vậy ở ví dụ này, có thể kết luận được 2 điều quan trọng như sau:

  • handlePhoto callback chỉ là một cách giữ cho hàm được chạy sau.
  • Thứ tự chạy hàm không phải từ trên xuống dưới mà hàm gọi chạy xong thì mới đến hàm được gọi.

2. Callback hell

Callback hell hay còn được gọi là pyramid of doom, hadouken (tên game điện tử 4 nút hồi xưa, chắc anh em nào cũng biết) là cách code không tối ưu, dựa trên concept đã giải thích ở trên.

BtjZedW.jpg

Lấy ví dụ, chúng ta có cần làm việc A, việc B rồi cuối cùng là việc C theo thứ tự

function thuc_day(viecnaodo){
    viecnaodo();
}

function danh_rang(viecnaodo){
    viecnaodo();
}

function di_an_sang(viecnaodo){
    viecnaodo();
}

// Code không tối ưu
function main(){
    thuc_day(function(){
      danh_rang(function(){
        di_an_sang(function(){
          console.log('OMG!!!!');
        });
      });
    });
}

3. Cách phòng tránh callback hell

a. Giữ cho code luôn sáng sủa Phía dưới là đoạn code rất tối sử dụng browser-request để tạo AJAX request tới server:

var form = document.querySelector('form')
form.onsubmit = function (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, function (err, response, body) {
    var statusMessage = document.querySelector('.status')
    if (err) return statusMessage.value = err
    statusMessage.value = body
  })
}

Đoạn code trên có 2 hàm vô danh, điều này là không nên, hãy đặt tên cho chúng

document.querySelector('form').onsubmit = formSubmit

function formSubmit (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, postResponse)
}

function postResponse (err, response, body) {
  var statusMessage = document.querySelector('.status')
  if (err) return statusMessage.value = err
  statusMessage.value = body
}

b. Mô-đun hóa Ý tưởng này được Isaac Schlueter (cha đẻ của npm) đưa ra thông qua phát biểu:

"Hãy viết một chương trình thành những mô-đun nhỏ mỗi mô-đun phụ trách một việc cụ thể, và rồi hợp nhất chúng lại với nhau tạo thành một khối vững chắc hơn. Code sẽ không thể bị callback hell nếu tuân theo quy tắc này."

Bây giờ, chúng ta sẽ thử chia tách đoạn code trên thành mô-đun lưu trên các file khác nhau.

Ở dưới là một file mới có tên formuploader.js, trong đó có 2 hàm:

module.exports.submit = formSubmit

function formSubmit (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, postResponse)
}

function postResponse (err, response, body) {
  var statusMessage = document.querySelector('.status')
  if (err) return statusMessage.value = err
  statusMessage.value = body
}

module.exports là một node giống như trên hệ thống mô-đun trên node.js . Cách code này có ưu điểm là chúng ta có thể sử dụng mô-đun được ở mọi chỗ, rất dễ hiểu và không cần tới hệ thống file cấu hình hay kịch bản phức tạp nào.

Bây giờ chúng ta đã có formuploader.js rồi thì việc còn lại chỉ là dùng hàm require và sử dụng.

var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit

Như vậy, ứng dụng của chúng ta đã được cắt giảm chỉ còn có 2 dòng code nhưng đi theo sau đó là một số lợi ích khá to lớn:

  • Dễ hiểu đối với những developer mới vào nghề bởi họ không còn bị choáng ngợp khi đọc toàn bộ hàm formuploader
  • formuploader có thể được sử dụng ở bất cứ chỗ nào trong project mà không bị lặp code

c. Sử dụng promise

Promise là một tính năng mới có trong ES6 để giải quyết vấn đề callback hell cực kỳ hiệu quả. Để biết thêm về Promise có những ưu điểm thế nào, cách dùng ra sao, mời các bạn tham khảo link sau: Những tính năng mới nổi bật của ES6

Nguồn: callbackhell.com

Tham khảo: kipalog.com