Lập trình · 20/10/2026

JavaScript: vì sao removeEventListener không gỡ được callback dùng bind?

Component đã chạy cleanup nhưng sự kiện vẫn gọi handler cũ. Một nguyên nhân dễ bỏ qua là gọi bind hai lần: một lần khi đăng ký và một lần khi gỡ listener. Cùng method và cùng object không có nghĩa hai lời gọi bind tạo ra cùng một hàm.

JavaScript: vì sao removeEventListener không gỡ được callback dùng bind?

Component đã chạy cleanup nhưng sự kiện vẫn gọi handler cũ. Một nguyên nhân dễ bỏ qua là gọi bind hai lần: một lần khi đăng ký và một lần khi gỡ listener. Cùng method và cùng object không có nghĩa hai lời gọi bind tạo ra cùng một hàm.

1. Mỗi bind tạo một function mới

const counter = {
  count: 0,
  increment() { this.count += 1; }
};
const first = counter.increment.bind(counter);
const second = counter.increment.bind(counter);
console.log(first === second); // false
first();
console.log(counter.count); // 1

Hai hàm có cùng cách xử lý this nhưng khác danh tính. bind không sửa method gốc thành hàm đã gắn ngữ cảnh; nó trả một hàm mới. Điều này quan trọng khi API cần so khớp chính callback đã đăng ký.

2. Gọi bind khi gỡ tạo ra callback khác

Các ví dụ dùng EventTarget độc lập, không cần nút HTML và không phát sinh request. Tên sự kiện tick chỉ phục vụ minh họa.

const target = new EventTarget();
const counter = {
  count: 0,
  increment() { this.count += 1; }
};
target.addEventListener('tick', counter.increment.bind(counter));
target.removeEventListener('tick', counter.increment.bind(counter));
target.dispatchEvent(new Event('tick'));
console.log(counter.count); // 1

Không tìm thấy listener tương ứng thì removeEventListener không có tác dụng. Lệnh vẫn chạy xong nên chỉ nhìn log cleanup là chưa đủ. Kiểm tra số lần handler thực sự chạy sau thời điểm gỡ.

3. Giữ callback để đăng ký và gỡ cùng một hàm

const target = new EventTarget();
const counter = {
  count: 0,
  increment() { this.count += 1; }
};
const handler = counter.increment.bind(counter);
target.addEventListener('tick', handler);
target.dispatchEvent(new Event('tick'));
console.log(counter.count); // 1
target.removeEventListener('tick', handler);
target.dispatchEvent(new Event('tick'));
console.log(counter.count); // 1

Đăng ký và gỡ phải dùng cùng target, loại sự kiện, callback và giá trị capture. Không cần giữ cùng object options chỉ để so danh tính options. Nếu đã đăng ký cả capture true và false, gỡ từng đăng ký tương ứng.

Arrow function tạo mới trong hai lời gọi add/remove cũng gặp vấn đề tương tự. Trong class hoặc component, lưu handler theo từng instance và gắn việc gỡ vào vòng đời sở hữu listener.

4. Gom listener theo một vòng đời bằng signal

const target = new EventTarget();
const controller = new AbortController();
let calls = 0;
target.addEventListener('tick', () => calls++, { signal: controller.signal });
target.addEventListener('refresh', () => calls++, { signal: controller.signal });
target.dispatchEvent(new Event('tick'));
target.dispatchEvent(new Event('refresh'));
console.log(calls); // 2
controller.abort();
target.dispatchEvent(new Event('tick'));
target.dispatchEvent(new Event('refresh'));
console.log(calls); // 2

Trong ví dụ này, abort gỡ hai listener đã đăng ký với signal. Cách này hữu ích khi một instance sở hữu nhiều đăng ký. Với lượt mount mới, tạo controller mới; signal đã abort không trở về trạng thái ban đầu.

Gỡ listener chỉ ngăn các lần gọi tương lai, không tự hoàn tác công việc đã chạy. Nếu handler đã bắt đầu một tác vụ bất đồng bộ, cần quản lý việc hủy hoặc bỏ qua kết quả của tác vụ đó riêng.

5. Kiểm thử hành vi sau cleanup

  • Phát sự kiện trước khi gỡ để chứng minh handler đã đăng ký.
  • Phát lại sau khi gỡ và xác nhận bộ đếm không tăng.
  • Thử nhiều lượt tạo và hủy instance để phát hiện đăng ký tích lũy.
  • Kiểm tra capture khớp khi dùng cây DOM thực tế.
  • Tạo controller mới cho mỗi vòng đời độc lập.

Tám khối ví dụ Việt–Anh đã chạy bằng EventTarget của Node.js, kèm kiểm tra cleanup lặp lại. Chưa kiểm thử capture/bubbling trong cây DOM hay đo bộ nhớ trình duyệt. Tham khảo MDN Function.bind, removeEventListener và addEventListener.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.