Skip to content

事件处理性能:让页面响应如丝般顺滑

当页面开始卡顿

你打开一个网站,开始滚动页面查看内容,但滚动并不流畅——页面时而停顿,时而跳跃,滚动条的移动不够丝滑。或者你在搜索框输入文字,每敲一个字母都会触发网络请求,浏览器明显变慢了。这些都是事件处理性能问题的典型表现。

事件处理看似简单,但如果不注意性能,很容易让页面变得卡顿。一个 scroll 事件在用户快速滚动时,每秒可能触发上百次;一个 mousemove 事件在鼠标移动时,触发频率甚至可能达到每秒数百次。如果每次触发都执行复杂的操作,浏览器就会不堪重负。

本章将带你深入了解事件处理的性能问题,掌握各种优化技巧,让你的页面无论如何交互都能保持流畅。

识别性能问题

高频事件的天然特性

某些事件具有高频触发的特性。当用户进行连续操作时,这些事件会在极短时间内被触发多次。

javascript
// 监控事件触发频率
let scrollCount = 0;
let lastTime = Date.now();

window.addEventListener("scroll", () => {
  scrollCount++;
  const now = Date.now();

  if (now - lastTime >= 1000) {
    console.log(`Scroll events per second: ${scrollCount}`);
    scrollCount = 0;
    lastTime = now;
  }
});

// 快速滚动时,输出可能是:
// Scroll events per second: 85
// Scroll events per second: 120
// Scroll events per second: 95

常见的高频事件包括:

  • scroll:滚动事件
  • mousemove:鼠标移动事件
  • touchmove:触摸移动事件
  • resize:窗口大小改变事件
  • input:输入事件(特别是在快速输入时)

性能问题的表现

当事件处理出现性能问题时,你会观察到:

  1. 主线程阻塞:页面冻结,用户操作无响应
  2. 帧率下降:动画卡顿,滚动不流畅
  3. 内存占用增加:大量事件监听器未被清理
  4. 网络请求过多:频繁的 API 调用
javascript
// ❌ 性能问题示例:每次滚动都执行复杂计算
window.addEventListener("scroll", () => {
  // 复杂的 DOM 查询
  const elements = document.querySelectorAll(".expensive-selector");

  // 强制同步布局(非常昂贵的操作)
  elements.forEach((element) => {
    const rect = element.getBoundingClientRect(); // 触发布局计算
    element.style.transform = `translateY(${rect.top}px)`; // 又触发一次
  });

  // 发送网络请求
  fetch("/api/track-scroll");
});

// 用户滚动一次,这段代码可能执行 100+ 次
// 每次都查询 DOM、计算布局、发送请求
// 页面会严重卡顿

优化技术 1:事件委托

事件委托不仅减少代码量,更重要的是大幅降低内存占用。

javascript
// ❌ 1000 个监听器
const items = document.querySelectorAll(".list-item"); // 1000 个元素
items.forEach((item) => {
  item.addEventListener("click", handleClick); // 1000 个监听器
  item.addEventListener("mouseenter", handleHover); // 又是 1000 个
  item.addEventListener("mouseleave", handleHoverEnd); // 再来 1000 个
});
// 总计:3000 个事件监听器,占用大量内存

// ✅ 3 个监听器
const list = document.querySelector(".list");
list.addEventListener("click", (e) => {
  const item = e.target.closest(".list-item");
  if (item) handleClick(e);
});
list.addEventListener(
  "mouseenter",
  (e) => {
    const item = e.target.closest(".list-item");
    if (item) handleHover(e);
  },
  true
); // 捕获阶段,因为 mouseenter 不冒泡
list.addEventListener(
  "mouseleave",
  (e) => {
    const item = e.target.closest(".list-item");
    if (item) handleHoverEnd(e);
  },
  true
);
// 总计:3 个事件监听器,内存占用降低 99.9%

性能提升明显:

  • 内存占用:从数 MB 降低到几 KB
  • 初始化时间:从几百毫秒降低到几毫秒
  • 动态元素:自动支持,无需额外绑定

优化技术 2:防抖(Debounce)

防抖的核心思想是:在事件触发后,延迟执行处理函数。如果在延迟期间事件再次触发,则重新计时。只有当事件停止触发超过指定时间后,才真正执行处理函数。

就像电梯门一样:只要有人继续进来,门就会重新计时,直到一段时间内没有人进入,门才会关闭。

