跳到主要内容

事件✅

事件冒泡是什么?

答案

参考 w3c ui event 冒泡机制是为了解决在如何在 DOM 树上广播事件。整个广播流程参考下图:

整个事件从创建到完成的流程如下

  1. 事件产生,用户点击等操作,此时会创建一个 event target 对象包含出发事件时的相关信息
    • target 指向触发事件的元素
    • currentTarget 只读属性指向事件分发过程中的当前对象
    • 在回调函数中 this 指向 currentTarget.
  2. 根据当前触发元素确定传播路径
  3. 事件分发,此时时间会在 DOM 树上进行广播,广播分为三个阶段
    1. 捕获阶段 从传播路径的根元素向目标元素传播,从 window 开始
    2. 目标阶段 当传播到达事件目标时
    3. 冒泡阶段 当从事件目标对象向父元素广播时从 window 结束。

事件广播过程会修改 event target 相关状态,此外可以利用 stopPropagation 对事件传播进行截断。

  1. 事件处理阶段,在事件分发过程中若传播路径有元素绑定了回调事件,event target 对象作为传入参数触发执行。

参考 w3c ui event 说明

但目标元素触发事件时,事件的分发分为三个阶段:

  1. 捕获阶段(capture phase) 从根元素逐级向目标元素传递
  2. 目标阶段(target phase) 传递到目标元素的阶段
  3. 冒泡阶段(bubbling phase) 从目标元素重新传递到根元素的阶段

