前端面经-JS篇(三)--事件、性能优化、防抖与节流

一、事件

下面会将js的事件的所有内容讲清楚

JavaScript 中的事件处理是前端开发中非常核心的概念之一。了解事件的工作原理、处理机制以及常见的难点,能够帮助你更好地编写交互式应用程序。以下是关于 JavaScript 事件的详细讲解,包括基本概念、常见事件、事件流、事件处理器、事件委托、冒泡与捕获等内容。

1. JavaScript 事件基本概念

事件是用户与网页交互时所触发的行为,例如点击按钮、移动鼠标、键盘输入等。这些事件通常会触发一个回调函数(事件处理程序),以响应用户的行为。

常见的事件类型:
  • 鼠标事件clickdblclickmousedownmouseupmousemovemouseovermouseout

  • 键盘事件keydownkeypresskeyup

  • 表单事件submitchangefocusblur

  • 加载事件loadDOMContentLoadedbeforeunloadunload

  • 触摸事件touchstarttouchmovetouchendtouchcancel

2. 事件模型

2.1 事件流(Event Flow)

JavaScript 中的事件流主要有两个阶段:

  1. 捕获阶段(Capturing Phase) :事件从根节点(documentwindow)向事件目标元素传播。

  2. 目标阶段(Target Phase):事件到达目标元素,触发目标元素的事件处理程序。

  3. 冒泡阶段(Bubbling Phase):事件从目标元素向根节点传播。

事件流的顺序

  1. 捕获阶段:从根节点到目标元素。

  2. 目标阶段:事件到达目标元素。

  3. 冒泡阶段:从目标元素向上冒泡到根节点。

事件传递定义了元素事件触发的顺序。 如果你将 <p> 元素插入到 <div> 元素中,用户点击 <p> 元素, 哪个元素的 "click" 事件先被触发呢?

冒泡中,内部元素的事件会先被触发,然后再触发外部元素,即: <p> 元素的点击事件先触发,然后会触发 <div> 元素的点击事件。

捕获中,外部元素的事件会先被触发,然后才会触发内部元素的事件,即: <div> 元素的点击事件先触发 ,然后再触发 <p> 元素的点击事件。

2.2 事件捕获(Event Capturing)

事件捕获与冒泡相反,事件会从最外层(根节点)开始向内层元素传播。虽然默认情况下是事件冒泡,但你可以通过 addEventListener 的第三个参数来启用事件捕获。

复制代码
document.getElementById('parent').addEventListener('click', function() {
    alert('Parent clicked');
}, true);  // 第三个参数为 true 启用捕获

在捕获阶段,事件从根节点开始向目标元素传播。

2.3 事件冒泡(Event Bubbling)

事件冒泡是指事件从目标元素开始向外层元素传播。默认情况下,事件会在冒泡阶段向父元素传播,直到到达 documentwindow

复制代码
document.getElementById('child').addEventListener('click', function() {
    alert('Child clicked');
});
document.getElementById('parent').addEventListener('click', function() {
    alert('Parent clicked');
});

如果点击 child 元素,会先触发 child 的点击事件,再触发 parent 的点击事件(事件冒泡)。

3. 事件处理程序(Event Handlers)

3.1 添加事件监听器

可以使用 addEventListener() 方法来为 DOM 元素添加事件监听器。

复制代码
 let button = document.querySelector('button');
button.addEventListener('click', function() {
    alert('Button clicked');
});

addEventListener(type, listener, options)

  • type:事件类型,如 clickkeyup 等。

  • listener:事件处理函数。

  • options:可选参数,指定事件的行为,如是否启用捕获阶段。默认值为 false, 即冒泡传递,当值为 true 时, 事件使用捕获传递。

3.2 移除事件监听器

使用 removeEventListener() 可以移除之前添加的事件监听器。

复制代码
let button = document.querySelector('button');
function handleClick() {
    alert('Button clicked');
}
button.addEventListener('click', handleClick);
// 移除事件监听器
button.removeEventListener('click', handleClick);