防抖的实现

javascript
/**
 * 防抖函数
 * @param {Function} func - 要执行的函数
 * @param {number} delay - 延迟时间(毫秒)
 * @returns {Function} 防抖后的函数
 */
function debounce(func, delay) {
  let timeoutId = null;

  return function (...args) {
    // 清除之前的定时器
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    // 设置新的定时器
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

实际应用:搜索建议

javascript
const searchInput = document.getElementById("search");
const suggestions = document.getElementById("suggestions");

// ❌ 没有防抖:每次输入都请求
searchInput.addEventListener("input", async (e) => {
  const query = e.target.value;
  const results = await fetch(`/api/search?q=${query}`).then((r) => r.json());
  displaySuggestions(results);
});
// 输入 "javascript" 会发送 10 次请求:
// j, ja, jav, java, javas, javasc, javascr, javascri, javascrip, javascript

// ✅ 使用防抖:停止输入后才请求
const debouncedSearch = debounce(async (query) => {
  const results = await fetch(`/api/search?q=${query}`).then((r) => r.json());
  displaySuggestions(results);
}, 300); // 300ms 延迟

searchInput.addEventListener("input", (e) => {
  debouncedSearch(e.target.value);
});
// 输入 "javascript" 只发送 1 次请求(输入结束 300ms 后)

性能提升:

  • 网络请求:从 10 次减少到 1 次
  • 服务器压力:降低 90%
  • 用户体验:更快的响应,更少的闪烁

可取消的防抖

有时需要提前执行或取消防抖函数:

javascript
function debounce(func, delay) {
  let timeoutId = null;

  const debounced = function (...args) {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };

  // 立即执行
  debounced.immediate = function (...args) {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }
    func.apply(this, args);
  };

  // 取消执行
  debounced.cancel = function () {
    if (timeoutId) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }
  };

  return debounced;
}

// 使用
const search = debounce(performSearch, 300);

searchInput.addEventListener("input", (e) => {
  search(e.target.value);
});

// 立即执行搜索
searchButton.addEventListener("click", () => {
  search.immediate(searchInput.value);
});

// 用户离开页面时取消pending的搜索
window.addEventListener("beforeunload", () => {
  search.cancel();
});

优化技术 3:节流(Throttle)

节流的核心思想是:限制函数执行的频率。无论事件触发多频繁,处理函数都按照固定的时间间隔执行。

就像水龙头的限流阀:无论你开得多大,水流的速度都是固定的。

节流的实现

javascript
/**
 * 节流函数
 * @param {Function} func - 要执行的函数
 * @param {number} limit - 时间间隔(毫秒)
 * @returns {Function} 节流后的函数
 */
function throttle(func, limit) {
  let inThrottle = false;
  let lastResult;

  return function (...args) {
    if (!inThrottle) {
      inThrottle = true;
      lastResult = func.apply(this, args);

      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }

    return lastResult;
  };
}

实际应用:无限滚动加载

javascript
let page = 1;
let loading = false;

// ❌ 没有节流:疯狂触发
window.addEventListener("scroll", () => {
  if (loading) return;

  const scrollTop = window.scrollY;
  const windowHeight = window.innerHeight;
  const documentHeight = document.documentElement.scrollHeight;

  if (scrollTop + windowHeight >= documentHeight - 100) {
    loading = true;
    loadMoreContent(++page).then(() => {
      loading = false;
    });
  }
});
// 滚动到底部时,检查函数可能被调用 50+ 次

// ✅ 使用节流:每 200ms 最多执行一次
const throttledScroll = throttle(() => {
  if (loading) return;

  const scrollTop = window.scrollY;
  const windowHeight = window.innerHeight;
  const documentHeight = document.documentElement.scrollHeight;

  if (scrollTop + windowHeight >= documentHeight - 100) {
    loading = true;
    loadMoreContent(++page).then(() => {
      loading = false;
    });
  }
}, 200);

window.addEventListener("scroll", throttledScroll);
// 每 200ms 最多检查一次,大幅降低 CPU 使用率

改进的节流:首次立即执行 + 尾部执行

javascript
function throttle(func, limit) {
  let inThrottle = false;
  let lastArgs = null;
  let lastThis = null;

  return function (...args) {
    lastArgs = args;
    lastThis = this;

    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;

      setTimeout(() => {
        inThrottle = false;

        // 如果在节流期间有新的调用,执行最后一次
        if (lastArgs) {
          func.apply(lastThis, lastArgs);
          lastArgs = null;
          lastThis = null;
        }
      }, limit);
    }
  };
}

