Làm việc với Callback trong Javascript

Chào các bạn! Hôm nay Suntech sẽ giới thiệu với các bạn bài học đầu tiên trong series các bài học về Promises, async/await. Trong bài viết này chúng ta sẽ cùng nhau tìm hiểu về callbacks, Handling errors, Pyramid of Doom... Nào let's go!

1. Lập trình bất đồng bộ async dựa trên callback.

Như chúng ta đã biết trong JavaScript nhiều function được cung cấp sẵn cho phép chúng ta thực hiện async mà điển hình trong số đó là việc load các scripts hay modules. Để làm rõ hơn vấn đề chúng ta cùng nhau tìm hiểu qua ví dụ dưới đây.

Chúng ta có function loadScript(src) để load một script với src:

function loadScript(src) {
  // creates a <script> tag and append it to the page
  // this causes the script with given src to start loading and run when complete
  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}

Sau khi được tạo tự động, trình duyệt lúc này tự động tải tập lệnh và sẽ thực thi tập lệnh đó khi hoàn tất. Tuy nhiên nếu có bất kỳ mã nào bên dưới loadScript(…), nó sẽ không đợi cho đến khi quá trình tải tập lệnh hoàn thành.

loadScript('/my/script.js');
// the code below loadScript
// doesn't wait for the script loading to finish
// ...

Nhưng bạn lại không muốn dừng lại ở việc tải tập lệnh xuống rồi thực thi khi hoàn thành mà bạn còn muốn sử dụng các hàm và biến mới từ tập lệnh. Lúc này điều bạn cần làm là thêm 1 callback để làm đối số cho loadScript thực thi khi load script.

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(script);

  document.head.append(script);
}

Bây giờ nếu chúng ta muốn gọi các hàm mới từ script, chúng ta nên viết nó như sau

loadScript('/my/script.js', function() {
  // the callback runs after the script is loaded
  newFunction(); // so now it works
  ...
});

Dưới đây là 1 ví dụ với 1 script thực. Nó chính là lập trình bất đồng bộ async dựa trên callback.

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  script.onload = () => callback(script);
  document.head.append(script);
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
  alert(`Cool, the script ${script.src} is loaded`);
  alert( _ ); // function declared in the loaded script
});

2. Callback trong callback

Câu hỏi đặt ra lúc này là làm thế nào để chúng ra có thể tải tuần tự 2 scripts (script 1 và script 2 ngay sau đó). Giải pháp lúc này là ta sẽ đặt loadScriptcallback thứ 2 bên trong callback thứ nhất, như sau:

loadScript('/my/script.js', function(script) {

  alert(`Cool, the ${script.src} is loaded, let's load one more`);

  loadScript('/my/script2.js', function(script) {
    alert(`Cool, the second script is loaded`);
  });

});

Tương tự chúng ta sẽ có thể có script thứ 3 rồi thứ 4... ?

loadScript('/my/script.js', function(script) {

  loadScript('/my/script2.js', function(script) {

    loadScript('/my/script3.js', function(script) {
      // ...continue after all scripts are loaded
    });

  });

});

Tuy nhiên điều này không được khuyến khích bởi nó rất dễ sinh ra callback hell.

3. Handling errors

Trong các ví dụ trên, chúng ta đã không xem xét lỗi. Vậy điều gì sẽ xảy ra nếu quá trình tải scripts không thành công? Callback của chúng ta liệu sẽ có thể react với điều đó.

Đâymột bản cải tiến của loadScript giúp chúng ta có thể theo dõi lỗi trong quá trình tải script:

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

Nó sẽ trả về callback(null, script) nếu load thành công và callback(error) nếu thất bại.

4. Pyramid of Doom

Như cách chúng ta đặt vấn đề ở trên. Nó chỉ thực sự có hiệu quả nếu có 1 hoặc 2 callback lồng nhau, nhưng nếu có không phải là 1, 2 callback nữa mà là 10, 20 hay thậm trí là cả trăm callback lồng nhau thì vấn đề sẽ ra sao? Liệu nó có còn hoạt động tốt?

loadScript('1.js', function(error, script) {

  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript('2.js', function(error, script) {
      if (error) {
        handleError(error);
      } else {
        // ...
        loadScript('3.js', function(error, script) {
          if (error) {
            handleError(error);
          } else {
            // ...continue after all scripts are loaded (*)
          }
        });

      }
    });
  }
});

Ở đoạn code trên nó sẽ load 1.js trước và nếu không có lỗi nó sẽ load tiếp 2.js và nếu 2.js không có lỗi thì sẽ tiếp tục load đến 3.js, cứ như vậy. Tuy nhiên thực tế lại không phải đơn giản như vậy. Khi có quá nhiều các callback lồng nhau nó sẽ khiến việc quản lí của ta gặp càng nhiều khó khăn. Lúc này người ta gọi nó là "callback hell" hay "pyramid of doom".

Và để giải quyết được vấn đề này thì bạn có thể tham khảo bài viết Cách giải quyết Callback hell của chúng tôi. Chúc các bạn thành công!

Trần Quang Hào
SUNTECH VIỆT NAM   Đăng ký để nhận thông báo mới nhất