只有传入相同的 listener 函数,才能移除事件监听器。

3.3 事件对象

每个事件触发时,都会传递一个事件对象作为参数到事件处理函数中。事件对象包含有关事件的详细信息,比如触发事件的元素、鼠标位置、键盘按键等。

复制代码
document.getElementById('button').addEventListener('click', function(event) {
    console.log(event.target);  // 触发事件的元素
    console.log(event.type);    // 事件类型('click')
});

事件对象常见的属性:

  • event.target:触发事件的目标元素。

  • event.type:事件的类型。

  • event.clientXevent.clientY:鼠标点击的坐标。

  • event.key:按下的键(键盘事件)。

4. 事件委托(Event Delegation)

事件委托是一种通过将事件监听器绑定到父元素,而不是每个子元素上,来优化事件处理的技术。通过事件的冒泡机制,父元素可以捕获到其子元素的事件。

复制代码
document.getElementById('parent').addEventListener('click', function(event) {
    if (event.target && event.target.matches('li')) {
        alert('List item clicked: ' + event.target.textContent);
    }
});

事件委托的优势:

  • 减少内存消耗,避免为每个子元素绑定事件。

  • 动态添加的新元素也可以触发事件。

5. 事件的默认行为和 preventDefault

某些事件会有默认行为,例如点击链接会跳转,提交表单会刷新页面。你可以使用 event.preventDefault() 来阻止事件的默认行为。

复制代码
document.getElementById('form').addEventListener('submit', function(event) {
    event.preventDefault();  // 阻止表单提交
    console.log('Form submission prevented');
});

event.preventDefault():阻止事件的默认行为(如链接跳转、表单提交等)。

6. 事件的传播控制

6.1 stopPropagationstopImmediatePropagation
  • event.stopPropagation():停止事件的传播,事件不会再冒泡到父元素。

    document.getElementById('child').addEventListener('click', function(event) {
    event.stopPropagation(); // 阻止冒泡
    alert('Child clicked');
    });

event.stopImmediatePropagation():除了停止事件传播外,还会阻止当前事件处理程序后续的执行。

复制代码
document.getElementById('child').addEventListener('click', function(event) {
    event.stopImmediatePropagation();
    alert('Child clicked');
});
6.2 once 选项

onceaddEventListener 的一个选项,用于指定事件监听器是否只执行一次,执行后自动移除。

复制代码
document.getElementById('button').addEventListener('click', function() {
    alert('Button clicked');
}, { once: true });

once 设置为 true 时,事件处理程序在第一次触发后会自动移除。

7. 常见的事件处理难点

7.1 内存泄漏
  • 未清除的事件监听器和定时器会导致内存泄漏,特别是当 DOM 元素被删除或不再需要时。

    解决方案:

    • 使用 removeEventListener 移除不再需要的事件监听器。

    • 使用 clearIntervalclearTimeout 清除定时器。

7.2 事件冒泡与捕获的控制
  • 默认情况下,事件从目标元素向父元素冒泡,可能会导致不期望的行为。

    解决方案:

    • 使用 stopPropagation() 控制事件的传播。

    • 使用 event.preventDefault() 阻止默认行为。

7.3 动态元素的事件绑定
  • 动态添加到 DOM 中的元素可能不会触发事件,因为它们在事件监听器绑定时并未存在。

    解决方案:

    • 使用事件委托,确保父元素捕获子元素的事件。
7.4 性能优化
  • 对于大量的 DOM 元素或事件监听器,使用事件委托可以显著减少内存使用和提升性能。

二、防抖与节流

防抖(Debouncing)和节流(Throttling)是优化高频事件(如滚动、调整窗口大小、键盘输入等)性能的两种常用技术。在高频触发的事件中,如果每次事件触发都执行相关操作,会导致性能问题。防抖和节流的目的是减少不必要的事件处理,提高页面的响应性能。

1. 防抖(Debouncing)

**防抖(Debouncing)**的目的是确保事件在某段时间内只执行一次。即:事件触发后,延迟执行,如果在延迟时间内事件再次触发,则重新计算延迟时间,直到事件不再触发。

