jQuery 事件處理 (Events)

jQuery 提供了統一且簡潔的方式來處理各種 DOM 事件,包括滑鼠事件、鍵盤事件、表單事件等,並且自動處理跨瀏覽器的相容性問題。

事件綁定

on() - 標準綁定方式

on() 是 jQuery 推薦的事件綁定方法:

// 基本語法
$(selector).on(events, handler);

// 綁定 click 事件
$('#btn').on('click', function () {
  alert('按鈕被點擊了!');
});

// 綁定多個事件(空格分隔)
$('input').on('focus blur', function () {
  $(this).toggleClass('focused');
});

// 使用物件綁定多個事件
$('#box').on({
  mouseenter: function () {
    $(this).addClass('hover');
  },
  mouseleave: function () {
    $(this).removeClass('hover');
  },
  click: function () {
    $(this).toggleClass('active');
  },
});

傳遞資料給事件處理函式

$('#btn').on('click', { name: 'John', id: 123 }, function (event) {
  console.log(event.data.name); // 'John'
  console.log(event.data.id); // 123
});

off() - 解除綁定

// 移除特定事件的所有處理函式
$('#btn').off('click');

// 移除所有事件
$('#btn').off();

// 移除特定處理函式(需要使用命名函式)
function handleClick() {
  console.log('clicked');
}

$('#btn').on('click', handleClick);
$('#btn').off('click', handleClick);

命名空間

使用命名空間可以更精確地管理事件:

// 綁定帶命名空間的事件
$('#btn').on('click.myPlugin', function () {
  console.log('plugin click');
});

$('#btn').on('click.analytics', function () {
  console.log('analytics click');
});

// 只移除特定命名空間的事件
$('#btn').off('click.myPlugin');

// 移除某命名空間下的所有事件
$('#btn').off('.myPlugin');
舊方法提醒bind()unbind()delegate()undelegate() 在 jQuery 3.0 已被棄用,請統一使用 on()off()

事件委派 (Event Delegation)

事件委派是將事件綁定在父元素上,利用事件冒泡來處理子元素的事件。這對於動態新增的元素特別有用。

// 語法
$(parentSelector).on(events, childSelector, handler);

// 範例:綁定在 ul 上,但處理 li 的點擊
$('ul').on('click', 'li', function () {
  $(this).toggleClass('selected');
});

// 動態新增的 li 也會有效
$('ul').append('<li>新項目</li>');

事件委派的好處

  1. 動態元素:新增的元素自動具有事件處理
  2. 效能:只需要一個事件處理函式,而非每個元素各一個
  3. 記憶體:減少事件處理函式的數量
// 不好的做法 - 每個按鈕都綁定事件
$('.delete-btn').on('click', function () {
  $(this).closest('tr').remove();
});
// 問題:新增的按鈕不會有事件

// 好的做法 - 使用事件委派
$('table').on('click', '.delete-btn', function () {
  $(this).closest('tr').remove();
});
// 新增的按鈕也會有效

常用事件

滑鼠事件

$('#box').on('click', fn); // 點擊
$('#box').on('dblclick', fn); // 雙擊
$('#box').on('mouseenter', fn); // 滑鼠進入(不冒泡)
$('#box').on('mouseleave', fn); // 滑鼠離開(不冒泡)
$('#box').on('mouseover', fn); // 滑鼠移入(冒泡)
$('#box').on('mouseout', fn); // 滑鼠移出(冒泡)
$('#box').on('mousemove', fn); // 滑鼠移動
$('#box').on('mousedown', fn); // 滑鼠按下
$('#box').on('mouseup', fn); // 滑鼠放開
$('#box').on('contextmenu', fn); // 右鍵選單

hover() - 滑鼠懸停

hover()mouseentermouseleave 的簡寫:

// 兩個函式:進入和離開
$('#box').hover(
  function () {
    $(this).addClass('hover');
  },
  function () {
    $(this).removeClass('hover');
  }
);

// 一個函式:進入和離開執行相同動作
$('#box').hover(function () {
  $(this).toggleClass('hover');
});

鍵盤事件

$(document).on('keydown', fn); // 按鍵按下
$(document).on('keyup', fn); // 按鍵放開
$(document).on('keypress', fn); // 按鍵按下(已棄用,建議用 keydown)

// 取得按下的鍵
$(document).on('keydown', function (e) {
  console.log('鍵碼:', e.which); // 鍵碼
  console.log('按鍵:', e.key); // 按鍵名稱
  console.log('Ctrl:', e.ctrlKey); // 是否按住 Ctrl
  console.log('Shift:', e.shiftKey); // 是否按住 Shift
  console.log('Alt:', e.altKey); // 是否按住 Alt

  // 檢查特定按鍵
  if (e.key === 'Enter') {
    // 按下 Enter
  }

  if (e.key === 'Escape') {
    // 按下 Esc
  }

  // 組合鍵
  if (e.ctrlKey && e.key === 's') {
    e.preventDefault(); // 阻止瀏覽器儲存
    // 自訂儲存動作
  }
});

表單事件

$('input').on('focus', fn); // 獲得焦點
$('input').on('blur', fn); // 失去焦點
$('input').on('change', fn); // 值改變(失去焦點時)
$('input').on('input', fn); // 值改變(即時)
$('input').on('select', fn); // 文字被選取
$('form').on('submit', fn); // 表單送出
$('form').on('reset', fn); // 表單重設

// change vs input
$('input').on('change', function () {
  // 失去焦點時才觸發
});

$('input').on('input', function () {
  // 每次輸入都觸發(即時驗證常用)
});

文件/視窗事件

// 文件就緒
$(document).ready(fn);
// 或
$(fn);