实际应用:滚动进度条

javascript
const progressBar = document.getElementById("reading-progress");

const updateProgress = throttle(() => {
  const scrollTop = window.scrollY;
  const documentHeight = document.documentElement.scrollHeight;
  const windowHeight = window.innerHeight;

  const scrollPercentage = (scrollTop / (documentHeight - windowHeight)) * 100;
  progressBar.style.width = `${scrollPercentage}%`;
}, 100); // 每 100ms 最多更新一次

window.addEventListener("scroll", updateProgress);

// 性能对比:
// 不节流:每秒 100+ 次 DOM 更新,导致重绘频繁
// 节流后:每秒最多 10 次更新,流畅且性能优秀

防抖 vs 节流:何时使用哪个?

防抖适用场景

特点:等待用户"停止"操作后执行

  • 搜索建议:用户停止输入后才发送请求
  • 窗口 resize:用户停止调整窗口大小后才重新布局
  • 表单验证:用户停止输入后才验证
  • 自动保存:用户停止编辑后才保存
javascript
// 搜索建议:防抖
const search = debounce((query) => {
  fetch(`/api/search?q=${query}`);
}, 300);

// 自动保存:防抖
const autoSave = debounce((content) => {
  localStorage.setItem("draft", content);
}, 1000);

// 窗口 resize:防抖
const handleResize = debounce(() => {
  recalculateLayout();
}, 250);

节流适用场景

特点:在连续操作中按固定频率执行

  • 滚动事件:滚动时定期更新位置
  • 鼠标移动:拖拽时定期更新位置
  • 动画帧:按固定帧率执行
  • 实时统计:定期更新数据
javascript
// 滚动进度:节流
const updateScrollProgress = throttle(() => {
  const progress = calculateScrollProgress();
  updateProgressBar(progress);
}, 100);

// 鼠标移动:节流
const handleMouseMove = throttle((e) => {
  updateCursorPosition(e.clientX, e.clientY);
}, 16); // 约 60fps

// 图表更新:节流
const updateChart = throttle((data) => {
  renderChart(data);
}, 500);

对比示例

javascript
// 场景:用户输入搜索关键词 "hello"

// 防抖:
// h -> 取消
// he -> 取消
// hel -> 取消
// hell -> 取消
// hello -> (300ms 后) 执行搜索
// 总结:只执行 1 次

// 节流(100ms):
// h -> 执行搜索 "h"
// he -> 跳过(100ms 未到)
// hel -> 执行搜索 "hel"(100ms 已到)
// hell -> 跳过
// hello -> 执行搜索 "hello"(100ms 已到)
// 总结:执行 3 次,每 100ms 最多 1 次

优化技术 4:被动事件监听器(Passive Listeners)

某些事件(特别是触摸和滚动事件)的默认行为会被 preventDefault() 阻止。浏览器在触发事件时,必须等待所有监听器执行完毕,才能知道是否要执行默认行为。这会导致滚动延迟。

被动监听器告诉浏览器:"我承诺不会调用 preventDefault()",浏览器就可以立即执行默认行为,无需等待。

javascript
// ❌ 非被动监听器(默认)
document.addEventListener("touchstart", (e) => {
  // 浏览器必须等待这个函数执行完
  // 才能知道是否要执行默认的滚动行为
  handleTouch(e);
});
// 可能导致滚动延迟

// ✅ 被动监听器
document.addEventListener(
  "touchstart",
  (e) => {
    // 浏览器知道我们不会调用 preventDefault()
    // 可以立即执行滚动,不需要等待
    handleTouch(e);
  },
  { passive: true }
);
// 滚动更流畅

实际应用

javascript
// 滚动事件监听(用于数据统计)
let scrollDistance = 0;

window.addEventListener(
  "scroll",
  () => {
    scrollDistance += Math.abs(window.scrollY - lastScrollY);
    lastScrollY = window.scrollY;
  },
  { passive: true }
); // 告诉浏览器我们不会阻止滚动

// 触摸事件监听(用于手势识别)
let touchStartY = 0;

document.addEventListener(
  "touchstart",
  (e) => {
    touchStartY = e.touches[0].clientY;
  },
  { passive: true }
);