防抖的常见应用场景:

  • 搜索框输入:在用户输入时,不要每个字符都发送请求,而是等用户停止输入一段时间后再发送请求。

  • 窗口调整大小:调整窗口大小时,不要每次都触发重绘,而是等到调整结束后再进行一次操作。

防抖的实现原理:

防抖的核心思想是:如果事件在一段时间内不断触发,就取消上一次的操作,重新等待新的时间段。只有在事件停止触发后,才会执行操作。

示例:防抖函数的实现

复制代码
function debounce(func, wait) {
    let timeout;
    return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            func.apply(this, args);
        }, wait);
    };
}

// 使用防抖函数
const searchInput = document.getElementById('search');
const handleSearch = debounce(function() {
    console.log('Searching...');
}, 500);

searchInput.addEventListener('input', handleSearch);
  • 每次用户输入时,handleSearch 会被调用,但它会重新计算延迟时间(500ms),如果在 500 毫秒内没有再输入,才会执行 func

  • 如果用户继续输入,clearTimeout(timeout) 会清除上一次的定时器,重新设置一个新的定时器。

应用场景:
  • 表单验证:当用户输入时,等输入完成后再执行表单验证,而不是每输入一个字符就执行验证。

  • 窗口大小调整:避免在每次调整窗口大小时都执行昂贵的重绘操作。

2. 节流(Throttling)

**节流(Throttling)**的目的是确保事件在一定时间内最多只执行一次。与防抖不同,节流不会等待事件停止触发,而是以固定的时间间隔执行事件处理程序。

节流的常见应用场景:

  • 滚动事件:监听滚动事件时,不要每次滚动都触发操作,而是每隔一定时间处理一次滚动。

  • 窗口调整大小:节流可以用来限制在窗口大小调整时触发的重绘次数。

节流的实现原理:

节流的核心思想是:无论事件触发的频率如何,都以一定时间间隔执行操作。这样可以限制事件处理程序的调用频率。

示例:节流函数的实现

复制代码
function throttle(func, wait) {
    let lastTime = 0;
    return function(...args) {
        const now = new Date().getTime();
        if (now - lastTime >= wait) {
            func.apply(this, args);
            lastTime = now;
        }
    };
}

// 使用节流函数
const handleScroll = throttle(function() {
    console.log('Scrolling...');
}, 1000);

window.addEventListener('scroll', handleScroll);
  • 在此实现中,每次触发 scroll 事件时,首先会检查上次事件处理的时间(lastTime),如果距离上次事件处理超过了 wait 时间,则执行 func,并更新 lastTime

  • 如果在 wait 时间内再次触发事件,则不会执行操作,直到下一次间隔。

应用场景:
  • 滚动事件:当用户滚动页面时,可以每 1000 毫秒处理一次滚动事件,避免每次滚动都触发操作,减少性能开销。

  • 按钮点击:可以控制按钮在一定时间内只能被点击一次,避免频繁提交相同请求。

3. 防抖与节流的比较

特性 防抖(Debouncing) 节流(Throttling)
执行时机 等待一段时间后触发执行,只有最后一次触发才会执行 每隔一定时间执行一次,限制事件的频率
应用场景 搜索框、表单验证、窗口调整等操作 滚动事件、窗口大小调整、按钮点击等
执行频率 只有在事件停止触发后才执行一次 在固定时间间隔内执行一次
实现方式 清除定时器,重新设置定时器 记录时间戳,检查是否达到设定的间隔时间

4. 总结:防抖与节流的选择

  • 防抖适用于:用户操作频繁但希望等待操作停止后再执行(如输入框搜索、窗口调整等)。

  • 节流适用于:需要定期执行某些操作,但不希望过于频繁执行(如滚动事件、页面元素显示、按钮点击等)。

5. 实践建议

  • 对于 频繁触发的事件(如滚动、窗口调整、键盘输入等),可以通过防抖和节流来优化性能,避免无意义的多次执行。

  • 防抖更适合事件执行不频繁的情况,比如等待输入完成后再执行操作。

  • 节流更适合事件执行频繁的情况,比如用户滚动页面时限制处理次数。

