事件✅
事件冒泡是什么?
答案
参考 w3c ui event 冒泡机制是为了解决在如何在 DOM 树上广播事件。整个广播流程参考下图:
整个事件从创建到完成的流程如下
- 事件产生,用户点击等操作,此时会创建一个
event target对象包含出发事件时的相关信息target指向触发事件的元素currentTarget只读属性指向事件分发过程中的当前对象- 在回调函数中
this指向 currentTarget.
- 根据当前触发元素确定传播路径
- 事件分发,此时时间会在 DOM 树上进行广播,广播分为三个阶段
- 捕获阶段 从传播路径的根元素向目标元素传播,从 window 开始
- 目标阶段 当传播到达事件目标时
- 冒泡阶段 当从事件目标对象向父元素广播时从 window 结束。
事件广播过程会修改
event target相关状态,此外可以利用stopPropagation对事件传播进行截断。
- 事件处理阶段,在事件分发过程中若传播路径有元素绑定了回调事件,
event target对象作为传入参数触发执行。
但目标元素触发事件时,事件的分发分为三个阶段:
- 捕获阶段(capture phase) 从根元素逐级向目标元素传递
- 目标阶段(target phase) 传递到目标元素的阶段
- 冒泡阶段(bubbling phase) 从目标元素重新传递到根元素的阶段
事件传播演示
事件委托的作用和意义?
答案
事件委托是一种常用的事件处理模式,主要通过将事件监听器添加到父元素上,而不是每个子元素上,从而提高性能和简化代码。其作用和意义包括:
- 提高性能:减少事件监听器的数量,降低内存消耗,尤其在子元素较多时更为明显。
- 简化代码:通过事件冒泡,父元素可以处理所有子元素的事件,避免为每个子元素单独绑定事件。
- 动态添加元素:对于动态生成的子元素,父元素的事件监听器可以自动响应,无需重新绑定事件。
实际示例
说明 load,ready,DOMContentLoaded 的区别
答案
| 属性 | DOMContentLoaded | load | ready |
|---|---|---|---|
| 触发时机 | DOM结构解析完成 | 页面所有资源加载完毕 | DOM结构解析完成 |
| 典型用途 | 尽早操作DOM、初始化交互 | 依赖全部资源的操作 | 早期DOM操作、兼容性处理 |
| 是否等待外部资源 | 否 | 是 | 否 |
| 适用范围 | 原生JS | 原生JS | jQuery专用 |
// 原生 DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
// DOM 可操作
})
// 原生 load
window.addEventListener('load', () => {
// 所有资源加载完毕
})
// jQuery ready
$(function () {
// DOM 可操作
})
推荐优先使用 DOMContentLoaded 或 jQuery 的 ready,可提升页面响应速度和用户体验。
load 事件需等待所有资源加载,页面大图或慢资源会延迟触发,影响初始化时机。
延伸阅读
- MDN: DOMContentLoaded — 官方事件说明
- jQuery ready 文档 — jQuery 兼容性处理
- 资源加载和页面事件详解 — 详细流程与原理
stopImmediatePropagation 和 stopPropagation 区别
答案
stopPropagation 和 stopImmediatePropagation 都用于阻止事件冒泡,但作用范围不同:
|方法|作用范围|典型场景| |event.stopPropagation()|阻止事件继续向父元素冒泡,但同一元素上后续监听器仍会执行|只需阻止冒泡,不影响本元素其他事件处理| |event.stopImmediatePropagation()|阻止事件冒泡,并阻止当前元素上后续所有同类型事件监听器执行|彻底阻断冒泡和本元素后续处理|
const btn = document.getElementById('btn')
btn.addEventListener('click', () => { console.log('handler1') })
btn.addEventListener('click', (e) => {
e.stopImmediatePropagation()
console.log('handler2')
})
btn.addEventListener('click', () => { console.log('handler3') })
/*
点击 btn 时,只输出 handler2,handler1/handler3 都不会执行
*/
如果用 stopPropagation(),则 handler1、handler2 都会执行,handler3 不会因冒泡被阻止。
stopPropagation只影响冒泡,不影响同一元素的其他监听器。stopImmediatePropagation更彻底,常用于防止重复触发或冲突处理。
如需彻底阻止事件在当前元素的所有后续处理,优先用 stopImmediatePropagation。
延伸阅读
addEventListener 和 attribute onclick
答案
为什么 {capture:true} 不是先触发
参见此回答 Event listeners registered for capturing phase not triggered before bubbling - why?
mouseEnter、mouseLeave、mouseOver、mouseOut 有什么区别?
答案
下表总结了四个鼠标事件的主要区别:
| 事件名 | 触发时机 | 是否冒泡 | 子元素影响 | 典型用途 |
|---|---|---|---|---|
| mouseenter | 鼠标进入元素本身 | 否 | 否 | 只需关注本元素的进入 |
| mouseleave | 鼠标离开元素本身 | 否 | 否 | 只需关注本元素的离开 |
| mouseover | 鼠标进入元素或其子元素 | 是 | 是 | 需监控元素及子元素的进入 |
| mouseout | 鼠标离开元素或其子元素 | 是 | 是 | 需监控元素及子元素的离开 |
element.addEventListener('mouseenter', () => console.log('mouseenter'))
element.addEventListener('mouseleave', () => console.log('mouseleave'))
element.addEventListener('mouseover', () => console.log('mouseover'))
element.addEventListener('mouseout', () => console.log('mouseout'))
有嵌套子元素时,推荐用 mouseenter/mouseleave,可避免鼠标在父子元素间频繁触发事件。
mouseover/mouseout 适合需要对子元素进入/离开也做处理的场景,如复杂菜单。
延伸阅读
drag
答案
核心概念:
HTML5 Drag and Drop API 提供原生的拖拽功能,支持在浏览器内和不同应用间进行拖放操作。核心包括 draggable 属性和一系列拖拽事件(dragstart、drag、dragend、dragenter、dragover、dragleave、drop),通过 DataTransfer 对象传递数据。适用于文件上传、UI组件重排、数据传输等交互场景。
实际示例:
// 1. 基础拖拽实现
class SimpleDragDrop {
constructor () {
this.init()
}
init () {
this.setupDraggableElements()
this.setupDropZones()
}
setupDraggableElements () {
const draggables = document.querySelectorAll('[draggable="true"]')
draggables.forEach(element => {
element.addEventListener('dragstart', (e) => {
// 设置拖拽数据
e.dataTransfer.setData('text/plain', element.textContent)
e.dataTransfer.setData('text/html', element.outerHTML)
e.dataTransfer.setData('application/json', JSON.stringify({
id: element.id,
className: element.className,
content: element.textContent
}))
// 设置拖拽效果
e.dataTransfer.effectAllowed = 'copy'
// 设置拖拽时的视觉反馈
element.style.opacity = '0.5'
})
element.addEventListener('dragend', (e) => {
// 恢复元素样式
element.style.opacity = '1'
})
})
}
setupDropZones () {
const dropZones = document.querySelectorAll('.drop-zone')
dropZones.forEach(zone => {
zone.addEventListener('dragover', (e) => {
e.preventDefault() // 允许drop
e.dataTransfer.dropEffect = 'copy'
zone.classList.add('drag-over')
})
zone.addEventListener('dragleave', (e) => {
zone.classList.remove('drag-over')
})
zone.addEventListener('drop', (e) => {
e.preventDefault()
zone.classList.remove('drag-over')
// 获取拖拽数据
const text = e.dataTransfer.getData('text/plain')
const html = e.dataTransfer.getData('text/html')
const jsonData = e.dataTransfer.getData('application/json')
// 处理拖拽数据
this.handleDrop(zone, { text, html, json: jsonData })
})
})
}
handleDrop (zone, data) {
console.log('Drop data:', data)
zone.innerHTML += `<div class="dropped-item">${data.text}</div>`
}
}
// 2. 高级拖拽:可排序列表
class SortableList {
constructor (container) {
this.container = container
this.items = Array.from(container.children)
this.draggedElement = null
this.init()
}
init () {
this.items.forEach(item => {
item.draggable = true
item.addEventListener('dragstart', this.handleDragStart.bind(this))
item.addEventListener('dragover', this.handleDragOver.bind(this))
item.addEventListener('drop', this.handleDrop.bind(this))
item.addEventListener('dragend', this.handleDragEnd.bind(this))
})
}
handleDragStart (e) {
this.draggedElement = e.target
e.target.style.opacity = '0.5'
// 创建拖拽时的自定义图像
const dragImage = e.target.cloneNode(true)
dragImage.style.transform = 'rotate(5deg)'
dragImage.style.opacity = '0.8'
document.body.appendChild(dragImage)
e.dataTransfer.setDragImage(dragImage, 50, 50)
setTimeout(() => document.body.removeChild(dragImage), 0)
}
handleDragOver (e) {
e.preventDefault()
const afterElement = this.getDragAfterElement(e.clientY)
if (afterElement == null) {
this.container.appendChild(this.draggedElement)
} else {
this.container.insertBefore(this.draggedElement, afterElement)
}
}
handleDrop (e) {
e.preventDefault()
// 保存新的排序
this.saveOrder()
}
handleDragEnd (e) {
e.target.style.opacity = '1'
this.draggedElement = null
}
getDragAfterElement (y) {
const draggableElements = [...this.container.querySelectorAll('[draggable]:not([style*="opacity: 0.5"])')]
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect()
const offset = y - box.top - box.height / 2
if (offset < 0 && offset > closest.offset) {
return { offset, element: child }
} else {
return closest
}
}, { offset: Number.NEGATIVE_INFINITY }).element
}
saveOrder () {
const order = Array.from(this.container.children).map((item, index) => ({
id: item.id,
order: index
}))
console.log('New order:', order)
// 发送到服务器保存
fetch('/api/save-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order })
})
}
}
// 3. 文件拖拽上传
class FileDropUpload {
constructor (dropZone) {
this.dropZone = dropZone
this.fileList = []
this.init()
}
init () {
this.dropZone.addEventListener('dragover', this.handleDragOver.bind(this))
this.dropZone.addEventListener('dragleave', this.handleDragLeave.bind(this))
this.dropZone.addEventListener('drop', this.handleFileDrop.bind(this))
}
handleDragOver (e) {
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
this.dropZone.classList.add('drag-over')
// 检查是否包含文件
const hasFiles = Array.from(e.dataTransfer.types).includes('Files')
if (!hasFiles) {
e.dataTransfer.dropEffect = 'none'
}
}
handleDragLeave (e) {
// 只有当真正离开drop zone时才移除样式
if (!this.dropZone.contains(e.relatedTarget)) {
this.dropZone.classList.remove('drag-over')
}
}
handleFileDrop (e) {
e.preventDefault()
this.dropZone.classList.remove('drag-over')
const files = Array.from(e.dataTransfer.files)
this.processFiles(files)
}
processFiles (files) {
files.forEach(file => {
// 验证文件类型和大小
if (this.validateFile(file)) {
this.fileList.push(file)
this.displayFile(file)
this.uploadFile(file)
}
})
}
validateFile (file) {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']
const maxSize = 5 * 1024 * 1024 // 5MB
if (!allowedTypes.includes(file.type)) {
alert(`不支持的文件类型: ${file.type}`)
return false
}
if (file.size > maxSize) {
alert(`文件过大: ${(file.size / 1024 / 1024).toFixed(2)}MB,最大允许5MB`)
return false
}
return true
}
displayFile (file) {
const fileElement = document.createElement('div')
fileElement.className = 'file-item'
fileElement.innerHTML = `
<span class="file-name">${file.name}</span>
<span class="file-size">${(file.size / 1024).toFixed(2)} KB</span>
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
`
this.dropZone.appendChild(fileElement)
return fileElement
}
async uploadFile (file) {
const formData = new FormData()
formData.append('file', file)
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
})
if (response.ok) {
console.log(`文件 ${file.name} 上传成功`)
}
} catch (error) {
console.error(`文件 ${file.name} 上传失败:`, error)
}
}
}
// 使用示例
const simpleDragDrop = new SimpleDragDrop()
const sortableContainer = document.getElementById('sortable-list')
if (sortableContainer) {
new SortableList(sortableContainer)
}
const fileDropZone = document.getElementById('file-drop-zone')
if (fileDropZone) {
new FileDropUpload(fileDropZone)
}
面试官视角:
要点清单:
- 理解HTML5拖拽事件的完整生命周期
- 掌握DataTransfer对象的数据传输机制
- 了解不同拖拽效果和自定义拖拽图像
加分项:
- 提及拖拽的可访问性考虑
- 了解触摸设备上的拖拽实现差异
- 知道如何处理文件拖拽和安全限制
常见失误:
- 忘记在dragover事件中调用preventDefault()
- 不理解拖拽数据类型和格式要求
- 忽略拖拽过程中的视觉反馈和用户体验
延伸阅读:
- HTML Drag and Drop API - MDN — 完整API文档
- DataTransfer - MDN — 数据传输对象详解
- Drag and Drop accessibility — 无障碍拖拽指南
拖曳控制窗口
答案
核心概念 实现鼠标拖拽弹窗或悬浮元素,本质是监听并协调鼠标事件流:
mousedown(按下):在目标元素或标题栏按下时,记录当前鼠标指针相对元素左上角的偏移量(offsetX = e.clientX - rect.left),开启拖拽标志isDragging = true。mousemove(移动):在document(而非仅元素本身)上监听移动事件,当处于拖拽状态时,实时计算新位置:left = e.clientX - offsetX,并更新元素绝对/固定定位样式。mouseup(抬起):在document上监听鼠标释放,清理isDragging标志并解绑移动事件。
手写示例:
function makeDraggable (element, handle = element) {
let isDragging = false
let startX = 0
let startY = 0
let initialLeft = 0
let initialTop = 0
handle.style.cursor = 'move'
const onMouseDown = (e) => {
isDragging = true
startX = e.clientX
startY = e.clientY
const rect = element.getBoundingClientRect()
initialLeft = rect.left
initialTop = rect.top
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
e.preventDefault() // 防止选中页面文字
}
const onMouseMove = (e) => {
if (!isDragging) return
const dx = e.clientX - startX
const dy = e.clientY - startY
// 边界限制(保持在视口内)
const maxLeft = window.innerWidth - element.offsetWidth
const maxTop = window.innerHeight - element.offsetHeight
const nextLeft = Math.min(Math.max(0, initialLeft + dx), maxLeft)
const nextTop = Math.min(Math.max(0, initialTop + dy), maxTop)
element.style.left = `${nextLeft}px`
element.style.top = `${nextTop}px`
}
const onMouseUp = () => {
isDragging = false
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
handle.addEventListener('mousedown', onMouseDown)
return function destroy () {
handle.removeEventListener('mousedown', onMouseDown)
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
}
面试官视角
- 考查边界处理:为什么
mousemove和mouseup必须绑定到document而不是拖拽元素本身?(因为鼠标移动过快会滑出元素,导致丢失事件卡顿)。 - 加分项:考虑视口边缘限制(Boundary clamping)与文字选中阻止(
e.preventDefault())。
要实时统计用户浏览器窗口大小,该如何做
答案
核心概念 实时获取并响应浏览器视口尺寸变化,核心技术方案如下:
window.onresize/resize事件:当窗口宽高发生变化时触发,读取window.innerWidth和window.innerHeight。- 频率控制(节流):resize 事件高频连续触发(每秒可达几十次),若在回调中进行复杂 DOM 重排或接口计算,必须通过
throttle降低处理频率。 - 现代推荐方案(
ResizeObserver):若需精准监听具体 DOM 容器(非全局 window)的盒模型尺寸变化,推荐使用原生的ResizeObserverAPI。
手写示例:
function initWindowResizeTracker (callback, wait = 100) {
let timer = null
const handleResize = () => {
if (timer) return
timer = setTimeout(() => {
timer = null
const dimensions = {
width: window.innerWidth,
height: window.innerHeight,
screenWidth: window.screen.width,
screenHeight: window.screen.height
}
callback(dimensions)
}, wait)
}
window.addEventListener('resize', handleResize)
// 首次立即调用一次获取初始数据
callback({
width: window.innerWidth,
height: window.innerHeight
})
return () => window.removeEventListener('resize', handleResize)
}
// 使用
const stopTracking = initWindowResizeTracker((dim) => {
console.log('窗口尺寸变更:', dim)
})
面试官视角
- 考查事件性能开销控制与监听器的生命周期解绑(避免 SPA 单页应用中组件卸载后造成内存泄漏)。