document.addEventListener(
  "touchmove",
  (e) => {
    const touchY = e.touches[0].clientY;
    const deltaY = touchY - touchStartY;

    // 如果确实需要阻止默认行为,则不能使用 passive
    // 这里我们只是记录,不阻止,所以可以使用 passive
    trackSwipe(deltaY);
  },
  { passive: true }
);

何时不能使用 passive

如果你需要调用 preventDefault(),就不能使用 passive: true

javascript
// ❌ 这样会报错
document.addEventListener(
  "touchstart",
  (e) => {
    e.preventDefault(); // 错误!passive 监听器不能调用 preventDefault
  },
  { passive: true }
);

// ✅ 如果需要阻止默认行为,不要使用 passive
document.addEventListener("touchstart", (e) => {
  if (shouldPreventDefault(e)) {
    e.preventDefault(); // 可以调用
  }
}); // 不设置 passive

// ✅ 或者根据条件动态设置
const needsPrevent = checkIfNeedsPrevent();

document.addEventListener("touchstart", handleTouch, {
  passive: !needsPrevent,
});

优化技术 5:及时移除事件监听器

未被移除的事件监听器会导致内存泄漏,特别是当元素被移除但监听器仍然存在时。

常见内存泄漏场景

javascript
// ❌ 内存泄漏示例
function createModal() {
  const modal = document.createElement("div");
  modal.className = "modal";
  document.body.appendChild(modal);

  // 添加事件监听器
  modal.addEventListener("click", handleModalClick);
  window.addEventListener("resize", handleResize);

  // 关闭模态框
  function closeModal() {
    document.body.removeChild(modal);
    // 问题:事件监听器没有被移除!
    // modal 的 click 监听器仍然存在(虽然元素已被移除)
    // window 的 resize 监听器仍然存在
  }

  return { closeModal };
}

// 每次调用都会泄漏内存
const modal1 = createModal(); // +2 个监听器
modal1.closeModal(); // 监听器未清理

const modal2 = createModal(); // +2 个监听器
modal2.closeModal(); // 监听器未清理

// 内存中现在有 4 个僵尸监听器

正确的清理方式

javascript
// ✅ 正确的做法
function createModal() {
  const modal = document.createElement("div");
  modal.className = "modal";
  document.body.appendChild(modal);

  // 使用命名函数,方便移除
  function handleModalClick(e) {
    // 处理点击
  }

  function handleResize() {
    // 处理窗口大小改变
  }

  // 添加监听器
  modal.addEventListener("click", handleModalClick);
  window.addEventListener("resize", handleResize);

  function closeModal() {
    // 移除监听器
    modal.removeEventListener("click", handleModalClick);
    window.removeEventListener("resize", handleResize);

    // 移除元素
    document.body.removeChild(modal);
  }

  return { closeModal };
}

使用 AbortController 批量移除

AbortController 提供了一种优雅的方式来批量移除事件监听器。

javascript
class Component {
  constructor(element) {
    this.element = element;
    this.abortController = new AbortController();
    this.signal = this.abortController.signal;

    this.init();
  }

  init() {
    // 所有监听器使用同一个 signal
    this.element.addEventListener("click", this.handleClick, {
      signal: this.signal,
    });

    window.addEventListener("resize", this.handleResize, {
      signal: this.signal,
    });

    document.addEventListener("keydown", this.handleKeydown, {
      signal: this.signal,
    });
  }

  handleClick = (e) => {
    console.log("Click");
  };

  handleResize = () => {
    console.log("Resize");
  };

  handleKeydown = (e) => {
    console.log("Keydown");
  };

  destroy() {
    // 一次性移除所有监听器
    this.abortController.abort();
    this.element.remove();
  }
}

// 使用
const component = new Component(document.getElementById("my-component"));

// 销毁时自动清理所有监听器
component.destroy();

优化技术 6:避免强制同步布局

在事件处理器中读取布局信息(如 offsetHeightgetBoundingClientRect())后立即修改样式,会导致浏览器强制重新计算布局,这是非常昂贵的操作。

javascript
// ❌ 强制同步布局(非常慢)
elements.forEach((element) => {
  const height = element.offsetHeight; // 读取布局
  element.style.height = height + 10 + "px"; // 修改样式,触发布局
  // 下一轮循环又读取布局,浏览器被迫再次计算
});
// 每个元素都导致一次完整的布局计算(Layout Thrashing)

// ✅ 批量读取,然后批量写入
const heights = [];

