Tái cấu trúc một đoạn mã có sẵn (Phần 2)

phần 1 chúng ta đã làm quen và bắt đầu đi vào refactoring một đoạn mã "Hello world" đơn giản. Trong phần này, tôi sẽ tiếp tục cùng các bạn tìm hiểu những kỹ thuật nâng cao của refactoring.

2. Carousel

Trong phần này chúng ta sẽ cùng đi xây dựng ứng dụng jQuery Carousel với các chức năng:

  • Điều khiển dịch chuyển sang trái, phải giữa các slide
  • Tự động dịch chuyển sau 10s
  • Nếu bạn tải lại trang ở vị trí slide x thì khi tải xong trang, carousel sẽ dừng ở đúng slide x

Đây là những chức năng cơ bản của một ứng dụng carousel, và nó chứa những đoạn mã quan trọng của ứng dụng cần được refactoring.

Trước hết, chúng ta sẽ xây dựng đoạn mã HTML cho carousel trước:

<!DOCTYPE html>
<html>
    <head>
      <title></title>
      <link rel="stylesheet" href="css/style.css">
      <script src="js/jquery.min.js"></script>
      <script src="js/app.js"></script>
    </head>
    <body>
      <div class="wrapper">
        <ul>
          <li><img src="images/img1.jpg" alt="Kitten" /></li>
          <li><img src="images/img2.jpg" alt="Kitten" /></li>
          <li><img src="images/img3.jpg" alt="Kitten" /></li>
          <li><img src="images/img4.jpg" alt="Kitten" /></li>
          <li><img src="images/img5.jpg" alt="Kitten" /></li>
        </ul>
        <div class="controls">
          <a href="#" class="left">Left</a>
          <a href="#" class="right">Right</a>
          <span></span>
        </div>
      </div>
    </body>
</html>

Bên cạnh đoạn mã HTML trên, chúng ta sẽ có đôi chút css cho carousel trông dễ nhìn hơn. Song nó không quan trọng với chủ đề chúng ta tìm hiểu hiện tại nên chúng ta sẽ không cần chú ý tới nó lắm.

Cuối cùng chúng ta sẽ đi xây dựng đoạn mã jQuery cho carousel:

$(function() {
  if(location.hash && location.hash.indexOf("image") > -1) {
    var number = parseInt(location.hash.charAt(location.hash.length -1));
    $("ul").animate({
      "margin-left": number * -300
    }, function() {
      currentImage = number;
      $(".controls span").text("Current: " + (currentImage + 1));
    });
  }
  var timeout = setTimeout(function() {
    $(".left").trigger("click");
  }, 10000);

  var currentImage = 0;
  $(".left").click(function() {
    clearTimeout(timeout);
    if(currentImage == $("li").length - 1) {
      $("ul").animate({
        "margin-left": 0
      }, function() {
        currentImage = 0;
        $(".controls span").text("Current: " + (currentImage + 1));
      });
    } else {
      $("ul").animate({
        "margin-left": "-=300px"
      }, function() {
        currentImage+=1;
        $(".controls span").text("Current: " + (currentImage + 1));
      });
    }
    timeout = setTimeout(function() {
      $(".left").trigger("click");
    }, 10000);
    return false;
  });

  $(".right").click(function() {
    clearTimeout(timeout);
    if(currentImage == 0) {
      $("ul").animate({
        "margin-left": ($("li").length - 1) * -300
      }, function() {
        currentImage = $("li").length - 1;
        $(".controls span").text("Current: " + (currentImage + 1));
      });
    } else {
      $("ul").animate({
        "margin-left": "+=300px"
      }, function() {
        currentImage-=1;
        $(".controls span").text("Current: " + (currentImage + 1));
      });
    }
    timeout = setTimeout(function() {
      $(".left").trigger("click");
    }, 10000);
    return false;
  });
});

