Phân tích mã nguồn Zepto: Mô-đun Xử lý Sự kiện

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().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:
  • focusfocusin nếu trình duyệt hỗ trợ
  • mouseentermouseover + 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().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;
}

Thẻ: zepto dom-events JavaScript event-delegation web-compatibility

Đăng vào ngày 6 tháng 6 lúc 22:22