三、性能优化

JavaScript 性能优化是开发高效、快速应用的关键,特别是在涉及大型应用、复杂逻辑或处理大量数据时。优化 JavaScript 性能不仅能提高页面加载速度,还能改善用户体验,避免性能瓶颈。性能优化通常包括代码层面的优化、内存管理、网络优化等多个方面。

以下是一些常见的 JavaScript 性能优化策略:

1. 减少 DOM 操作

DOM 操作是浏览器渲染页面的瓶颈之一。每次访问 DOM 或修改它,都会导致页面重新渲染,这会消耗大量时间。

优化方法:
  • 批量更新 DOM:一次性进行多个 DOM 更新,减少频繁的 DOM 操作。

    例如,使用 documentFragment 来将多个 DOM 元素添加到页面中:

    const fragment = document.createDocumentFragment();
    const ul = document.getElementById('list');

    for (let i = 0; i < 1000; i++) {
    const li = document.createElement('li');
    li.textContent = Item ${i};
    fragment.appendChild(li);
    }

    ul.appendChild(fragment); // 一次性将所有元素添加到 DOM 中

最小化 Reflow 和 RepaintReflowRepaint 是浏览器渲染的两个昂贵操作。在修改元素布局、样式或位置时,避免引起不必要的重排和重绘。

尽量将 CSS 修改合并为一次修改,避免在样式改变时逐步进行 DOM 操作。

2. 使用事件委托

事件委托是一种通过将事件监听器绑定到父元素,而不是每个子元素上,来优化事件处理的方法。这有助于减少内存消耗和提高性能,尤其是当 DOM 中有大量元素时。

复制代码
// 不使用事件委托,每个 li 元素都绑定一个事件处理器
document.querySelectorAll('li').forEach(item => {
    item.addEventListener('click', () => {
        console.log(item.textContent);
    });
});

// 使用事件委托,将事件监听器绑定到父元素上
document.getElementById('parent').addEventListener('click', (event) => {
    if (event.target && event.target.matches('li')) {
        console.log(event.target.textContent);
    }
});

3. 减少 JavaScript 代码的执行时间

优化方法:
  • 避免使用大量的循环:循环是 JavaScript 中最常见的性能瓶颈。使用优化的算法来减少不必要的循环次数。

    例如,使用 for 循环而不是 forEach,因为 forEach 会创建闭包,这会增加开销。

    // 优化前
    array.forEach(item => {
    console.log(item);
    });

    // 优化后
    for (let i = 0; i < array.length; i++) {
    console.log(array[i]);
    }

优化算法 :选择最优的算法和数据结构。例如,使用 MapSet 替代普通对象来提高查找效率,特别是当数据量较大时。

4. 懒加载和延迟加载

懒加载是指只有在需要时才加载资源,而不是在页面加载时一次性加载所有资源。这对于提高页面加载速度非常有用。

优化方法:
  • 图片懒加载 :使用 IntersectionObserver API 实现图片懒加载,只有当图片进入视口时才加载它们。

    const images = document.querySelectorAll('img[data-src]');
    const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
    if (entry.isIntersecting) {
    const img = entry.target;
    img.src = img.dataset.src; // 设置真实的图片 URL
    observer.unobserve(img);
    }
    });
    }, { threshold: 0.1 });

    images.forEach(image => {
    observer.observe(image);
    });

异步加载 JavaScript 和 CSS :对于不影响首屏渲染的 JavaScript 和 CSS,可以使用 asyncdefer 属性异步加载。

复制代码
<script src="script.js" async></script>  <!-- 异步加载 -->
<script src="script.js" defer></script>  <!-- 延迟加载 -->

5. 减少 HTTP 请求和资源大小