Trên đây là đoạn mã xử lý các phần công việc của carousel mà chúng ta cần, tôi nghĩ nó rất đơn giản để bạn có thể đọc và hiểu nó. Chúng ta dễ dàng nhận thấy đoạn mã trên đang gặp phải rất nhiều vẫn đề mà chúng ta gặp phải ở phần trước. Ngoài ra còn một số vấn đề khác mà có thể bạn chưa nhận ra. Để không sót bất kì trường hợp nào, tôi nghĩ rằng bạn nên liệt kê các vấn đề đó ra.

Tôi xin liệt kê các vấn đề như sau:

  • Lặp mã: Có nhiều đoạn mã được sử dụng nhiều lần, và đoạn xử lý cho dịch chuyển slide sang trái và phải giống nhau.
  • Selector tồi: Các selector không được chọn theo ngữ cảnh mà được chọn một cách quá chung chung.
  • Lạm dụng document ready: tất cả các đoạn mã đều để ở bên trong khối $(function() {}. Đây là một vấn đề lớn khi bạn viết một plugin, và giải pháp cho vấn đề này thường là sử dụng prototype. Nhưng tôi xin gác lại vấn đề này trong bài viết vì nó đã đi ra ngoài phạm vi của bài viết.

Lưu ý: Cách viết $(function() {} là cách viết ngắn gọn của $(document).ready(function() {});. Tham khảo thêm tại đây.

  • Sử dụng return false;
  • Thay vì sử dụng click() thì nên sử dụng on()
  • Sử dụng cài đặt 10000 nhiều lần, client không cài đặt được

Return false; the anti-patterm

Hiện nay chúng ta đang sử dụng return false cho cả hai sự kiện:

$(".left").click(function() {
  // things happen here
  return false;
});
$(".right").click(function() {
  // things happen here
  return false;
});

Chủ đề về những vấn đề của return false đã được Doug Neiner giải thích rất rõ trong bài viết Stop (Mis)Using Return False.

Thông thường khi chúng ta sử dụng return false, điều mà chúng ta muốn thực hiện đó là thoát khỏi những sự kiện bên trong function đang định nghĩa và gọi tới sự kiện event.preventDefault();:

$(".left").click(function() {
  // things happen here
  event.preventDefault();
});

Chắc hẳn bạn hiểu rằng event.preventDefault() dùng để ngăn chặn các sự kiện mặc định thực hiện. Và trong nhiều trường hợp, return false sẽ trả về một kết quả tương tự, nhưng có một số điểm khác biệt.

Trước hết chúng ta hãy đi tìm hiểu một ví dụ sau:

$(function() {
  $("div").on("click", function() {
    console.log("div got clicked");
  });

  $("div p").on("click", function() {
    console.log("p got clicked");
    event.stopPropagation();
  });
});

Khi bạn mở trình duyệt và nhấp chuột vào thẻ <p> được bao bởi thẻ <div>, kết quả nhận được ở console là gì? Bạn sẽ chỉ nhận được kết quả là "p got clicked" bởi vì sự kiện event.stopPropagation(); đã ngăn chặn tất cả các sự kiện từ các thẻ là thẻ cha của nó theo sắp xếp của DOM. Điều nãy rất hữu ích, tuy nhiên không phải lúc nào chúng ta cũng sử dụng.

Vậy return false có gì khác biệt khi chúng ta nhận được kết quả tương tự, bởi khi chúng ta sử dụng return false thì mặc định jQuery sẽ gọi tới hai sự kiện là event.stopPropagation();event.preventDefault();. Điều này sẽ phát sinh những lỗi mà bạn sẽ khó có thể kiểm soát được.

Vậy giải pháp ở đây là gì? Tuy hơi mất chút thời gian nhưng nếu bạn chỉ muốn ngăn chặn các hành động mặc định, hãy dùng preventDefault(), nếu bạn muốn ngăn chặn các hành động dây chuyền, hãy dùng stopPropagation(). Nếu bạn muốn cả hai, đừng dùng return false, hãy làm nó rõ ràng bằng việc dùng:

event.stopPropagation();
event.preventDefault();

Điều này giúp bạn cùng các đồng nghiệp có thể dễ dàng hiểu và kiểm soát được những lỗi phát sinh.

Hãy áp dụng điều đó vào ngay đoạn mã của chúng ta bằng cách thay thế return false bằng event.preventDefault(); và hãy đặt nó ở trên cùng chuỗi xử lý sự kiện.

$(".left").click(function(event) {
  event.preventDefault();
  // things happen here
});
$(".right").click(function(event) {
  event.preventDefault();
  // things happen here
});

On() và Off()

Từ phiên bản jQuery 1.7, để xử lý sự kiện binding và unbinding, jQuery đã thêm vào API của họ hai phương thức on() và off() bổ xung cho các phương thức cũ là click(), hover(), mouseout(), live(), bind()... Điều này không làm thay đổi tốc độ, cũng không làm đoạn mã của bạn trông dễ nhìn hơn. Nhưng nó tạo nên sự đồng bộ và theo tôi thấy nó đẹp hơn (like)

$(".left").on("click", function(event) {
  event.preventDefault();
  // things happen here
});
$(".right").on("click", function(event) {
  event.preventDefault();
  // things happen here
});

Trùng lặp

Chúng ta có thể thấy khá nhiều đoạn mã, các selector, value bị trùng lặp trong đoạn mã của chúng ta. Và cách xử lý thì rất đơn giản theo những gì đã giới thiệu tại phần 1. Hãy xử lý từng bước nhỏ theo cách nghĩ của người đọc và sử dụng nó, bạn sẽ nhận được kết quả tốt hơn rất nhiều.

$(function() {
  var CAROUSEL_TRANSITION_TIME = 10000;
  var ul = $("ul");
  var maxIndexLi = $("li").length - 1;
  var controlText = $(".controls span");
  var leftLink = $(".left");
  var rightLink = $(".right");

  var updateControlText = function() {
    controlText.text("Current: " + (currentImage + 1));
  };

  if(location.hash && location.hash.indexOf("image") > -1) {
    var number = parseInt(location.hash.charAt(location.hash.length -1));
    ul.animate({
      "margin-left": number * -300
    }, function() {
      currentImage = number;
      updateControlText();
    });
  }
  var timeout = setTimeout(function() {
    leftLink.trigger("click");
  }, CAROUSEL_TRANSITION_TIME);

  var currentImage = 0;
  leftLink.on("click", function(event) {
    event.preventDefault();
    clearTimeout(timeout);
    if(currentImage == maxIndexLi) {
      ul.animate({
        "margin-left": 0
      }, function() {
        currentImage = 0;
        updateControlText();
      });
    } else {
      ul.animate({
        "margin-left": "-=300px"
      }, function() {
        currentImage+=1;
        updateControlText();
      });
    }
    timeout = setTimeout(function() {
      leftLink.trigger("click");
    }, CAROUSEL_TRANSITION_TIME);
  });

  rightLink.on("click", function(event) {
    event.preventDefault();
    clearTimeout(timeout);
    if(currentImage == 0) {
      ul.animate({
        "margin-left": (maxIndexLi) * -300
      }, function() {
        currentImage = maxIndexLi;
        updateControlText();
      });
    } else {
      ul.animate({
        "margin-left": "+=300px"
      }, function() {
        currentImage-=1;
        updateControlText();
      });
    }
    timeout = setTimeout(function() {
      leftLink.trigger("click");
    }, CAROUSEL_TRANSITION_TIME);
  });
});

Ở phần tiếp theo, chúng ta sẽ tiếp tục đi sâu vào những kỹ thuật nâng cao mà chúng ta thường gặp phải.

Good luck ! (ok)

Bài viết không thể tránh được những sơ sót chủ quan, mong mọi người đóng góp.