事件✅
事件冒泡是什么?
答案
参考 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 — 无障碍拖拽指南
拖曳控制窗口
关键词:拖拽 api、mousedown
、mousemove
和mouseup
事件
实现鼠标拖拽功能通常涉及到监听和处理鼠标事件,比如:mousedown
、mousemove
和mouseup
事件。下面是一个基本的步骤指南以及一个简易的示例代码(使用 HTML 和 JavaScript),展示了如何实现一个元素的鼠标拖拽功能。
基本步骤
-
监听
mousedown
事件: 当用户按下鼠标按钮时,记录被拖拽元素的初始位置,并设置一个标志(如isDragging
)表示拖拽开始。 -
监听
mousemove
事件: 当用户移动鼠标时,如果拖拽已开始,则根据鼠标当前位置和初始位置的差值,更新被拖拽元素的位置。 -
监听
mouseup
事件: 当用户释放鼠标按钮时,清除拖拽开始的标志(如isDragging
),表示拖拽结束。
示例代码
这里是一个简单的 HTML 和 JavaScript 示例,演示了如何让一个div
元素可拖拽:
<!DOCTYPE html>
<html>
<head>
<title>鼠标拖拽示例</title>
<style>
#draggable {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
cursor: pointer;
}
</style>
</head>
<body>
<div id="draggable"></div>
<script>
// 获取元素
var draggable = document.getElementById("draggable");
var isDragging = false;
var offset = { x: 0, y: 0 };
draggable.addEventListener("mousedown", function (e) {
isDragging = true;
offset.x = e.clientX - draggable.getBoundingClientRect().left;
offset.y = e.clientY - draggable.getBoundingClientRect().top;
});
document.addEventListener("mousemove", function (e) {
if (isDragging) {
draggable.style.left = e.clientX - offset.x + "px";
draggable.style.top = e.clientY - offset.y + "px";
}
});
document.addEventListener("mouseup", function () {
isDragging = false;
});
</script>
</body>
</html>
注意事项
- 这个示例仅作为演示使用,实际应用可能需要更多的错误处理和边界条件判断。
- 为了防止拖拽时的文本选中现象,可能需要监听并阻止
mousemove
事件的默认行为。 - 记得附加适当的样式(如
cursor: move;
),提升用户体验。
根据你的需要,这个基本的逻辑和代码可以进行调整和扩展,以实现更复杂的拖拽功能。
要实时统计用户浏览器窗口大小,该如何做
要实时统计用户浏览器窗口大小,可以利用 JavaScript 中的 resize
事件。当浏览器窗口尺寸变化时,此事件会被触发。通过侦听此事件,可以实时获取并处理浏览器窗口的宽度和高度。
基础示例
下面是一个简单的示例,展示如何使用 resize
事件来获取并打印当前浏览器窗口的宽度和高度:
// 定义一个函数来处理窗口大小变化
function handleResize () {
const width = window.innerWidth
const height = window.innerHeight
console.log(`当前窗口大小:宽度 = ${width}, 高度 = ${height}`)
}
// 在窗口 resize 事件上添加监听器
window.addEventListener('resize', handleResize)
// 初始化时执行一次,确保获取初始窗口大小
handleResize()
节流优化
如果你担心 resize
事件触发得太频繁,可能会影响页面性能,可以引入“节流”(throttle)机制来限制事件处理函数的执行频率。节流确保了即使事件持续触发,事件处理函数也只在每隔一段时间执行一次。
以下是如何应用节流优化的示例:
function throttle (fn, wait) {
let inThrottle, lastFn, lastTime
return function () {
const context = this
const args = arguments
if (!inThrottle) {
fn.apply(context, args)
lastTime = Date.now()
inThrottle = true
} else {
clearTimeout(lastFn)
lastFn = setTimeout(function () {
if (Date.now() - lastTime >= wait) {
fn.apply(context, args)
lastTime = Date.now()
}
}, Math.max(wait - (Date.now() - lastTime), 0))
}
}
}
// 使用节流函数包装我们的处理器
const throttledHandleResize = throttle(handleResize, 100)
// 添加节流化的事件监听
window.addEventListener('resize', throttledHandleResize)
这个 throttle
函数通过确保被包装的 handleResize
函数在指定的时间间隔(本例中为 100 毫秒)内最多只执行一次,来减少 resize
事件处理函数的调用频率。
应用场景
这样实时统计用户浏览器窗口大小的方法可以用于多种应用场景,如响应式布局调整、基于窗口大小动态加载资源、或者其他需要根据视窗大小变化进行调整的交互效果实现。
使用这种方法时,重要的是平衡事件处理函数的执行频率和页面的性能,特别是当你的窗口大小调整处理函数中包含复杂操作时。通过合理利用“节流”或“防抖”(debounce)技术,可以有效地解决这个问题。