优化方法:
  • 合并文件:将多个 JavaScript 和 CSS 文件合并为一个文件,减少 HTTP 请求的次数。

  • 使用内容分发网络(CDN):将资源托管在 CDN 上,利用其地理分布的优势,提高资源加载速度。

  • 资源压缩 :使用 gzipBrotli 压缩技术减少资源文件的大小。

    使用工具如 Webpack 来压缩和优化 JavaScript 和 CSS 文件。

    // 使用 Webpack 压缩 JavaScript 文件
    module.exports = {
    mode: 'production',
    optimization: {
    minimize: true
    }
    };

懒加载和按需加载资源:将资源拆分为多个小文件,使用按需加载的技术,只加载页面当前需要的资源。

6. 优化 DOM 查询和操作

在 DOM 操作时,如果多次查询 DOM,会造成性能问题。减少对 DOM 的查询和操作次数,避免每次都重新查找 DOM 元素。

优化方法:
  • 缓存 DOM 查询结果:避免在循环中多次查询相同的 DOM 元素。

    复制代码
    // 优化前:每次都查询 DOM
    for (let i = 0; i < 1000; i++) {
        document.querySelector('.item').style.color = 'red';
    }
    
    // 优化后:缓存查询结果
    const item = document.querySelector('.item');
    for (let i = 0; i < 1000; i++) {
        item.style.color = 'red';
    }

    减少对 innerHTML 的操作innerHTML 操作会导致浏览器重新解析 HTML,增加性能开销。尽量使用 textContentappendChild 等方法来操作 DOM。

7. 避免内存泄漏

内存泄漏是指程序中不再使用的对象没有被及时回收,导致内存占用不断增加,影响性能。

优化方法:
  • 清理定时器和事件监听器 :如果使用了 setIntervalsetTimeout,在不再需要时清除它们。

  • 移除事件监听器:当 DOM 元素不再需要时,移除它们的事件监听器,避免内存泄漏。

8. Web Workers 和多线程

Web Workers 提供了一种在后台线程中运行 JavaScript 的方式,从而避免阻塞主线程的执行,适用于需要大量计算的操作。

优化方法:
  • Web Worker:将 CPU 密集型任务移至后台线程,避免阻塞主线程。

    const worker = new Worker('worker.js');
    worker.postMessage('start'); // 向 Worker 发送消息
    worker.onmessage = function(event) {
    console.log(event.data); // 接收 Worker 返回的数据
    };

9. 使用 requestAnimationFrame 实现平滑动画

requestAnimationFrame 用于优化动画效果,它会在浏览器重绘前执行回调函数。使用 requestAnimationFrame 而不是 setTimeoutsetInterval 来创建动画,可以确保动画平滑并减少性能消耗。

复制代码
function animate() {
    // 动画逻辑
    console.log('Animating...');
    requestAnimationFrame(animate);
}

requestAnimationFrame(animate);
相关推荐
IT瘾君1 小时前
JavaWeb:Html&Css
前端·html
264玫瑰资源库1 小时前
问道数码兽 怀旧剧情回合手游源码搭建教程(反查重优化版)
java·开发语言·前端·游戏
喝拿铁写前端1 小时前
从圣经Babel到现代编译器:没开玩笑,普通程序员也能写出自己的编译器!
前端·架构·前端框架
HED2 小时前
VUE项目发版后用户访问的仍然是旧页面?原因和解决方案都在这啦!
前端·vue.js
拉不动的猪2 小时前
前端自做埋点,我们应该要注意的几个问题
前端·javascript·面试
王景程2 小时前
如何测试短信接口
java·服务器·前端
安冬的码畜日常2 小时前
【AI 加持下的 Python 编程实战 2_10】DIY 拓展:从扫雷小游戏开发再探问题分解与 AI 代码调试能力(中)
开发语言·前端·人工智能·ai·扫雷游戏·ai辅助编程·辅助编程
烛阴2 小时前
Node.js中必备的中间件大全:提升性能、安全与开发效率的秘密武器
javascript·后端·express
清风细雨_林木木3 小时前
Vue开发网站会有“#”原因是前端路由使用了 Hash 模式
前端·vue.js·哈希算法