事件传播演示

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>事件传播演示</title>
  <style>
      body {
          font-family: Arial, sans-serif;
          padding: 20px;
      }
      .container {
          border: 2px solid #007bff;
          padding: 20px;
          margin: 10px;
          border-radius: 8px;
          background-color: #f8f9fa;
      }
      .grand-parent {
          border-color: #dc3545;
          background-color: #f8d7da;
      }
      .parent {
          border-color: #ffc107;
          background-color: #fff3cd;
      }
      .children {
          border-color: #28a745;
          background-color: #d4edda;
          text-align: center;
          cursor: pointer;
          padding: 40px;
          user-select: none;
      }
      .children:hover {
          background-color: #c3e6cb;
      }
      .controls {
          margin: 20px 0;
          padding: 15px;
          background-color: #e9ecef;
          border-radius: 5px;
      }
      label {
          margin-right: 15px;
          font-weight: bold;
      }
      .log {
          height: 200px;
          border: 1px solid #ccc;
          padding: 10px;
          overflow-y: auto;
          background-color: #f8f9fa;
          margin-top: 15px;
          font-family: monospace;
      }
      .clear-btn {
          margin-top: 10px;
          padding: 5px 10px;
          background-color: #6c757d;
          color: white;
          border: none;
          border-radius: 3px;
          cursor: pointer;
      }
      .phase-capture { color: #007bff; }
      .phase-target { color: #dc3545; font-weight: bold; }
      .phase-bubble { color: #28a745; }
  </style>
</head>
<body>
  <h1>事件传播机制演示</h1>

  <div class="controls">
      <label for="enableCapture">
          <input id="enableCapture" name="enableCapture" type="checkbox" />
          开启捕获阶段监听
      </label>
      <span style="margin-left: 20px; color: #666;">
          点击最内层的 "children" 元素观察事件传播
      </span>
  </div>
  
  <div class="container grand-parent">
      <strong>grand-parent (祖父元素)</strong>
      <div class="container parent">
          <strong>parent (父元素)</strong>
          <div class="container children">
              <strong>children (子元素)</strong><br>
              点击我观察事件传播!
          </div>
      </div>
  </div>
  
  <div>
      <h3>事件传播日志:</h3>
      <div class="log" id="eventLog"></div>
      <button class="clear-btn" onclick="clearLog()">清空日志</button>
  </div>

  <script>
      const divs = document.querySelectorAll('.container');
      const checkbox = document.getElementById('enableCapture');
      const eventLog = document.getElementById('eventLog');
      
      let eventCount = 0;
      
      // 事件处理函数
      function createEventHandler(phase) {
          return function(e) {
              eventCount++;
              const currentElement = e.currentTarget;
              const targetElement = e.target;
              const className = currentElement.className.split(' ').find(cls => 
                  ['grand-parent', 'parent', 'children'].includes(cls)
              ) || 'unknown';
              
              let phaseText = '';
              let phaseClass = '';
              
              switch(e.eventPhase) {
                  case Event.CAPTURING_PHASE:
                      phaseText = '捕获阶段';
                      phaseClass = 'phase-capture';
                      break;
                  case Event.AT_TARGET:
                      phaseText = '目标阶段';
                      phaseClass = 'phase-target';
                      break;
                  case Event.BUBBLING_PHASE:
                      phaseText = '冒泡阶段';
                      phaseClass = 'phase-bubble';
                      break;
              }
              
              const logEntry = document.createElement('div');
              logEntry.innerHTML = `
                  <span style="color: #666;">${eventCount}.</span>
                  <span class="${phaseClass}">${phaseText}</span> - 
                  当前处理元素: <strong>${className}</strong>, 
                  事件目标: <strong>${targetElement.className.split(' ').find(cls => 
                      ['grand-parent', 'parent', 'children'].includes(cls)
                  ) || 'unknown'}</strong>
              `;
              
              eventLog.appendChild(logEntry);
              eventLog.scrollTop = eventLog.scrollHeight;
              
              console.log(`${phaseText} - 当前元素: ${className}, 目标: ${targetElement.className}`);
          };
      }
      
      // 绑定事件监听器
      function bindEvents(useCapture) {
          // 移除现有监听器
          divs.forEach(div => {
              div.removeEventListener('click', createEventHandler('bubble'));
              div.removeEventListener('click', createEventHandler('capture'));
          });
          
          // 重新绑定监听器
          divs.forEach(div => {
              // 冒泡阶段监听器(总是绑定)
              div.addEventListener('click', createEventHandler('bubble'), false);
              
              // 捕获阶段监听器(根据复选框决定)
              if (useCapture) {
                  div.addEventListener('click', createEventHandler('capture'), true);
              }
          });
          
          // 同时在 window 和 document 上演示事件传播
          window.removeEventListener('click', windowHandler, true);
          window.removeEventListener('click', windowHandler, false);
          document.removeEventListener('click', documentHandler, true);
          document.removeEventListener('click', documentHandler, false);
          
          if (useCapture) {
              window.addEventListener('click', windowHandler, true);
              document.addEventListener('click', documentHandler, true);
          }
          window.addEventListener('click', windowHandler, false);
          document.addEventListener('click', documentHandler, false);
      }
      
      function windowHandler(e) {
          if (e.target.classList.contains('children')) {
              eventCount++;
              const phaseText = e.eventPhase === Event.CAPTURING_PHASE ? '捕获阶段' : '冒泡阶段';
              const phaseClass = e.eventPhase === Event.CAPTURING_PHASE ? 'phase-capture' : 'phase-bubble';
              
              const logEntry = document.createElement('div');
              logEntry.innerHTML = `
                  <span style="color: #666;">${eventCount}.</span>
                  <span class="${phaseClass}">${phaseText}</span> - 
                  当前处理元素: <strong>window</strong>
              `;
              eventLog.appendChild(logEntry);
              eventLog.scrollTop = eventLog.scrollHeight;
          }
      }
      
      function documentHandler(e) {
          if (e.target.classList.contains('children')) {
              eventCount++;
              const phaseText = e.eventPhase === Event.CAPTURING_PHASE ? '捕获阶段' : '冒泡阶段';
              const phaseClass = e.eventPhase === Event.CAPTURING_PHASE ? 'phase-capture' : 'phase-bubble';
              
              const logEntry = document.createElement('div');
              logEntry.innerHTML = `
                  <span style="color: #666;">${eventCount}.</span>
                  <span class="s${phaseClass}">${phaseText}</span> - 
                  当前处理元素: <strong>document</strong>
              `;
              eventLog.appendChild(logEntry);
              eventLog.scrollTop = eventLog.scrollHeight;
          }
      }
      
      // 清空日志
      function clearLog() {
          eventLog.innerHTML = '';
          eventCount = 0;
      }
      
      // 监听复选框变化
      checkbox.addEventListener('change', () => {
          bindEvents(checkbox.checked);
          clearLog();
      });
      
      // 初始化
      bindEvents(checkbox.checked);
      
      // 页面加载完成提示
      document.addEventListener('DOMContentLoaded', () => {
          console.log('事件传播演示页面加载完成');
      });
  </script>
</body>
</html>

事件委托的作用和意义?

答案

事件委托是一种常用的事件处理模式,主要通过将事件监听器添加到父元素上,而不是每个子元素上,从而提高性能和简化代码。其作用和意义包括:

  1. 提高性能:减少事件监听器的数量,降低内存消耗,尤其在子元素较多时更为明显。
  2. 简化代码:通过事件冒泡,父元素可以处理所有子元素的事件,避免为每个子元素单独绑定事件。
  3. 动态添加元素:对于动态生成的子元素,父元素的事件监听器可以自动响应,无需重新绑定事件。

实际示例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>事件委托演示</title>
  <style>
      body {
          font-family: Arial, sans-serif;
          padding: 20px;
      }
      #parent {
          border: 2px solid #007bff;
          padding: 20px;
          margin: 20px 0;
          border-radius: 8px;
          background-color: #f8f9fa;
      }
      .btn {
          background: #28a745;
          color: white;
          border: none;
          padding: 10px 15px;
          margin: 5px;
          border-radius: 5px;
          cursor: pointer;
          transition: all 0.3s ease;
          list-style: none;
      }
      .btn:hover {
          background: #218838;
          transform: translateY(-2px);
      }
      .add-btn {
          background: #007bff;
          margin-top: 10px;
      }
      .add-btn:hover {
          background: #0056b3;
      }
      .result {
          margin-top: 20px;
          padding: 15px;
          background-color: #d4edda;
          border: 1px solid #c3e6cb;
          border-radius: 5px;
          min-height: 50px;
      }
      .counter {
          font-weight: bold;
          color: #007bff;
      }
  </style>
</head>
<body>
  <h1>演示事件委托</h1>

  <div>
      <h3>原有按钮列表(事件委托处理)</h3>
      <ul id='parent'>
          <li class="btn" data-id="1">按钮 1</li>
          <li class="btn" data-id="2">按钮 2</li>
          <li class="btn" data-id="3">按钮 3</li>
      </ul>
      
      <button class="add-btn" onclick="addNewButton()">动态添加新按钮</button>
  </div>
  
  <div class="result">
      <h4>点击结果:</h4>
      <div id="output">点击上面的按钮查看事件委托效果</div>
      <p>点击计数: <span class="counter" id="counter">0</span></p>
  </div>
  
  <script>
      let clickCount = 0;
      let buttonCount = 3;
      
      // 使用事件委托:在父元素上绑定一个事件监听器
      const parentElement = document.getElementById('parent');
      const outputElement = document.getElementById('output');
      const counterElement = document.getElementById('counter');
      
      parentElement.addEventListener('click', function(e) {
          // 检查点击的是否是按钮
          if (e.target.classList.contains('btn')) {
              clickCount++;
              const buttonId = e.target.dataset.id;
              const buttonText = e.target.textContent;
              
              // 更新显示
              outputElement.innerHTML = `
                  <strong>点击了:</strong> ${buttonText} (ID: ${buttonId})<br>
                  <strong>事件目标:</strong> ${e.target.tagName} 元素<br>
                  <strong>事件委托容器:</strong> ${e.currentTarget.tagName} 元素<br>
                  <strong>时间:</strong> ${new Date().toLocaleTimeString()}
              `;
              
              counterElement.textContent = clickCount;
              
              // 控制台输出
              console.log(`事件委托处理: 点击了按钮 ${buttonId}`);
          }
      });
      
      // 动态添加新按钮的函数
      function addNewButton() {
          buttonCount++;
          const newButton = document.createElement('li');
          newButton.className = 'btn';
          newButton.dataset.id = buttonCount;
          newButton.textContent = `按钮 ${buttonCount} (动态添加)`;
          
          parentElement.appendChild(newButton);
          
          console.log(`添加了新按钮: 按钮 ${buttonCount}`);
      }
      
      // 演示:如果我们不使用事件委托,每次添加新元素都需要重新绑定事件
      function demonstrateWithoutDelegation() {
          // 这种方式需要为每个按钮单独绑定事件,效率较低
          document.querySelectorAll('.btn').forEach(btn => {
              btn.addEventListener('click', function() {
                  console.log('非委托方式处理');
              });
          });
      }
  </script>
</body>
</html>

说明 load,ready,DOMContentLoaded 的区别

答案
属性DOMContentLoadedloadready
触发时机DOM结构解析完成页面所有资源加载完毕DOM结构解析完成
典型用途尽早操作DOM、初始化交互依赖全部资源的操作早期DOM操作、兼容性处理
是否等待外部资源
适用范围原生JS原生JSjQuery专用
// 原生 DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
// DOM 可操作
})