// 第一步:批量读取
elements.forEach((element) => {
  heights.push(element.offsetHeight);
});

// 第二步:批量写入
elements.forEach((element, i) => {
  element.style.height = heights[i] + 10 + "px";
});
// 只触发一次布局计算

实际应用:动画优化

javascript
// ❌ 在 scroll 事件中读写混合
window.addEventListener("scroll", () => {
  elements.forEach((element) => {
    const rect = element.getBoundingClientRect(); // 读取
    element.style.transform = `translateY(${rect.top * 0.5}px)`; // 写入
  });
});
// 每次滚动都会导致多次强制同步布局

// ✅ 使用 requestAnimationFrame 优化
let ticking = false;

window.addEventListener("scroll", () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      updateParallax();
      ticking = false;
    });
    ticking = true;
  }
});

function updateParallax() {
  const scrollY = window.scrollY;

  elements.forEach((element) => {
    // 所有读取操作使用相同的 scrollY
    const translateY = scrollY * 0.5;
    element.style.transform = `translateY(${translateY}px)`;
  });
}

性能测试与监控

使用 Performance API

javascript
// 测量事件处理器的执行时间
function measureEventPerformance(eventName, handler) {
  return function (event) {
    const startTime = performance.now();

    handler.call(this, event);

    const endTime = performance.now();
    const duration = endTime - startTime;

    if (duration > 16) {
      // 超过一帧的时间(60fps)
      console.warn(`${eventName} handler took ${duration.toFixed(2)}ms`);
    }
  };
}

// 使用
element.addEventListener(
  "click",
  measureEventPerformance("click", (e) => {
    // 你的处理逻辑
    heavyComputation();
  })
);

使用 Chrome DevTools

  1. Performance 面板:录制用户交互,查看事件处理器的执行时间
  2. Memory 面板:检查内存泄漏,查看事件监听器数量
  3. Rendering 面板:开启 "Paint flashing" 和 "FPS meter" 查看重绘和帧率

实际优化案例

案例 1:优化表格排序

javascript
// ❌ 未优化的版本
table.addEventListener("click", (e) => {
  if (e.target.tagName === "TH") {
    const column = e.target.dataset.column;

    // 直接操作 DOM,每次都重新渲染整个表格
    const rows = Array.from(table.querySelectorAll("tbody tr"));
    rows.sort((a, b) => {
      const aVal = a.querySelector(`td:nth-child(${column})`).textContent;
      const bVal = b.querySelector(`td:nth-child(${column})`).textContent;
      return aVal.localeCompare(bVal);
    });

    const tbody = table.querySelector("tbody");
    tbody.innerHTML = "";
    rows.forEach((row) => tbody.appendChild(row));
  }
});

// ✅ 优化后的版本
// 1. 使用事件委托(已经在用)
// 2. 使用防抖避免快速点击
// 3. 使用 DocumentFragment 减少重排
// 4. 缓存数据避免重复查询

let sortedData = null;

const sortTable = debounce((column) => {
  if (!sortedData) {
    // 首次排序,提取数据
    const rows = Array.from(table.querySelectorAll("tbody tr"));
    sortedData = rows.map((row) => {
      const cells = Array.from(row.querySelectorAll("td"));
      return {
        element: row,
        data: cells.map((cell) => cell.textContent),
      };
    });
  }

  // 排序数据
  sortedData.sort((a, b) => {
    return a.data[column].localeCompare(b.data[column]);
  });

  // 使用 DocumentFragment 批量更新
  const fragment = document.createDocumentFragment();
  sortedData.forEach((item) => fragment.appendChild(item.element));

  const tbody = table.querySelector("tbody");
  tbody.innerHTML = "";
  tbody.appendChild(fragment);
}, 100);

table.addEventListener("click", (e) => {
  if (e.target.tagName === "TH") {
    sortTable(parseInt(e.target.dataset.column));
  }
});

案例 2:优化无限滚动

javascript
// ✅ 综合优化的无限滚动
class InfiniteScroll {
  constructor(options) {
    this.container = options.container;
    this.threshold = options.threshold || 200;
    this.onLoad = options.onLoad;
    this.loading = false;
    this.page = 1;
    this.hasMore = true;

    // 使用 Intersection Observer 代替 scroll 事件
    this.setupObserver();
  }

