Kiến thức nền tảng
Sự kiện trong trình duyệt tuân theo mô hình xuất bản–đăng ký (publish-subscribe). DOM event không phải ngoại lệ — mỗi lần gọi
addEventListener tương đương với một đăng ký, và mỗi lần gọi
dispatchEvent là một lần xuất bản. Dưới đây là minh họa khái niệm bằng cách triển khai thủ công:
const BulletinBoard = {
listeners: [],
subscribe(handler) {
this.listeners.push(handler);
},
publish(payload) {
this.listeners.forEach(handler => handler(payload));
}
};
// Hai thành phần độc lập đăng ký cùng một sự kiện
BulletinBoard.subscribe(data => console.log('Lắng nghe A:', data));
BulletinBoard.subscribe(data => console.log('Lắng nghe B:', data));
// Một bên kích hoạt sự kiện
BulletinBoard.publish({ status: 'mới' });
Giao diện DOM Event cơ bản
Các thao tác chính với sự kiện DOM gồm tạo, khởi tạo và phát tán:
// Tạo đối tượng sự kiện tùy chỉnh
const evt = document.createEvent('CustomEvent');
// Khởi tạo: tên, có nổi bọt?, có hủy được?
evt.initCustomEvent('data-loaded', true, false, { id: 123 });
// Phát tán lên một phần tử
document.getElementById('container').dispatchEvent(evt);
// Đăng ký xử lý
element.addEventListener('data-loaded', handler, false);
element.removeEventListener('data-loaded', handler, false);
Vấn đề tương thích trình duyệt
Một số sự kiện như
focus,
blur,
mouseenter,
mouseleave không nổi bọt — gây khó khăn cho việc ủy quyền (event delegation). Giải pháp phổ biến là sử dụng các sự kiện thay thế hỗ trợ nổi bọt:
focusin/focusout thay cho focus/blur
mouseover/mouseout kết hợp kiểm tra relatedTarget để mô phỏng mouseenter/mouseleave
Đặc biệt, thuộc tính
relatedTarget giúp xác định phần tử liền kề khi di chuyển chuột:
- Trong
mouseover: trỏ tới phần tử vừa rời khỏi
- Trong
mouseout: trỏ tới phần tử sắp bước vào
Do đó,
mouseenter chỉ xảy ra khi
relatedTarget không nằm trong cây con của phần tử đích.
Cấu trúc mô-đun sự kiện Zepto
$.Event(): Tạo sự kiện linh hoạt
$.Event = function(type, config = {}) {
if (typeof type !== 'string') {
config = type;
type = config.type || '';
}
const eventType = specialEvents[type] || 'Events';
const nativeEvent = document.createEvent(eventType);
const bubbles = config.bubbles !== false;
const cancelable = config.cancelable !== false;
// Gán thuộc tính tùy chỉnh
Object.keys(config).forEach(key => {
if (key !== 'bubbles' && key !== 'cancelable') {
nativeEvent[key] = config[key];
}
});
nativeEvent.initEvent(type, bubbles, cancelable);
return enhanceEvent(nativeEvent);
};
Hàm này đóng gói logic tạo sự kiện gốc, đồng thời thêm khả năng truyền cấu hình như
bubbles,
detail, hoặc
target.
$.proxy(): Điều khiển ngữ cảnh thực thi
$.proxy = function(fn, context, ...fixedArgs) {
if (typeof fn !== 'function') {
throw new TypeError('Hàm đầu vào bắt buộc phải là function');
}
return function(...callArgs) {
const args = fixedArgs.length ? [...fixedArgs, ...callArgs] : callArgs;
return fn.apply(context, args);
};
};
So sánh với
Function.prototype.bind,
$.proxy cung cấp cú pháp ngắn gọn hơn và xử lý trường hợp
context là chuỗi (truy cập thuộc tính hàm động).
.on() và .off(): Quản lý vòng đời sự kiện
Triển khai
.on() phân nhánh rõ ràng giữa hai chế độ:
- Trực tiếp: gắn xử lý lên phần tử hiện tại
- Ủy quyền: gắn xử lý lên phần tử cha, sau đó dùng
.closest(selector) để tìm phần tử đích tại thời điểm sự kiện xảy ra
$.fn.on = function(events, selector, data, callback, once = false) {
// Xử lý dạng đối tượng: { click: h1, input: h2 }
if (typeof events === 'object' && !isString(events)) {
for (const [type, handler] of Object.entries(events)) {
this.on(type, selector, data, handler, once);
}
return this;
}
// Chuẩn hóa tham số
if (!isString(selector) && typeof callback !== 'function') {
[callback, data, selector] = [data, selector, undefined];
}
return this.each(function() {
const el = this;
const proxyHandler = once
? createOnceWrapper(callback, el)
: selector
? createDelegatedHandler(el, selector, callback)
: callback;
registerListener(el, events, proxyHandler, data, selector);
});
};
Hàm
registerListener nội bộ duy trì bảng băm
handlers để lưu trữ tất cả bộ xử lý đã đăng ký, kèm đánh dấu
zid nhằm tối ưu truy vấn.
Xử lý đặc biệt cho sự kiện không nổi bọt
Zepto tự động chuyển đổi:
focus → focusin nếu trình duyệt hỗ trợ
mouseenter → mouseover + kiểm tra relatedTarget
const EVENT_MAP = {
mouseenter: 'mouseover',
mouseleave: 'mouseout',
focus: 'focusin',
blur: 'focusout'
};
function getNativeEventType(type) {
if (type in EVENT_MAP) {
return supportsFocusIn && type in EVENT_MAP.focus
? EVENT_MAP.focus[type]
: EVENT_MAP[type];
}
return type;
}
.trigger() và .triggerHandler()
Khác biệt then chốt:
.trigger() gọi dispatchEvent trên DOM, kích hoạt toàn bộ cơ chế sự kiện (nổi bọt, mặc định)
.triggerHandler() chỉ chạy các hàm xử lý đã đăng ký qua Zepto, bỏ qua DOM và không nổi bọt
$.fn.trigger = function(name, extraArgs = []) {
const event = isString(name) ? $.Event(name, { detail: extraArgs }) : name;
return this.each(function() {
if (this.dispatchEvent) {
this.dispatchEvent(event);
} else if (name in NATIVE_METHODS && typeof this[name] === 'function') {
this[name]();
}
});
};
Hỗ trợ kiểm soát luồng sự kiện
Các phương thức kiểm tra trạng thái như
isDefaultPrevented() được triển khai bằng kỹ thuật "monkey-patching": ghi đè các phương thức gốc (
preventDefault,
stopPropagation) để thiết lập cờ tương ứng trong đối tượng sự kiện.
function enhanceEvent(evt) {
const originalMethods = {
preventDefault: evt.preventDefault,
stopPropagation: evt.stopPropagation,
stopImmediatePropagation: evt.stopImmediatePropagation
};
evt.preventDefault = function() {
evt.isDefaultPrevented = () => true;
return originalMethods.preventDefault.call(evt);
};
evt.stopPropagation = function() {
evt.isPropagationStopped = () => true;
return originalMethods.stopPropagation.call(evt);
};
return evt;
}