// 頁面完全載入(含圖片等資源)
$(window).on('load', fn);

// 視窗大小改變
$(window).on('resize', fn);

// 捲動
$(window).on('scroll', fn);

// 離開頁面前
$(window).on('beforeunload', function () {
  return '確定要離開嗎?';
});

事件物件 (Event Object)

事件處理函式會接收一個事件物件,包含事件的相關資訊:

$('#box').on('click', function (event) {
  // 事件類型
  console.log(event.type); // 'click'

  // 目標元素(實際被點擊的元素)
  console.log(event.target); // DOM 元素

  // 綁定事件的元素(等同於 this)
  console.log(event.currentTarget); // DOM 元素

  // 滑鼠座標
  console.log(event.pageX); // 相對於文件
  console.log(event.pageY);
  console.log(event.clientX); // 相對於視窗
  console.log(event.clientY);

  // 事件發生時間
  console.log(event.timeStamp);

  // 傳遞的資料
  console.log(event.data);

  // 相關元素(mouseover/mouseout)
  console.log(event.relatedTarget);
});

阻止預設行為

$('a').on('click', function (event) {
  event.preventDefault(); // 阻止連結跳轉
  // 自訂處理
});

$('form').on('submit', function (event) {
  event.preventDefault(); // 阻止表單送出
  // 使用 Ajax 送出
});

阻止事件冒泡

$('.child').on('click', function (event) {
  event.stopPropagation(); // 阻止事件冒泡到父元素
});

// 同時阻止預設行為和冒泡
$('a').on('click', function (event) {
  event.preventDefault();
  event.stopPropagation();
  // 或者
  return false; // 等同於上面兩行
});

stopImmediatePropagation()

阻止同一元素上其他處理函式執行,並阻止冒泡:

$('#btn').on('click', function (event) {
  console.log('第一個處理函式');
  event.stopImmediatePropagation();
});

$('#btn').on('click', function () {
  console.log('第二個處理函式'); // 不會執行
});

觸發事件

trigger() - 觸發事件

// 觸發 click 事件
$('#btn').trigger('click');

// 簡寫
$('#btn').click();

// 觸發並傳遞資料
$('#btn').trigger('click', ['參數1', '參數2']);

// 接收資料
$('#btn').on('click', function (event, param1, param2) {
  console.log(param1, param2);
});

// 觸發自訂事件
$('#box').trigger('myCustomEvent');

triggerHandler() - 只觸發處理函式

trigger() 不同,triggerHandler()

  • 不會觸發事件的預設行為
  • 不會冒泡
  • 只影響第一個匹配的元素
  • 返回處理函式的返回值
// trigger() 會觸發 focus 的預設行為(獲得焦點)
$('input').trigger('focus');

// triggerHandler() 只執行處理函式
$('input').triggerHandler('focus');

一次性事件

one() - 只執行一次

// 事件只會觸發一次
$('#btn').one('click', function () {
  console.log('只會執行一次');
});

// 多個事件各執行一次
$('#btn').one('click mouseenter', function (e) {
  console.log(e.type + ' 觸發了');
});

// 搭配事件委派
$('ul').one('click', 'li', function () {
  console.log('第一次點擊 li');
});

自訂事件

// 定義自訂事件
$('#box').on('highlight', function (event, color) {
  $(this).css('background-color', color || 'yellow');
});

// 觸發自訂事件
$('#btn').on('click', function () {
  $('#box').trigger('highlight', ['red']);
});

// 實際應用:發布/訂閱模式
var $events = $({}); // 建立事件中心

// 訂閱
$events.on('userLoggedIn', function (e, user) {
  console.log('使用者登入:', user.name);
});

// 發布
$events.trigger('userLoggedIn', [{ name: 'John' }]);

實用範例

表單驗證

$('form').on('submit', function (e) {
  e.preventDefault();

  var isValid = true;

  $(this)
    .find('input[required]')
    .each(function () {
      if (!$(this).val().trim()) {
        $(this).addClass('error');
        isValid = false;
      } else {
        $(this).removeClass('error');
      }
    });

  if (isValid) {
    this.submit(); // 原生 submit
  }
});

// 即時驗證
$('input').on('input', function () {
  var $input = $(this);
  if ($input.val().trim()) {
    $input.removeClass('error');
  }
});

防止重複點擊

$('#submit').on('click', function () {
  var $btn = $(this);

  if ($btn.data('loading')) return;

  $btn.data('loading', true).text('處理中...');

  // 模擬 Ajax
  setTimeout(function () {
    $btn.data('loading', false).text('送出');
  }, 2000);
});

快捷鍵

$(document).on('keydown', function (e) {
  // Ctrl/Cmd + S 儲存
  if ((e.ctrlKey || e.metaKey) && e.key === 's') {
    e.preventDefault();
    saveDocument();
  }

  // Esc 關閉 Modal
  if (e.key === 'Escape') {
    $('.modal').hide();
  }
});

節流 (Throttle)

限制事件觸發頻率:

function throttle(fn, delay) {
  var lastCall = 0;
  return function () {
    var now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      fn.apply(this, arguments);
    }
  };
}

// 捲動事件節流
$(window).on(
  'scroll',
  throttle(function () {
    console.log('捲動位置:', $(this).scrollTop());
  }, 100)
);

防抖 (Debounce)

延遲執行,在停止觸發後才執行:

function debounce(fn, delay) {
  var timer;
  return function () {
    var context = this;
    var args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function () {
      fn.apply(context, args);
    }, delay);
  };
}

// 搜尋輸入防抖
$('#search').on(
  'input',
  debounce(function () {
    var query = $(this).val();
    console.log('搜尋:', query);
    // 執行搜尋
  }, 300)
);