  setupObserver() {
    // Intersection Observer 性能远优于 scroll 事件
    this.observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting && !this.loading && this.hasMore) {
            this.loadMore();
          }
        });
      },
      {
        root: this.container,
        rootMargin: `${this.threshold}px`,
      }
    );

    // 观察一个底部标记元素
    this.sentinel = document.createElement("div");
    this.sentinel.className = "scroll-sentinel";
    this.container.appendChild(this.sentinel);
    this.observer.observe(this.sentinel);
  }

  async loadMore() {
    this.loading = true;

    try {
      const data = await this.onLoad(this.page);

      if (data.length === 0) {
        this.hasMore = false;
        this.observer.unobserve(this.sentinel);
        return;
      }

      // 使用 DocumentFragment 批量添加
      const fragment = document.createDocumentFragment();
      data.forEach((item) => {
        const element = this.createItemElement(item);
        fragment.appendChild(element);
      });

      this.container.insertBefore(fragment, this.sentinel);
      this.page++;
    } finally {
      this.loading = false;
    }
  }

  createItemElement(item) {
    const div = document.createElement("div");
    div.className = "item";
    div.textContent = item.title;
    return div;
  }

  destroy() {
    this.observer.disconnect();
    this.sentinel.remove();
  }
}

// 使用
const scrollLoader = new InfiniteScroll({
  container: document.getElementById("content"),
  threshold: 200,
  onLoad: async (page) => {
    const response = await fetch(`/api/items?page=${page}`);
    return response.json();
  },
});

最佳实践总结

1. 选择合适的优化技术

  • 大量相似元素:使用事件委托
  • 用户停止操作后执行:使用防抖
  • 连续操作中定期执行:使用节流
  • 触摸和滚动事件:使用 passive listeners
  • 不再需要的监听器:及时移除

2. 性能优先级

javascript
// 优先级从高到低:

// 1. 避免不必要的监听器(最重要)
// 使用事件委托,一个监听器代替成百上千个

// 2. 使用合适的 API
// Intersection Observer > Scroll Event
// ResizeObserver > Resize Event

// 3. 优化处理器性能
// 使用节流/防抖
// 避免强制同步布局
// 使用 requestAnimationFrame

// 4. 及时清理
// 移除不需要的监听器
// 使用 AbortController 批量管理

3. 测试与监控

javascript
// 在开发环境中添加性能监控
if (process.env.NODE_ENV === "development") {
  let eventCounts = {};

  const originalAddEventListener = EventTarget.prototype.addEventListener;
  EventTarget.prototype.addEventListener = function (type, listener, options) {
    eventCounts[type] = (eventCounts[type] || 0) + 1;
    console.log(`Event listeners: ${JSON.stringify(eventCounts)}`);

    return originalAddEventListener.call(this, type, listener, options);
  };
}

4. 代码检查清单

  • [ ] 是否使用了事件委托来减少监听器数量?
  • [ ] 高频事件(scroll、mousemove)是否使用了节流或防抖?
  • [ ] 搜索、保存等操作是否使用了防抖?
  • [ ] 触摸和滚动事件是否使用了 passive listeners?
  • [ ] 组件销毁时是否移除了所有监听器?
  • [ ] 是否避免了在事件处理器中进行强制同步布局?
  • [ ] 是否使用了现代 API(Intersection Observer、ResizeObserver)?

总结

事件处理性能优化是前端开发中至关重要的一环。通过本章学习,你应该掌握了:

  1. 识别性能问题:了解高频事件的特性和性能瓶颈
  2. 事件委托:用一个监听器代替成百上千个,大幅降低内存占用
  3. 防抖技术:等待用户停止操作后再执行,减少不必要的计算和网络请求
  4. 节流技术:限制函数执行频率,保持流畅的用户体验
  5. 被动监听器:告诉浏览器不会阻止默认行为,提升滚动性能
  6. 及时清理:移除不需要的监听器,避免内存泄漏
  7. 避免强制同步布局:分离读写操作,减少布局计算

性能优化不是过早优化,而是有意识的设计。在编写事件处理代码时,时刻想着性能,选择合适的技术,你的应用就能保持丝般顺滑的响应速度。

至此,我们已经完整学习了 JavaScript 事件系统的核心内容。从事件的基本概念,到事件处理器、监听器、事件对象、冒泡与捕获,再到事件委托、自定义事件和性能优化。掌握这些知识,你就能构建出高性能、用户体验优秀的交互式 Web 应用。