Backbone.js là một thư viện JavaScript nhẹ nhưng mạnh mẽ, dựa trên mô hình MVC và hệ thống sự kiện. Tuy nhiên, do thiếu cơ chế dọn dẹp tự động hoàn chỉnh, các ứng dụng Backbone dễ gặp rò rỉ bộ nhớ nếu không quản lý kỹ lưỡng vòng đời đối tượng và liên kết sự kiện. Dưới đây là 8 chiến lược thực tiễn, được tái cấu trúc từ góc nhìn kỹ thuật hiện đại, nhằm đảm bảo tài nguyên được giải phóng đúng thời điểm.
1. Ưu tiên listenTo thay vì on để đăng ký sự kiện
Hàm listenTo thiết lập mối quan hệ "nghe – nguồn" có thể theo dõi được, cho phép Backbone ghi nhận và hủy bỏ hàng loạt trình xử lý cùng lúc. Trong khi đó, on tạo liên kết rời rạc — việc quên gọi off sẽ giữ nguyên tham chiếu đến hàm callback và đối tượng chủ sở hữu.
// ❌ Nguy cơ rò rỉ: không có cơ chế dọn dẹp tự động
this.dataModel.on('update', this.refreshUI.bind(this));
// ✅ An toàn: sự kiện nằm dưới quản lý của view
this.listenTo(this.dataModel, 'update', this.refreshUI);
2. Gọi stopListening() trong phương thức remove()
Khi một view bị loại khỏi DOM, việc chỉ xóa phần tử HTML là chưa đủ — tất cả trình nghe sự kiện phải được giải phóng. Ghi đè remove() để đảm bảo stopListening() được thực thi trước khi gọi phương thức cha.
var DashboardView = Backbone.View.extend({
initialize() {
this.listenTo(this.model, 'change:status', this.updateStatus);
this.listenTo(this.items, 'add', this.renderItem);
},
remove() {
// Giải phóng toàn bộ sự kiện do listenTo đăng ký
this.stopListening();
// Xóa DOM và trả về đối tượng (để hỗ trợ chuỗi gọi)
return Backbone.View.prototype.remove.call(this);
}
});
3. Dọn dẹp sự kiện DOM thủ công một cách có chủ đích
Các sự kiện khai báo qua thuộc tính events được Backbone xử lý tự động khi gọi remove(). Nhưng với những sự kiện gắn trực tiếp lên DOM ngoài phạm vi view (ví dụ: $(document), $(window)), cần chủ động gỡ bỏ bằng off() kèm namespace.
var SidebarView = Backbone.View.extend({
events: {
'click .toggle-btn': 'togglePanel' // ← Tự động dọn dẹp
},
initialize() {
// ← Thủ công → dùng namespace để tránh ảnh hưởng chung
$(document).on('keydown.sidebar', this.handleKey.bind(this));
},
remove() {
$(document).off('.sidebar'); // Loại bỏ toàn bộ sự kiện có namespace "sidebar"
this.stopListening();
return Backbone.View.prototype.remove.call(this);
}
});
4. Ngăn chặn tham chiếu vòng tròn bằng thiết kế một chiều
Một tham chiếu vòng tròn xảy ra khi View → Model → View hoặc View → Closure → View. Để phá vỡ chuỗi này:
- Không lưu tham chiếu ngược từ model sang view;
- Sử dụng
bind()hoặc arrow function một cách thận trọng — ưu tiên truyền dữ liệu thay vì đối tượng; - Với callback phức tạp, tách logic ra ngoài view và truyền tham số rõ ràng.
// ❌ Nguy cơ: closure giữ tham chiếu tới view
this.model.on('error', function(err) {
this.showError(err); // 'this' trỏ tới view → vòng tròn
});
// ✅ Sửa: tách xử lý lỗi thành hàm độc lập, truyền dữ liệu thay vì ngữ cảnh
function logError(model, error) {
console.warn(`Lỗi từ ${model.id}:`, error);
}
this.listenTo(this.model, 'error', logError);
5. Đặt lại tham chiếu tới collection sau khi view bị hủy
Khi view giữ tham chiếu tới một collection lớn, việc không null hóa nó có thể ngăn GC thu hồi cả collection — đặc biệt nếu collection vẫn đang lắng nghe các sự kiện toàn cục. Luôn gán lại giá trị null hoặc undefined cho các thuộc tính tham chiếu bên ngoài.
var ListView = Backbone.View.extend({
initialize({ sourceCollection }) {
this.items = sourceCollection;
this.listenTo(this.items, 'change', this.rerender);
},
remove() {
this.stopListening();
this.items = null; // ← Mở đường cho GC thu hồi collection nếu không còn nơi nào khác giữ tham chiếu
return Backbone.View.prototype.remove.call(this);
}
});
6. Sử dụng listenToOnce cho sự kiện chỉ kích hoạt một lần
Các sự kiện như sync, fetch:success, hay initialize thường chỉ cần phản hồi duy nhất. Thay vì quản lý thủ công, hãy dùng listenToOnce — nó tự hủy sau lần đầu gọi, loại bỏ khả năng quên dọn dẹp.
this.listenToOnce(this.userProfile, 'sync', () => {
this.renderWelcomeMessage();
// Không cần gọi off — đã được xử lý tự động
});
7. Tránh sử dụng biến toàn cục và sự kiện không có namespace
Mỗi biến toàn cục là một gốc sống (root) trong đồ thị tham chiếu — khiến toàn bộ cây con không thể bị thu gom. Tương tự, sự kiện gắn vào window hoặc document mà không có namespace dễ gây xung đột và khó kiểm soát.
// ❌ Không nên
window.activeDashboard = new DashboardView();
// ✅ Nên: giới hạn phạm vi bằng IIFE hoặc ES module
const dashboard = new DashboardView();
// ✅ Sự kiện toàn cục phải có namespace rõ ràng
$(window).on('resize:dashboard', this.onWindowResize.bind(this));
// Và dọn dẹp chính xác:
$(window).off('resize:dashboard');
8. Giám sát bộ nhớ chủ động bằng công cụ DevTools
Không nên chờ đến khi ứng dụng chậm mới kiểm tra. Thực hành tốt là:
- Gắn thẻ heap snapshot trước và sau khi chuyển giữa các view lớn;
- Sử dụng Allocation Instrumentation on Timeline để phát hiện các đối tượng được tạo liên tục mà không bị giải phóng;
- Tìm kiếm các instance của view còn tồn tại sau khi đã gọi
remove()— kiểm tra cột Retainers để xác định đâu đang giữ tham chiếu.
Đặt Backbone.debug = true trong môi trường phát triển cũng giúp bật thêm thông tin cảnh báo nội bộ về việc gọi sai thứ tự listenTo/stopListening.