// 原生 load
window.addEventListener('load', () => {
// 所有资源加载完毕
})

// jQuery ready
$(function () {
// DOM 可操作
})
提示

推荐优先使用 DOMContentLoaded 或 jQuery 的 ready,可提升页面响应速度和用户体验。

注意

load 事件需等待所有资源加载,页面大图或慢资源会延迟触发,影响初始化时机。

延伸阅读

stopImmediatePropagation 和 stopPropagation 区别

答案

stopPropagationstopImmediatePropagation 都用于阻止事件冒泡,但作用范围不同:

|方法|作用范围|典型场景| |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

答案

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()
  • 不理解拖拽数据类型和格式要求
  • 忽略拖拽过程中的视觉反馈和用户体验

延伸阅读:

拖曳控制窗口

关键词:拖拽 api、mousedownmousemovemouseup事件

实现鼠标拖拽功能通常涉及到监听和处理鼠标事件,比如:mousedownmousemovemouseup事件。下面是一个基本的步骤指南以及一个简易的示例代码(使用 HTML 和 JavaScript),展示了如何实现一个元素的鼠标拖拽功能。

基本步骤

  1. 监听mousedown事件: 当用户按下鼠标按钮时,记录被拖拽元素的初始位置,并设置一个标志(如isDragging)表示拖拽开始。

  2. 监听mousemove事件: 当用户移动鼠标时,如果拖拽已开始,则根据鼠标当前位置和初始位置的差值,更新被拖拽元素的位置。

  3. 监听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)技术,可以有效地解决这个问题。