JavaScript事件处理是前端开发的核心内容之一,它使网页能够响应用户交互。本文将全面介绍JavaScript中的各种事件类型、优化技巧,并通过实际案例演示如何高效处理事件。
鼠标事件是用户通过鼠标与页面交互时触发的事件:
// 获取DOM元素
const box = document.getElementById('myBox');
// 1. click - 单击事件(左键)
box.addEventListener('click', function(e) {
console.log('单击事件', e.clientX, e.clientY); // 输出点击位置坐标
});
// 2. dblclick - 双击事件
box.addEventListener('dblclick', function() {
console.log('双击事件');
});
// 3. mousedown - 鼠标按下
box.addEventListener('mousedown', function(e) {
console.log('鼠标按下', e.button); // 0:左键, 1:中键, 2:右键
});
// 4. mouseup - 鼠标释放
box.addEventListener('mouseup', function() {
console.log('鼠标释放');
});
// 5. mousemove - 鼠标移动
box.addEventListener('mousemove', function(e) {
console.log('鼠标移动', e.movementX, e.movementY); // 相对于上次位置的偏移
});
// 6. mouseover/mouseout - 鼠标进入/离开元素(会冒泡)
box.addEventListener('mouseover', function() {
console.log('鼠标进入元素(会冒泡)');
});
// 7. mouseenter/mouseleave - 鼠标进入/离开元素(不冒泡)
box.addEventListener('mouseenter', function() {
console.log('鼠标进入元素(不冒泡)');
});
// 8. contextmenu - 右键菜单
box.addEventListener('contextmenu', function(e) {
e.preventDefault(); // 阻止默认右键菜单
console.log('右键点击');
});
鼠标事件对象属性:
clientX/clientY
:相对于视口的坐标pageX/pageY
:相对于文档的坐标offsetX/offsetY
:相对于事件源元素的坐标screenX/screenY
:相对于屏幕的坐标button
:按下的鼠标按钮(0:左键,1:中键,2:右键)altKey/ctrlKey/shiftKey
:是否按下了Alt/Ctrl/Shift键触摸事件专为移动设备设计:
const touchBox = document.getElementById('touchBox');
// 1. touchstart - 手指触摸屏幕
touchBox.addEventListener('touchstart', function(e) {
const touch = e.touches[0]; // 获取第一个触摸点
console.log('触摸开始', touch.clientX, touch.clientY);
e.preventDefault(); // 阻止默认行为(如滚动)
}, {passive: false});
// 2. touchmove - 手指在屏幕上移动
touchBox.addEventListener('touchmove', function(e) {
const touch = e.changedTouches[0]; // 获取变化的触摸点
console.log('触摸移动', touch.clientX, touch.clientY);
});
// 3. touchend - 手指离开屏幕
touchBox.addEventListener('touchend', function(e) {
console.log('触摸结束');
});
// 4. touchcancel - 触摸被意外中断(如来电)
touchBox.addEventListener('touchcancel', function() {
console.log('触摸中断');
});
触摸事件对象属性:
touches
:当前屏幕上所有触摸点的列表targetTouches
:当前元素上触摸点的列表changedTouches
:引发当前事件的触摸点列表clientX/clientY
, pageX/pageY
, identifier
等属性键盘事件响应用户的键盘操作:
// 监听整个文档的键盘事件
document.addEventListener('keydown', function(e) {
console.log('按键按下', e.key, e.code, e.keyCode);
// 常用键码判断
switch(e.key) {
case 'ArrowUp':
console.log('上箭头');
break;
case 'ArrowDown':
console.log('下箭头');
break;
case 'Enter':
console.log('回车键');
break;
case 'Escape':
console.log('ESC键');
break;
}
});
document.addEventListener('keyup', function(e) {
console.log('按键释放', e.key);
});
document.addEventListener('keypress', function(e) {
// 只响应字符键,不响应功能键
console.log('按键按下并释放', e.key);
});
键盘事件对象属性:
key
:按键的字符串表示(考虑键盘布局)code
:物理按键代码(不考虑键盘布局)keyCode/which
:已废弃的键码(建议使用key或code)altKey/ctrlKey/shiftKey
:是否按下了Alt/Ctrl/Shift键焦点事件在元素获得或失去焦点时触发:
const input = document.getElementById('myInput');
// 1. focus - 获得焦点
input.addEventListener('focus', function() {
console.log('输入框获得焦点');
this.style.backgroundColor = '#ffe';
});
// 2. blur - 失去焦点
input.addEventListener('blur', function() {
console.log('输入框失去焦点');
this.style.backgroundColor = '';
});
// 3. focusin - 获得焦点(会冒泡)
input.parentElement.addEventListener('focusin', function() {
console.log('子元素获得焦点(冒泡)');
});
// 4. focusout - 失去焦点(会冒泡)
input.parentElement.addEventListener('focusout', function() {
console.log('子元素失去焦点(冒泡)');
});
页面事件与页面生命周期相关:
// 1. load - 页面所有资源加载完成
window.addEventListener('load', function() {
console.log('页面完全加载完成');
});
// 2. DOMContentLoaded - DOM树构建完成
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM树构建完成');
});
// 3. beforeunload - 页面即将卸载
window.addEventListener('beforeunload', function(e) {
e.preventDefault();
e.returnValue = ''; // 某些浏览器需要这个来显示确认对话框
return '您有未保存的更改,确定要离开吗?';
});
// 4. unload - 页面正在卸载
window.addEventListener('unload', function() {
// 只能做轻量级操作,不能阻止页面关闭
console.log('页面正在卸载');
});
// 5. resize - 窗口大小改变
window.addEventListener('resize', function() {
console.log('窗口大小改变', window.innerWidth, window.innerHeight);
});
// 6. scroll - 滚动事件
window.addEventListener('scroll', function() {
console.log('滚动位置', window.scrollY);
});
// 7. visibilitychange - 页面可见性变化
document.addEventListener('visibilitychange', function() {
console.log('页面可见性:', document.visibilityState);
if(document.visibilityState === 'visible') {
console.log('页面变为可见');
} else {
console.log('页面变为隐藏');
}
});
HTML5引入了一些新的事件类型:
// 1. input事件 - 输入值变化时实时触发
const emailInput = document.getElementById('email');
emailInput.addEventListener('input', function() {
console.log('输入值变化:', this.value);
});
// 2. invalid事件 - 表单验证失败
emailInput.addEventListener('invalid', function(e) {
console.log('验证失败');
if(!this.validity.valid) {
if(this.validity.valueMissing) {
this.setCustomValidity('请输入邮箱地址');
} else if(this.validity.typeMismatch) {
this.setCustomValidity('请输入有效的邮箱地址');
}
}
});
// 3. drag & drop事件 - 拖放API
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
this.classList.add('dragover');
});
dropZone.addEventListener('dragleave', function() {
this.classList.remove('dragover');
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('dragover');
const files = e.dataTransfer.files;
console.log('拖放了文件:', files);
});
// 4. transitionend - CSS过渡结束
const animBox = document.getElementById('animBox');
animBox.addEventListener('transitionend', function() {
console.log('过渡动画结束');
});
// 5. animationend - CSS动画结束
animBox.addEventListener('animationend', function() {
console.log('动画结束');
});
事件委托利用事件冒泡机制,将子元素的事件处理委托给父元素:
// 传统方式 - 为每个li添加点击事件
/*
const listItems = document.querySelectorAll('#list li');
listItems.forEach(item => {
item.addEventListener('click', function() {
console.log('点击了:', this.textContent);
});
});
*/
// 事件委托方式 - 只需一个事件监听器
const list = document.getElementById('list');
list.addEventListener('click', function(e) {
// 检查点击的是否是li元素
if(e.target.tagName === 'LI') {
console.log('点击了:', e.target.textContent);
// 高亮被点击的项
const activeItem = this.querySelector('.active');
if(activeItem) activeItem.classList.remove('active');
e.target.classList.add('active');
}
// 如果列表项中有按钮等其他元素,可以使用closest方法
const deleteBtn = e.target.closest('.delete-btn');
if(deleteBtn) {
e.preventDefault();
const listItem = deleteBtn.closest('li');
console.log('删除:', listItem.textContent);
listItem.remove();
}
});
// 动态添加列表项
document.getElementById('addBtn').addEventListener('click', function() {
const newItem = document.createElement('li');
newItem.textContent = '新项目 ' + (list.children.length + 1);
newItem.innerHTML += ' ';
list.appendChild(newItem);
});
事件委托优点:
正确删除事件监听器防止内存泄漏:
// 添加事件监听器
function handleClick() {
console.log('按钮被点击');
}
const btn = document.getElementById('myBtn');
btn.addEventListener('click', handleClick);
// 错误的删除方式(无效)
// btn.onclick = null;
// 正确的删除方式
btn.removeEventListener('click', handleClick);
// 一次性事件
function handleOnce() {
console.log('这个事件只会触发一次');
btn.removeEventListener('click', handleOnce);
}
btn.addEventListener('click', handleOnce);
// 使用AbortController (现代浏览器)
const controller = new AbortController();
btn.addEventListener('click', function() {
console.log('使用AbortController添加的事件');
}, { signal: controller.signal });
// 移除所有通过该controller添加的事件
controller.abort();
// 1. 防抖(Debounce) - 事件频繁触发时,只执行最后一次
function debounce(func, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
window.addEventListener('resize', debounce(function() {
console.log('调整窗口大小(防抖处理)');
}, 300));
// 2. 节流(Throttle) - 事件频繁触发时,按固定频率执行
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function(...args) {
const context = this;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
window.addEventListener('scroll', throttle(function() {
console.log('滚动事件(节流处理)', window.scrollY);
}, 200));
// 3. 被动事件监听器(提高滚动性能)
document.addEventListener('wheel', function(e) {
console.log('滚轮事件');
}, { passive: true }); // 告诉浏览器不会调用preventDefault()
// 4. 使用事件委托处理大量元素
document.getElementById('bigList').addEventListener('click', function(e) {
const item = e.target.closest('.list-item');
if(item) {
console.log('点击了大列表中的项目:', item.dataset.id);
}
});
<div class="image-zoom-container">
<img src="example.jpg" alt="示例图片" class="zoom-image">
<div class="zoom-controls">
<button class="zoom-in">放大button>
<button class="zoom-out">缩小button>
<button class="zoom-reset">重置button>
div>
div>
class ImageZoom {
constructor(containerSelector) {
this.container = document.querySelector(containerSelector);
this.image = this.container.querySelector('.zoom-image');
this.zoomLevel = 1;
this.maxZoom = 4;
this.minZoom = 0.5;
this.isDragging = false;
this.lastPosition = { x: 0, y: 0 };
this.init();
}
init() {
// 初始化变换
this.updateTransform();
// 绑定按钮事件
this.container.querySelector('.zoom-in').addEventListener('click', () => this.zoom(1.2));
this.container.querySelector('.zoom-out').addEventListener('click', () => this.zoom(0.8));
this.container.querySelector('.zoom-reset').addEventListener('click', () => this.reset());
// 鼠标滚轮缩放
this.container.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
this.zoom(delta, { x: e.offsetX, y: e.offsetY });
}, { passive: false });
// 拖动功能
this.image.addEventListener('mousedown', (e) => {
if(this.zoomLevel > 1) {
this.isDragging = true;
this.lastPosition = { x: e.clientX, y: e.clientY };
this.image.style.cursor = 'grabbing';
}
});
document.addEventListener('mousemove', (e) => {
if(this.isDragging) {
const dx = e.clientX - this.lastPosition.x;
const dy = e.clientY - this.lastPosition.y;
this.position.x += dx;
this.position.y += dy;
this.lastPosition = { x: e.clientX, y: e.clientY };
this.updateTransform();
}
});
document.addEventListener('mouseup', () => {
this.isDragging = false;
this.image.style.cursor = this.zoomLevel > 1 ? 'grab' : 'default';
});
}
zoom(factor, center = null) {
const oldZoom = this.zoomLevel;
this.zoomLevel *= factor;
// 限制缩放范围
this.zoomLevel = Math.min(Math.max(this.zoomLevel, this.minZoom), this.maxZoom);
if(center) {
// 基于鼠标位置的缩放
const rect = this.image.getBoundingClientRect();
const offsetX = (center.x - rect.left) / oldZoom;
const offsetY = (center.y - rect.top) / oldZoom;
this.position.x -= offsetX * (this.zoomLevel - oldZoom);
this.position.y -= offsetY * (this.zoomLevel - oldZoom);
}
this.updateTransform();
this.image.style.cursor = this.zoomLevel > 1 ? 'grab' : 'default';
}
reset() {
this.zoomLevel = 1;
this.position = { x: 0, y: 0 };
this.updateTransform();
this.image.style.cursor = 'default';
}
updateTransform() {
this.image.style.transform = `translate(${this.position.x}px, ${this.position.y}px) scale(${this.zoomLevel})`;
}
}
// 使用示例
new ImageZoom('.image-zoom-container');
<ul id="optimizedList" class="list">
<li class="list-item" data-id="1">项目1 <button class="delete-btn">删除button>li>
<li class="list-item" data-id="2">项目2 <button class="delete-btn">删除button>li>
<li class="list-item" data-id="3">项目3 <button class="delete-btn">删除button>li>
ul>
<button id="addListItem">添加项目button>
class OptimizedList {
constructor(listSelector) {
this.list = document.querySelector(listSelector);
this.itemCount = this.list.children.length;
// 使用事件委托处理列表项点击
this.list.addEventListener('click', this.handleListClick.bind(this));
// 添加新项目按钮
document.getElementById('addListItem').addEventListener('click', () => {
this.addItem(`新项目 ${++this.itemCount}`);
});
}
handleListClick(e) {
// 检查点击的是否是列表项
const listItem = e.target.closest('.list-item');
if(listItem) {
// 高亮处理
const currentActive = this.list.querySelector('.active');
if(currentActive) currentActive.classList.remove('active');
listItem.classList.add('active');
console.log('选择了项目:', listItem.dataset.id);
}
// 检查点击的是否是删除按钮
const deleteBtn = e.target.closest('.delete-btn');
if(deleteBtn) {
e.stopPropagation(); // 阻止事件冒泡到列表项
const item = deleteBtn.closest('.list-item');
console.log('删除项目:', item.dataset.id);
item.remove();
}
}
addItem(text) {
const newItem = document.createElement('li');
newItem.className = 'list-item';
newItem.dataset.id = Date.now(); // 使用时间戳作为唯一ID
newItem.innerHTML = `${text} `;
// 添加动画效果
newItem.style.opacity = '0';
this.list.appendChild(newItem);
// 触发CSS过渡
setTimeout(() => {
newItem.style.opacity = '1';
}, 10);
}
}
// 使用示例
new OptimizedList('#optimizedList');
<div class="lazy-load-container">
<img data-src="image1.jpg" alt="图片1" class="lazy-image">
<img data-src="image2.jpg" alt="图片2" class="lazy-image">
<img data-src="image3.jpg" alt="图片3" class="lazy-image">
div>
class LazyImageLoader {
constructor(options = {}) {
// 默认配置
const defaults = {
container: document,
imageSelector: '.lazy-image',
threshold: 0,
rootMargin: '0px',
placeholder: 'data:image/png;base64,...', // 占位图
onLoad: null,
onError: null
};
this.config = { ...defaults, ...options };
this.images = [];
this.observer = null;
this.init();
}
init() {
// 获取所有待加载图片
this.images = Array.from(this.config.container.querySelectorAll(this.config.imageSelector));
if('IntersectionObserver' in window) {
// 使用IntersectionObserver API(现代浏览器)
this.initObserver();
} else {
// 降级方案(旧浏览器)
this.initFallback();
}
}
initObserver() {
this.observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if(entry.isIntersecting) {
const image = entry.target;
this.loadImage(image);
observer.unobserve(image);
}
});
}, {
root: null,
rootMargin: this.config.rootMargin,
threshold: this.config.threshold
});
// 开始观察所有图片
this.images.forEach(image => {
// 设置占位图
if(this.config.placeholder && !image.src) {
image.src = this.config.placeholder;
}
this.observer.observe(image);
});
}
initFallback() {
// 旧浏览器降级方案 - 滚动事件监听
const checkImages = throttle(() => {
this.images.forEach(image => {
if(this.isInViewport(image)) {
this.loadImage(image);
}
});
// 移除已加载的图片
this.images = this.images.filter(img => !img.dataset.loaded);
// 所有图片加载完成后移除事件监听
if(this.images.length === 0) {
window.removeEventListener('scroll', checkImages);
window.removeEventListener('resize', checkImages);
}
}, 200);
// 初始检查
checkImages();
// 监听滚动和调整大小事件
window.addEventListener('scroll', checkImages);
window.addEventListener('resize', checkImages);
}
isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top <= (window.innerHeight || document.documentElement.clientHeight) + this.config.threshold &&
rect.bottom >= 0 - this.config.threshold &&
rect.left <= (window.innerWidth || document.documentElement.clientWidth) + this.config.threshold &&
rect.right >= 0 - this.config.threshold
);
}
loadImage(image) {
if(image.dataset.loaded) return;
// 标记为已加载
image.dataset.loaded = 'true';
// 创建新的Image对象预加载
const loader = new Image();
loader.onload = () => {
// 图片加载成功
image.src = image.dataset.src;
image.classList.add('loaded');
// 移除data-src属性
image.removeAttribute('data-src');
// 触发回调
if(typeof this.config.onLoad === 'function') {
this.config.onLoad(image);
}
};
loader.onerror = () => {
// 图片加载失败
console.error('图片加载失败:', image.dataset.src);
// 触发回调
if(typeof this.config.onError === 'function') {
this.config.onError(image);
}
};
// 开始加载
loader.src = image.dataset.src;
}
// 添加新图片到懒加载系统
addImages(images) {
const newImages = Array.isArray(images) ? images : [images];
newImages.forEach(image => {
if(!this.images.includes(image)) {
this.images.push(image);
if(this.observer) {
this.observer.observe(image);
}
}
});
}
}
// 辅助函数 - 节流
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if(!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 使用示例
document.addEventListener('DOMContentLoaded', () => {
const lazyLoader = new LazyImageLoader({
imageSelector: '.lazy-image',
threshold: 0.1,
rootMargin: '100px',
placeholder: 'placeholder.jpg',
onLoad: (image) => {
console.log('图片加载完成:', image.src);
},
onError: (image) => {
console.error('图片加载失败:', image.dataset.src);
image.src = 'error.jpg';
}
});
// 动态添加新图片
document.getElementById('loadMoreBtn').addEventListener('click', () => {
const newImage = document.createElement('img');
newImage.className = 'lazy-image';
newImage.dataset.src = 'new-image.jpg';
document.querySelector('.lazy-load-container').appendChild(newImage);
lazyLoader.addImages(newImage);
});
});
命名规范:
handle
前缀,如handleClick
is
或has
前缀,如isLoading
性能优化:
// 不好 - 匿名函数无法被移除
element.addEventListener('click', function() { ... });
// 好 - 使用命名函数
function handleClick() { ... }
element.addEventListener('click', handleClick);
element.removeEventListener('click', handleClick);
内存管理:
// 组件销毁时移除事件监听
class MyComponent {
constructor() {
this.handleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.handleResize);
}
handleResize() { ... }
destroy() {
window.removeEventListener('resize', this.handleResize);
}
}
代码组织:
// 使用事件委托管理相关事件
class EventManager {
constructor() {
this.handlers = {
click: this.handleClick.bind(this),
mouseover: this.handleMouseOver.bind(this)
};
document.body.addEventListener('click', this.handlers.click);
document.body.addEventListener('mouseover', this.handlers.mouseover);
}
handleClick(e) { ... }
handleMouseOver(e) { ... }
destroy() {
document.body.removeEventListener('click', this.handlers.click);
document.body.removeEventListener('mouseover', this.handlers.mouseover);
}
}
移动端300ms点击延迟:
<meta name="viewport" content="width=device-width, initial-scale=1.0">
// 使用fastclick库或touch事件模拟
document.addEventListener('DOMContentLoaded', function() {
if('ontouchstart' in window) {
FastClick.attach(document.body);
}
});
事件穿透问题:
// 当上层元素消失后,下层元素会意外触发点击
// 解决方案:在下层元素上设置pointer-events: none
document.getElementById('overlay').addEventListener('touchstart', function(e) {
// 处理逻辑...
// 防止穿透
e.preventDefault();
});
动态内容事件绑定:
// 使用事件委托或MutationObserver
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if(mutation.addedNodes.length) {
// 新元素添加,可以绑定事件
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
使用||=
操作符:
// 如果element.dataset.loaded为假值,则赋值为true
element.dataset.loaded ||= true;
参数解构:
// 事件处理函数中使用参数解构
element.addEventListener('click', ({ clientX, clientY }) => {
console.log(`点击位置: ${clientX}, ${clientY}`);
});
动态导入:
// 按需加载事件处理代码
button.addEventListener('click', async () => {
const module = await import('./heavyEventHandler.js');
module.handleClick();
});
使用Object.assign()
合并默认选项:
function setupEvents(options) {
const defaults = { capture: false, passive: true };
const config = Object.assign({}, defaults, options);
element.addEventListener('click', handler, config);
}
通过掌握这些事件处理技术和优化方法,您可以构建出响应迅速、内存高效且易于维护的交互式Web应用程序。记住根据实际需求选择合适的技术方案,并在性能与开发效率之间取得平衡。