引言

HTML DOM(Document Object Model,文档对象模型)是前端开发的核心技术之一,它提供了与HTML文档交互的接口,使开发者能够动态地访问和更新文档的内容、结构和样式。掌握DOM操作是构建现代、交互式网页应用的基础。本教程将从基础概念开始,逐步深入,带你全面了解DOM的技术奥秘,并通过实际案例分析,帮助你从前端开发入门者成长为精通DOM操作的专业开发者。

HTML DOM基础概念

什么是DOM

DOM是一个跨平台和语言独立的接口,它将HTML或XML文档表示为一个树结构,其中每个节点都是文档的一部分(如元素、属性、文本等)。通过DOM,编程语言可以连接到页面,从而改变文档的结构、样式和内容。

<!DOCTYPE html> <html> <head> <title>DOM示例</title> </head> <body> <h1>欢迎学习DOM</h1> <p>这是一个简单的DOM示例。</p> </body> </html> 

上面的HTML文档可以表示为以下DOM树:

Document └── html ├── head │ └── title │ └── "DOM示例" └── body ├── h1 │ └── "欢迎学习DOM" └── p └── "这是一个简单的DOM示例。" 

DOM树结构

DOM树由节点组成,每个节点代表文档的一部分。主要有以下几种节点类型:

  1. 文档节点(Document):整个文档的根节点
  2. 元素节点(Element):HTML元素,如<div><p>
  3. 属性节点(Attr):元素的属性,如idclass
  4. 文本节点(Text):元素中的文本内容
  5. 注释节点(Comment):HTML注释

节点关系

DOM树中的节点有以下关系:

  • 父节点(Parent):直接包含当前节点的节点
  • 子节点(Child):被当前节点直接包含的节点
  • 兄弟节点(Sibling):拥有相同父节点的节点
  • 祖先节点(Ancestor):父节点、祖父节点等
  • 后代节点(Descendant):子节点、孙节点等
// 访问节点关系的示例 const body = document.body; // 获取body元素 const html = document.documentElement; // 获取html元素 console.log(body.parentNode); // 输出html元素 console.log(html.childNodes); // 输出html元素的所有子节点 console.log(body.previousSibling); // 输出body的前一个兄弟节点 console.log(body.nextSibling); // 输出body的后一个兄弟节点 

DOM操作基础

选择元素

在DOM中操作元素之前,首先需要选择这些元素。JavaScript提供了多种方法来选择DOM元素:

// 通过ID选择元素 const elementById = document.getElementById('myId'); // 通过类名选择元素 const elementsByClass = document.getElementsByClassName('myClass'); // 通过标签名选择元素 const elementsByTag = document.getElementsByTagName('div'); // 通过CSS选择器选择单个元素 const elementByQuery = document.querySelector('#myId .myClass'); // 通过CSS选择器选择多个元素 const elementsByQueryAll = document.querySelectorAll('div.myClass'); 

修改元素

选择元素后,可以修改它们的内容、属性和样式:

// 修改元素内容 const myElement = document.getElementById('myElement'); myElement.textContent = '新的文本内容'; // 修改文本内容 myElement.innerHTML = '<strong>新的HTML内容</strong>'; // 修改HTML内容 // 修改元素属性 myElement.id = 'newId'; // 修改id属性 myElement.className = 'newClass'; // 修改class属性 myElement.setAttribute('data-custom', 'customValue'); // 设置自定义属性 // 修改元素样式 myElement.style.color = 'red'; // 修改颜色 myElement.style.fontSize = '16px'; // 修改字体大小 myElement.style.display = 'none'; // 隐藏元素 

创建和删除元素

动态创建和删除元素是构建复杂页面结构的重要技能:

// 创建新元素 const newDiv = document.createElement('div'); // 创建div元素 const newText = document.createTextNode('这是新文本'); // 创建文本节点 // 设置元素属性和内容 newDiv.id = 'newDiv'; newDiv.className = 'container'; newDiv.appendChild(newText); // 将文本节点添加到div元素中 // 将新元素添加到DOM中 document.body.appendChild(newDiv); // 将div添加到body的末尾 // 在指定元素前插入新元素 const referenceElement = document.getElementById('referenceElement'); document.body.insertBefore(newDiv, referenceElement); // 替换元素 const oldElement = document.getElementById('oldElement'); document.body.replaceChild(newDiv, oldElement); // 删除元素 const elementToRemove = document.getElementById('elementToRemove'); elementToRemove.parentNode.removeChild(elementToRemove); 

构建复杂页面结构的技术

语义化HTML

使用语义化HTML标签可以构建更清晰、更易于维护的页面结构,同时也有助于SEO和可访问性:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>语义化HTML示例</title> </head> <body> <header> <nav> <ul> <li><a href="#home">首页</a></li> <li><a href="#about">关于</a></li> <li><a href="#contact">联系</a></li> </ul> </nav> </header> <main> <section> <h1>文章标题</h1> <article> <p>这是文章内容...</p> </article> </section> <aside> <h2>相关链接</h2> <ul> <li><a href="#">链接1</a></li> <li><a href="#">链接2</a></li> </ul> </aside> </main> <footer> <p>&copy; 2023 版权所有</p> </footer> </body> </html> 

文档对象模型的高级操作

除了基本的DOM操作,还有一些高级技术可以帮助构建更复杂的页面结构:

遍历DOM树

// 深度优先遍历DOM树 function traverseDOM(node, depth = 0) { if (!node) return; // 打印当前节点信息 const indent = ' '.repeat(depth * 2); console.log(`${indent}${node.nodeName}: ${node.textContent.trim() || ''}`); // 递归遍历子节点 for (let i = 0; i < node.childNodes.length; i++) { traverseDOM(node.childNodes[i], depth + 1); } } // 从body开始遍历 traverseDOM(document.body); 

克隆节点

// 克隆节点(浅拷贝,不克隆事件监听器) const originalElement = document.getElementById('original'); const shallowClone = originalElement.cloneNode(false); // false表示不克隆子节点 // 克隆节点(深拷贝,包括子节点) const deepClone = originalElement.cloneNode(true); // true表示克隆子节点 // 将克隆的节点添加到DOM中 document.body.appendChild(deepClone); 

操作文档片段

文档片段(DocumentFragment)是一个轻量级的DOM对象,可以用来存储临时的DOM结构,然后一次性添加到DOM中,减少页面重绘次数:

// 创建文档片段 const fragment = document.createDocumentFragment(); // 创建多个列表项并添加到片段中 for (let i = 1; i <= 10; i++) { const li = document.createElement('li'); li.textContent = `列表项 ${i}`; fragment.appendChild(li); } // 一次性将片段添加到DOM中 const ul = document.getElementById('myList'); ul.appendChild(fragment); 

事件处理

事件处理是构建交互式页面的关键。以下是事件处理的一些高级技术:

事件冒泡和捕获

// 事件冒泡示例 document.getElementById('outer').addEventListener('click', function() { console.log('外层元素被点击'); }); document.getElementById('inner').addEventListener('click', function(e) { console.log('内层元素被点击'); // 阻止事件冒泡 e.stopPropagation(); }); // 事件捕获示例 document.getElementById('outer').addEventListener('click', function() { console.log('外层元素被点击(捕获阶段)'); }, true); // true表示在捕获阶段触发 document.getElementById('inner').addEventListener('click', function() { console.log('内层元素被点击(捕获阶段)'); }, true); 

事件委托

事件委托是一种利用事件冒泡机制的技术,通过在父元素上设置事件监听器来管理多个子元素的事件:

<ul id="todoList"> <li data-id="1">任务1</li> <li data-id="2">任务2</li> <li data-id="3">任务3</li> </ul> 
// 使用事件委托处理列表项点击 document.getElementById('todoList').addEventListener('click', function(e) { // 检查点击的是否是li元素 if (e.target.tagName === 'LI') { const taskId = e.target.getAttribute('data-id'); console.log(`任务 ${taskId} 被点击`); // 切换完成状态 e.target.classList.toggle('completed'); } }); 

自定义事件

自定义事件允许你创建和触发自己的事件:

// 创建自定义事件 const customEvent = new CustomEvent('dataLoaded', { detail: { data: '这是自定义数据' } }); // 添加自定义事件监听器 document.addEventListener('dataLoaded', function(e) { console.log('自定义事件被触发:', e.detail.data); }); // 触发自定义事件 document.dispatchEvent(customEvent); 

动态内容加载

动态加载内容是现代Web应用的常见需求,以下是几种实现方式:

AJAX请求

// 使用XMLHttpRequest发送AJAX请求 function loadContent(url, callback) { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onload = function() { if (xhr.status === 200) { callback(null, xhr.responseText); } else { callback(new Error(`请求失败,状态码: ${xhr.status}`)); } }; xhr.onerror = function() { callback(new Error('网络错误')); }; xhr.send(); } // 使用示例 loadContent('https://api.example.com/data', function(error, data) { if (error) { console.error('加载数据失败:', error); return; } // 解析数据并更新DOM const container = document.getElementById('contentContainer'); container.innerHTML = data; }); 

Fetch API

// 使用Fetch API获取数据 fetch('https://api.example.com/data') .then(response => { if (!response.ok) { throw new Error(`HTTP错误,状态码: ${response.status}`); } return response.json(); // 假设返回的是JSON数据 }) .then(data => { // 处理数据并更新DOM updateDOM(data); }) .catch(error => { console.error('获取数据失败:', error); }); function updateDOM(data) { const container = document.getElementById('contentContainer'); // 清空容器 container.innerHTML = ''; // 创建并添加新内容 data.items.forEach(item => { const element = document.createElement('div'); element.className = 'item'; element.textContent = item.title; container.appendChild(element); }); } 

动态脚本加载

// 动态加载外部脚本 function loadScript(url, callback) { const script = document.createElement('script'); script.src = url; script.onload = function() { callback(null); }; script.onerror = function() { callback(new Error(`加载脚本失败: ${url}`)); }; document.head.appendChild(script); } // 使用示例 loadScript('https://example.com/external-script.js', function(error) { if (error) { console.error(error); return; } console.log('脚本加载完成'); // 在这里可以使用脚本中定义的函数或变量 }); 

性能优化

减少DOM重绘和回流

DOM操作是昂贵的,频繁的操作会导致性能问题。以下是一些减少DOM重绘和回流的技巧:

// 不好的做法:多次修改样式,导致多次回流 const element = document.getElementById('myElement'); element.style.width = '100px'; element.style.height = '100px'; element.style.backgroundColor = 'red'; // 好的做法:一次性修改样式 const element = document.getElementById('myElement'); element.style.cssText = 'width: 100px; height: 100px; background-color: red;'; // 或者使用class const element = document.getElementById('myElement'); element.className = 'new-style'; 

使用文档片段批量操作

// 不好的做法:多次添加到DOM,导致多次回流 const list = document.getElementById('list'); for (let i = 0; i < 1000; i++) { const item = document.createElement('li'); item.textContent = `Item ${i}`; list.appendChild(item); } // 好的做法:使用文档片段 const list = document.getElementById('list'); const fragment = document.createDocumentFragment(); for (let i = 0; i < 1000; i++) { const item = document.createElement('li'); item.textContent = `Item ${i}`; fragment.appendChild(item); } list.appendChild(fragment); 

脱离文档流操作

// 好的做法:脱离文档流操作,减少回流 const element = document.getElementById('myElement'); const parent = element.parentNode; // 临时移除元素 parent.removeChild(element); // 执行多次修改 element.style.width = '100px'; element.style.height = '100px'; element.innerHTML = '新的内容'; // 重新添加到DOM parent.appendChild(element); 

虚拟DOM

虚拟DOM是现代前端框架(如React、Vue)使用的性能优化技术,它通过在内存中创建轻量级的DOM表示,然后批量更新真实DOM来提高性能。

以下是一个简化的虚拟DOM实现示例:

// 虚拟DOM节点类 class VNode { constructor(tag, props = {}, children = []) { this.tag = tag; this.props = props; this.children = children; } } // 创建虚拟DOM function h(tag, props, ...children) { return new VNode(tag, props, children.flat()); } // 将虚拟DOM渲染为真实DOM function render(vnode) { // 如果是文本节点 if (typeof vnode === 'string' || typeof vnode === 'number') { return document.createTextNode(vnode); } // 创建元素 const element = document.createElement(vnode.tag); // 设置属性 Object.entries(vnode.props).forEach(([key, value]) => { element.setAttribute(key, value); }); // 渲染子节点 vnode.children.forEach(child => { element.appendChild(render(child)); }); return element; } // 使用示例 const virtualDOM = h('div', { id: 'app' }, h('h1', null, '虚拟DOM示例'), h('p', null, '这是一个简化的虚拟DOM实现') ); // 渲染为真实DOM const realDOM = render(virtualDOM); document.body.appendChild(realDOM); 

事件委托

事件委托不仅可以简化代码,还可以提高性能,特别是当有大量元素需要相同的事件处理时:

<!-- 不好的做法:为每个按钮添加事件监听器 --> <div id="buttonContainer"> <button class="btn">按钮1</button> <button class="btn">按钮2</button> <button class="btn">按钮3</button> <!-- 可能有数百个按钮 --> </div> 
// 不好的做法:为每个按钮添加事件监听器 const buttons = document.querySelectorAll('.btn'); buttons.forEach(button => { button.addEventListener('click', function() { console.log('按钮被点击:', this.textContent); }); }); 
// 好的做法:使用事件委托 document.getElementById('buttonContainer').addEventListener('click', function(e) { if (e.target.classList.contains('btn')) { console.log('按钮被点击:', e.target.textContent); } }); 

案例分析

案例1:动态表单构建

在这个案例中,我们将创建一个动态表单构建器,允许用户添加、删除和重新排序表单字段。

<!DOCTYPE html> <html lang="zh-CN"> <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; max-width: 800px; margin: 0 auto; padding: 20px; } .form-builder { border: 1px solid #ddd; padding: 20px; border-radius: 5px; } .form-field { border: 1px solid #eee; padding: 15px; margin-bottom: 10px; border-radius: 3px; position: relative; } .form-field.dragging { opacity: 0.5; } .field-controls { position: absolute; top: 5px; right: 5px; } .field-controls button { margin-left: 5px; cursor: pointer; } .add-field-btn { background-color: #4CAF50; color: white; border: none; padding: 10px 15px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; margin: 10px 0; cursor: pointer; border-radius: 4px; } .drag-handle { cursor: move; margin-right: 10px; } .field-type { margin-bottom: 10px; } .field-label, .field-placeholder { width: 100%; padding: 8px; margin-bottom: 10px; box-sizing: border-box; } .preview-form { margin-top: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 5px; } .preview-form input, .preview-form textarea, .preview-form select { width: 100%; padding: 8px; margin-bottom: 15px; box-sizing: border-box; } </style> </head> <body> <h1>动态表单构建器</h1> <div class="form-builder"> <h2>表单字段</h2> <div id="fieldsContainer"> <!-- 表单字段将在这里动态添加 --> </div> <button id="addFieldBtn" class="add-field-btn">添加字段</button> </div> <div class="preview-form"> <h2>表单预览</h2> <form id="previewForm"> <!-- 表单预览将在这里动态生成 --> </form> </div> <script> document.addEventListener('DOMContentLoaded', function() { const fieldsContainer = document.getElementById('fieldsContainer'); const addFieldBtn = document.getElementById('addFieldBtn'); const previewForm = document.getElementById('previewForm'); // 字段类型选项 const fieldTypes = [ { value: 'text', label: '文本输入' }, { value: 'email', label: '邮箱' }, { value: 'password', label: '密码' }, { value: 'textarea', label: '多行文本' }, { value: 'select', label: '下拉选择' }, { value: 'checkbox', label: '复选框' }, { value: 'radio', label: '单选按钮' } ]; // 添加字段按钮点击事件 addFieldBtn.addEventListener('click', addFormField); // 添加表单字段 function addFormField() { const fieldId = 'field_' + Date.now(); const fieldDiv = document.createElement('div'); fieldDiv.className = 'form-field'; fieldDiv.draggable = true; fieldDiv.dataset.fieldId = fieldId; // 创建字段类型选择 const typeSelect = document.createElement('select'); typeSelect.className = 'field-type'; fieldTypes.forEach(type => { const option = document.createElement('option'); option.value = type.value; option.textContent = type.label; typeSelect.appendChild(option); }); // 创建标签输入 const labelInput = document.createElement('input'); labelInput.type = 'text'; labelInput.className = 'field-label'; labelInput.placeholder = '字段标签'; // 创建占位符输入 const placeholderInput = document.createElement('input'); placeholderInput.type = 'text'; placeholderInput.className = 'field-placeholder'; placeholderInput.placeholder = '占位符文本'; // 创建控制按钮 const controlsDiv = document.createElement('div'); controlsDiv.className = 'field-controls'; const dragHandle = document.createElement('span'); dragHandle.className = 'drag-handle'; dragHandle.textContent = '≡'; const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除'; deleteBtn.addEventListener('click', function() { fieldDiv.remove(); updatePreview(); }); controlsDiv.appendChild(dragHandle); controlsDiv.appendChild(deleteBtn); // 添加所有元素到字段div fieldDiv.appendChild(typeSelect); fieldDiv.appendChild(labelInput); fieldDiv.appendChild(placeholderInput); fieldDiv.appendChild(controlsDiv); // 添加事件监听器 typeSelect.addEventListener('change', updatePreview); labelInput.addEventListener('input', updatePreview); placeholderInput.addEventListener('input', updatePreview); // 添加拖放事件 fieldDiv.addEventListener('dragstart', handleDragStart); fieldDiv.addEventListener('dragover', handleDragOver); fieldDiv.addEventListener('drop', handleDrop); fieldDiv.addEventListener('dragend', handleDragEnd); // 添加到容器 fieldsContainer.appendChild(fieldDiv); // 更新预览 updatePreview(); } // 拖放功能 let draggedElement = null; function handleDragStart(e) { draggedElement = this; this.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/html', this.innerHTML); } function handleDragOver(e) { if (e.preventDefault) { e.preventDefault(); } e.dataTransfer.dropEffect = 'move'; return false; } function handleDrop(e) { if (e.stopPropagation) { e.stopPropagation(); } if (draggedElement !== this) { const allFields = [...fieldsContainer.querySelectorAll('.form-field')]; const draggedIndex = allFields.indexOf(draggedElement); const targetIndex = allFields.indexOf(this); if (draggedIndex < targetIndex) { this.parentNode.insertBefore(draggedElement, this.nextSibling); } else { this.parentNode.insertBefore(draggedElement, this); } updatePreview(); } return false; } function handleDragEnd(e) { const fields = [...fieldsContainer.querySelectorAll('.form-field')]; fields.forEach(field => { field.classList.remove('dragging'); }); } // 更新表单预览 function updatePreview() { // 清空预览表单 previewForm.innerHTML = ''; // 获取所有字段 const fields = fieldsContainer.querySelectorAll('.form-field'); // 为每个字段创建预览 fields.forEach(field => { const typeSelect = field.querySelector('.field-type'); const labelInput = field.querySelector('.field-label'); const placeholderInput = field.querySelector('.field-placeholder'); const type = typeSelect.value; const label = labelInput.value || '未命名字段'; const placeholder = placeholderInput.value || ''; // 创建标签 const labelElement = document.createElement('label'); labelElement.textContent = label; // 根据类型创建输入元素 let inputElement; switch(type) { case 'textarea': inputElement = document.createElement('textarea'); inputElement.placeholder = placeholder; break; case 'select': inputElement = document.createElement('select'); const defaultOption = document.createElement('option'); defaultOption.textContent = placeholder || '请选择'; defaultOption.value = ''; inputElement.appendChild(defaultOption); // 添加一些示例选项 for (let i = 1; i <= 3; i++) { const option = document.createElement('option'); option.value = `option${i}`; option.textContent = `选项 ${i}`; inputElement.appendChild(option); } break; case 'checkbox': case 'radio': inputElement = document.createElement('input'); inputElement.type = type; inputElement.id = `field_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // 为复选框和单选按钮创建包装器 const wrapper = document.createElement('div'); wrapper.appendChild(inputElement); // 将标签与输入元素关联 labelElement.htmlFor = inputElement.id; wrapper.appendChild(labelElement); previewForm.appendChild(wrapper); return; // 提前返回,因为已经添加了元素 default: inputElement = document.createElement('input'); inputElement.type = type; inputElement.placeholder = placeholder; } // 添加标签和输入元素到预览表单 previewForm.appendChild(labelElement); previewForm.appendChild(inputElement); }); // 添加提交按钮 const submitBtn = document.createElement('button'); submitBtn.type = 'submit'; submitBtn.textContent = '提交'; previewForm.appendChild(submitBtn); } // 添加一个初始字段 addFormField(); }); </script> </body> </html> 

这个动态表单构建器展示了以下DOM操作技术:

  1. 动态创建元素:使用document.createElement创建各种表单元素。
  2. 事件处理:为按钮、下拉菜单等添加事件监听器。
  3. 拖放功能:实现字段的拖放重新排序。
  4. 表单预览:根据用户输入动态生成表单预览。
  5. 事件委托:使用事件委托处理动态添加的元素的事件。

案例2:无限滚动列表

无限滚动是一种常见的UI模式,当用户滚动到页面底部时,自动加载更多内容。以下是一个实现无限滚动列表的例子:

<!DOCTYPE html> <html lang="zh-CN"> <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; max-width: 800px; margin: 0 auto; padding: 20px; } .item-list { border: 1px solid #ddd; border-radius: 5px; overflow: hidden; } .list-item { padding: 15px; border-bottom: 1px solid #eee; display: flex; align-items: center; } .list-item:last-child { border-bottom: none; } .item-image { width: 60px; height: 60px; border-radius: 50%; margin-right: 15px; object-fit: cover; } .item-content { flex: 1; } .item-title { font-weight: bold; margin-bottom: 5px; } .item-description { color: #666; font-size: 14px; } .loading { text-align: center; padding: 20px; display: none; } .loading.active { display: block; } .spinner { border: 4px solid rgba(0, 0, 0, 0.1); border-radius: 50%; border-top: 4px solid #3498db; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 0 auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .end-message { text-align: center; padding: 20px; color: #666; display: none; } .end-message.active { display: block; } </style> </head> <body> <h1>无限滚动列表</h1> <div id="itemList" class="item-list"> <!-- 列表项将在这里动态添加 --> </div> <div id="loading" class="loading"> <div class="spinner"></div> <p>加载更多内容...</p> </div> <div id="endMessage" class="end-message"> <p>已加载全部内容</p> </div> <script> document.addEventListener('DOMContentLoaded', function() { const itemList = document.getElementById('itemList'); const loading = document.getElementById('loading'); const endMessage = document.getElementById('endMessage'); // 模拟数据 let currentPage = 0; const itemsPerPage = 10; let isLoading = false; let hasMore = true; // 初始加载 loadItems(); // 监听滚动事件 window.addEventListener('scroll', handleScroll); // 处理滚动事件 function handleScroll() { // 如果正在加载或没有更多数据,则不处理 if (isLoading || !hasMore) return; // 计算滚动位置 const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight; const clientHeight = document.documentElement.clientHeight || window.innerHeight; // 当滚动到距离底部200px时加载更多 if (scrollTop + clientHeight >= scrollHeight - 200) { loadItems(); } } // 加载项目 function loadItems() { isLoading = true; loading.classList.add('active'); // 模拟网络请求延迟 setTimeout(() => { // 生成模拟数据 const newItems = generateItems(currentPage, itemsPerPage); // 如果没有新数据,显示结束消息 if (newItems.length === 0) { hasMore = false; loading.classList.remove('active'); endMessage.classList.add('active'); isLoading = false; return; } // 创建文档片段 const fragment = document.createDocumentFragment(); // 为每个项目创建DOM元素 newItems.forEach(item => { const itemElement = createItemElement(item); fragment.appendChild(itemElement); }); // 一次性添加所有新项目 itemList.appendChild(fragment); // 更新页码 currentPage++; // 隐藏加载指示器 loading.classList.remove('active'); isLoading = false; }, 1000); // 模拟1秒的网络延迟 } // 生成模拟项目数据 function generateItems(page, perPage) { const items = []; const startIndex = page * perPage; // 模拟最多50个项目 if (startIndex >= 50) return items; const endIndex = Math.min(startIndex + perPage, 50); for (let i = startIndex; i < endIndex; i++) { items.push({ id: i + 1, title: `项目 ${i + 1}`, description: `这是项目 ${i + 1} 的描述文本。`, imageUrl: `https://picsum.photos/seed/item${i + 1}/60/60.jpg` }); } return items; } // 创建项目元素 function createItemElement(item) { const itemDiv = document.createElement('div'); itemDiv.className = 'list-item'; itemDiv.dataset.itemId = item.id; // 创建图片 const image = document.createElement('img'); image.className = 'item-image'; image.src = item.imageUrl; image.alt = item.title; // 创建内容容器 const content = document.createElement('div'); content.className = 'item-content'; // 创建标题 const title = document.createElement('div'); title.className = 'item-title'; title.textContent = item.title; // 创建描述 const description = document.createElement('div'); description.className = 'item-description'; description.textContent = item.description; // 组装元素 content.appendChild(title); content.appendChild(description); itemDiv.appendChild(image); itemDiv.appendChild(content); // 添加点击事件 itemDiv.addEventListener('click', function() { alert(`你点击了: ${item.title}`); }); return itemDiv; } }); </script> </body> </html> 

这个无限滚动列表展示了以下DOM操作技术:

  1. 动态内容加载:根据滚动位置动态加载更多内容。
  2. 文档片段:使用DocumentFragment批量添加DOM元素,减少重绘次数。
  3. 事件监听:监听滚动事件以触发内容加载。
  4. 性能优化:通过状态管理(isLoadinghasMore)避免重复加载。
  5. 条件渲染:根据状态显示或隐藏加载指示器和结束消息。

案例3:交互式数据可视化

在这个案例中,我们将创建一个交互式数据可视化组件,使用DOM操作来动态生成和更新图表。

<!DOCTYPE html> <html lang="zh-CN"> <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; max-width: 1000px; margin: 0 auto; padding: 20px; } .visualization-container { border: 1px solid #ddd; border-radius: 5px; padding: 20px; margin-bottom: 20px; } .chart-controls { margin-bottom: 20px; display: flex; gap: 10px; flex-wrap: wrap; } .chart-controls button { padding: 8px 15px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; } .chart-controls button:hover { background-color: #45a049; } .chart-controls button.active { background-color: #2196F3; } .chart-container { position: relative; height: 400px; border: 1px solid #eee; border-radius: 5px; overflow: hidden; } .bar-chart { display: flex; align-items: flex-end; height: 100%; padding: 20px; box-sizing: border-box; } .bar { flex: 1; margin: 0 5px; background-color: #4CAF50; position: relative; transition: height 0.5s ease; cursor: pointer; } .bar:hover { background-color: #45a049; } .bar-value { position: absolute; top: -25px; left: 0; right: 0; text-align: center; font-weight: bold; } .bar-label { position: absolute; bottom: -25px; left: 0; right: 0; text-align: center; font-size: 12px; } .line-chart { position: relative; height: 100%; padding: 20px; box-sizing: border-box; } .line-chart svg { width: 100%; height: 100%; } .pie-chart { position: relative; height: 100%; display: flex; justify-content: center; align-items: center; } .pie-chart svg { width: 300px; height: 300px; } .chart-legend { margin-top: 20px; display: flex; flex-wrap: wrap; justify-content: center; gap: 15px; } .legend-item { display: flex; align-items: center; } .legend-color { width: 15px; height: 15px; margin-right: 5px; } .tooltip { position: absolute; background-color: rgba(0, 0, 0, 0.8); color: white; padding: 8px 12px; border-radius: 4px; font-size: 14px; pointer-events: none; opacity: 0; transition: opacity 0.3s; z-index: 100; } .tooltip.active { opacity: 1; } .data-controls { margin-top: 20px; display: flex; gap: 10px; align-items: center; } .data-controls input { padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .data-controls button { padding: 8px 15px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; } .data-controls button:hover { background-color: #0b7dda; } </style> </head> <body> <h1>交互式数据可视化</h1> <div class="visualization-container"> <div class="chart-controls"> <button id="barChartBtn" class="active">柱状图</button> <button id="lineChartBtn">折线图</button> <button id="pieChartBtn">饼图</button> </div> <div class="chart-container"> <div id="barChart" class="bar-chart"> <!-- 柱状图将在这里动态生成 --> </div> <div id="lineChart" class="line-chart" style="display: none;"> <!-- 折线图将在这里动态生成 --> </div> <div id="pieChart" class="pie-chart" style="display: none;"> <!-- 饼图将在这里动态生成 --> </div> </div> <div id="chartLegend" class="chart-legend"> <!-- 图例将在这里动态生成 --> </div> <div class="data-controls"> <input type="text" id="labelInput" placeholder="标签"> <input type="number" id="valueInput" placeholder="值"> <button id="addDataBtn">添加数据</button> <button id="randomDataBtn">随机数据</button> <button id="clearDataBtn">清空数据</button> </div> </div> <div id="tooltip" class="tooltip"></div> <script> document.addEventListener('DOMContentLoaded', function() { // 获取DOM元素 const barChartBtn = document.getElementById('barChartBtn'); const lineChartBtn = document.getElementById('lineChartBtn'); const pieChartBtn = document.getElementById('pieChartBtn'); const barChart = document.getElementById('barChart'); const lineChart = document.getElementById('lineChart'); const pieChart = document.getElementById('pieChart'); const chartLegend = document.getElementById('chartLegend'); const tooltip = document.getElementById('tooltip'); const labelInput = document.getElementById('labelInput'); const valueInput = document.getElementById('valueInput'); const addDataBtn = document.getElementById('addDataBtn'); const randomDataBtn = document.getElementById('randomDataBtn'); const clearDataBtn = document.getElementById('clearDataBtn'); // 初始数据 let chartData = [ { label: '一月', value: 65 }, { label: '二月', value: 59 }, { label: '三月', value: 80 }, { label: '四月', value: 81 }, { label: '五月', value: 56 }, { label: '六月', value: 55 } ]; // 当前图表类型 let currentChartType = 'bar'; // 颜色数组 const colors = [ '#4CAF50', '#2196F3', '#FF9800', '#9C27B0', '#F44336', '#00BCD4', '#CDDC39', '#795548' ]; // 添加按钮点击事件 barChartBtn.addEventListener('click', () => switchChart('bar')); lineChartBtn.addEventListener('click', () => switchChart('line')); pieChartBtn.addEventListener('click', () => switchChart('pie')); addDataBtn.addEventListener('click', addData); randomDataBtn.addEventListener('click', generateRandomData); clearDataBtn.addEventListener('click', clearData); // 初始化图表 renderChart(); // 切换图表类型 function switchChart(type) { // 更新按钮状态 document.querySelectorAll('.chart-controls button').forEach(btn => { btn.classList.remove('active'); }); if (type === 'bar') { barChartBtn.classList.add('active'); barChart.style.display = 'flex'; lineChart.style.display = 'none'; pieChart.style.display = 'none'; } else if (type === 'line') { lineChartBtn.classList.add('active'); barChart.style.display = 'none'; lineChart.style.display = 'block'; pieChart.style.display = 'none'; } else if (type === 'pie') { pieChartBtn.classList.add('active'); barChart.style.display = 'none'; lineChart.style.display = 'none'; pieChart.style.display = 'flex'; } currentChartType = type; renderChart(); } // 渲染图表 function renderChart() { if (currentChartType === 'bar') { renderBarChart(); } else if (currentChartType === 'line') { renderLineChart(); } else if (currentChartType === 'pie') { renderPieChart(); } renderLegend(); } // 渲染柱状图 function renderBarChart() { // 清空现有内容 barChart.innerHTML = ''; // 找出最大值用于缩放 const maxValue = Math.max(...chartData.map(item => item.value)); // 为每个数据项创建柱状图 chartData.forEach((item, index) => { const bar = document.createElement('div'); bar.className = 'bar'; // 计算高度(相对于最大值的百分比) const height = (item.value / maxValue) * 80; // 80%的最大高度 bar.style.height = `${height}%`; bar.style.backgroundColor = colors[index % colors.length]; // 创建值标签 const valueLabel = document.createElement('div'); valueLabel.className = 'bar-value'; valueLabel.textContent = item.value; bar.appendChild(valueLabel); // 创建底部标签 const bottomLabel = document.createElement('div'); bottomLabel.className = 'bar-label'; bottomLabel.textContent = item.label; bar.appendChild(bottomLabel); // 添加鼠标事件 bar.addEventListener('mouseenter', function(e) { showTooltip(e, `${item.label}: ${item.value}`); }); bar.addEventListener('mouseleave', function() { hideTooltip(); }); // 添加到图表 barChart.appendChild(bar); }); } // 渲染折线图 function renderLineChart() { // 清空现有内容 lineChart.innerHTML = ''; // 创建SVG元素 const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 800 400'); // 设置边距 const margin = { top: 20, right: 30, bottom: 40, left: 50 }; const width = 800 - margin.left - margin.right; const height = 400 - margin.top - margin.bottom; // 创建主组 const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); g.setAttribute('transform', `translate(${margin.left},${margin.top})`); svg.appendChild(g); // 找出最大值和最小值用于缩放 const maxValue = Math.max(...chartData.map(item => item.value)); const minValue = Math.min(...chartData.map(item => item.value)); const valueRange = maxValue - minValue; // 创建X轴和Y轴 const xAxis = document.createElementNS('http://www.w3.org/2000/svg', 'line'); xAxis.setAttribute('x1', 0); xAxis.setAttribute('y1', height); xAxis.setAttribute('x2', width); xAxis.setAttribute('y2', height); xAxis.setAttribute('stroke', '#ccc'); g.appendChild(xAxis); const yAxis = document.createElementNS('http://www.w3.org/2000/svg', 'line'); yAxis.setAttribute('x1', 0); yAxis.setAttribute('y1', 0); yAxis.setAttribute('x2', 0); yAxis.setAttribute('y2', height); yAxis.setAttribute('stroke', '#ccc'); g.appendChild(yAxis); // 创建折线路径 let pathData = ''; const xStep = width / (chartData.length - 1); // 创建数据点 chartData.forEach((item, index) => { const x = index * xStep; const y = height - ((item.value - minValue) / valueRange) * height; if (index === 0) { pathData += `M ${x} ${y}`; } else { pathData += ` L ${x} ${y}`; } // 创建数据点圆圈 const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', x); circle.setAttribute('cy', y); circle.setAttribute('r', 5); circle.setAttribute('fill', colors[index % colors.length]); // 添加鼠标事件 circle.addEventListener('mouseenter', function(e) { showTooltip(e, `${item.label}: ${item.value}`); }); circle.addEventListener('mouseleave', function() { hideTooltip(); }); g.appendChild(circle); // 添加X轴标签 const xLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); xLabel.setAttribute('x', x); xLabel.setAttribute('y', height + 20); xLabel.setAttribute('text-anchor', 'middle'); xLabel.setAttribute('font-size', '12'); xLabel.textContent = item.label; g.appendChild(xLabel); }); // 创建折线 const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', pathData); path.setAttribute('fill', 'none'); path.setAttribute('stroke', '#2196F3'); path.setAttribute('stroke-width', '2'); g.appendChild(path); // 添加Y轴标签 for (let i = 0; i <= 5; i++) { const value = minValue + (valueRange * i / 5); const y = height - (i / 5) * height; const yLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); yLabel.setAttribute('x', -10); yLabel.setAttribute('y', y); yLabel.setAttribute('text-anchor', 'end'); yLabel.setAttribute('dominant-baseline', 'middle'); yLabel.setAttribute('font-size', '12'); yLabel.textContent = Math.round(value); g.appendChild(yLabel); // 添加网格线 const gridLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); gridLine.setAttribute('x1', 0); gridLine.setAttribute('y1', y); gridLine.setAttribute('x2', width); gridLine.setAttribute('y2', y); gridLine.setAttribute('stroke', '#eee'); gridLine.setAttribute('stroke-dasharray', '2,2'); g.appendChild(gridLine); } // 添加SVG到容器 lineChart.appendChild(svg); } // 渲染饼图 function renderPieChart() { // 清空现有内容 pieChart.innerHTML = ''; // 创建SVG元素 const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 400 400'); // 计算总值 const total = chartData.reduce((sum, item) => sum + item.value, 0); // 设置饼图参数 const centerX = 200; const centerY = 200; const radius = 150; let startAngle = 0; // 为每个数据项创建扇形 chartData.forEach((item, index) => { // 计算角度 const angle = (item.value / total) * 2 * Math.PI; const endAngle = startAngle + angle; // 计算起点和终点坐标 const x1 = centerX + radius * Math.cos(startAngle); const y1 = centerY + radius * Math.sin(startAngle); const x2 = centerX + radius * Math.cos(endAngle); const y2 = centerY + radius * Math.sin(endAngle); // 确定是否为大弧 const largeArc = angle > Math.PI ? 1 : 0; // 创建路径 const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); const pathData = `M ${centerX} ${centerY} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`; path.setAttribute('d', pathData); path.setAttribute('fill', colors[index % colors.length]); path.setAttribute('stroke', 'white'); path.setAttribute('stroke-width', '2'); // 添加鼠标事件 path.addEventListener('mouseenter', function(e) { showTooltip(e, `${item.label}: ${item.value} (${((item.value / total) * 100).toFixed(1)}%)`); }); path.addEventListener('mouseleave', function() { hideTooltip(); }); svg.appendChild(path); // 更新起始角度 startAngle = endAngle; }); // 添加SVG到容器 pieChart.appendChild(svg); } // 渲染图例 function renderLegend() { // 清空现有内容 chartLegend.innerHTML = ''; // 为每个数据项创建图例项 chartData.forEach((item, index) => { const legendItem = document.createElement('div'); legendItem.className = 'legend-item'; const colorBox = document.createElement('div'); colorBox.className = 'legend-color'; colorBox.style.backgroundColor = colors[index % colors.length]; const label = document.createElement('span'); label.textContent = `${item.label}: ${item.value}`; legendItem.appendChild(colorBox); legendItem.appendChild(label); chartLegend.appendChild(legendItem); }); } // 显示工具提示 function showTooltip(event, text) { tooltip.textContent = text; tooltip.style.left = `${event.pageX + 10}px`; tooltip.style.top = `${event.pageY - 30}px`; tooltip.classList.add('active'); } // 隐藏工具提示 function hideTooltip() { tooltip.classList.remove('active'); } // 添加数据 function addData() { const label = labelInput.value.trim(); const value = parseFloat(valueInput.value); if (label && !isNaN(value)) { chartData.push({ label, value }); renderChart(); // 清空输入 labelInput.value = ''; valueInput.value = ''; } } // 生成随机数据 function generateRandomData() { const months = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']; const newData = []; // 随机选择3-8个月 const count = Math.floor(Math.random() * 6) + 3; const selectedMonths = []; while (selectedMonths.length < count) { const month = months[Math.floor(Math.random() * months.length)]; if (!selectedMonths.includes(month)) { selectedMonths.push(month); newData.push({ label: month, value: Math.floor(Math.random() * 100) + 1 }); } } chartData = newData; renderChart(); } // 清空数据 function clearData() { chartData = []; renderChart(); } }); </script> </body> </html> 

这个交互式数据可视化组件展示了以下DOM操作技术:

  1. 动态SVG创建:使用DOM API动态创建SVG元素来绘制折线图和饼图。
  2. 图表切换:根据用户选择动态切换不同类型的图表。
  3. 数据绑定:将数据绑定到DOM元素,并在数据变化时更新视图。
  4. 交互功能:添加鼠标悬停效果和工具提示。
  5. 动态内容管理:允许用户添加、随机生成和清空数据。

高级主题

Shadow DOM

Shadow DOM是Web Components技术的一部分,它允许将封装的DOM树附加到元素上,并与主文档DOM保持隔离。这有助于创建可重用的组件,避免样式冲突和意外的DOM操作。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Shadow DOM示例</title> <style> body { font-family: 'Arial', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .container { border: 1px solid #ddd; padding: 20px; border-radius: 5px; margin-bottom: 20px; } /* 这些样式不会影响Shadow DOM内部 */ p { color: blue; } </style> </head> <body> <h1>Shadow DOM示例</h1> <div class="container"> <h2>普通DOM</h2> <p>这是普通DOM中的段落,会应用页面中的样式。</p> </div> <div class="container"> <h2>Shadow DOM</h2> <div id="shadowHost">这是Shadow Host元素</div> </div> <div class="container"> <h2>自定义元素与Shadow DOM</h2> <custom-card heading="自定义卡片" content="这是一个使用Shadow DOM的自定义元素。"></custom-card> </div> <script> // 创建基本的Shadow DOM const shadowHost = document.getElementById('shadowHost'); const shadowRoot = shadowHost.attachShadow({ mode: 'open' }); // 添加Shadow DOM内容 shadowRoot.innerHTML = ` <style> p { color: red; font-weight: bold; } .container { border: 1px solid #ccc; padding: 10px; border-radius: 3px; background-color: #f9f9f9; } </style> <div class="container"> <p>这是Shadow DOM中的段落,不会应用页面中的样式。</p> <p>Shadow DOM提供了样式封装,避免了样式冲突。</p> </div> `; // 创建自定义元素 class CustomCard extends HTMLElement { constructor() { super(); // 创建Shadow DOM const shadow = this.attachShadow({ mode: 'open' }); // 获取属性 const heading = this.getAttribute('heading') || '默认标题'; const content = this.getAttribute('content') || '默认内容'; // 创建Shadow DOM内容 shadow.innerHTML = ` <style> .card { border: 1px solid #ddd; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; width: 100%; max-width: 300px; } .card-header { background-color: #4CAF50; color: white; padding: 10px 15px; font-weight: bold; } .card-body { padding: 15px; } .card-footer { background-color: #f1f1f1; padding: 10px 15px; text-align: right; } button { background-color: #2196F3; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer; } button:hover { background-color: #0b7dda; } </style> <div class="card"> <div class="card-header">${heading}</div> <div class="card-body"> <p>${content}</p> </div> <div class="card-footer"> <button id="actionBtn">操作</button> </div> </div> `; // 添加事件监听器 const actionBtn = shadow.querySelector('#actionBtn'); actionBtn.addEventListener('click', () => { alert('按钮被点击了!'); }); } } // 注册自定义元素 customElements.define('custom-card', CustomCard); </script> </body> </html> 

Web Components

Web Components是一组Web平台API,允许创建可重用的自定义元素,它们的行为与HTML元素隔离。Web Components主要由三项技术组成:

  1. Custom Elements:允许定义自定义HTML元素及其行为。
  2. Shadow DOM:提供封装,将元素的样式、标记和行为隐藏在单独的DOM中。
  3. HTML Templates:提供声明式标记模板,不会在页面加载时渲染,但可以在运行时实例化。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Web Components示例</title> <style> body { font-family: 'Arial', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .container { border: 1px solid #ddd; padding: 20px; border-radius: 5px; margin-bottom: 20px; } </style> </head> <body> <h1>Web Components示例</h1> <div class="container"> <h2>用户卡片组件</h2> <user-card name="张三" email="zhangsan@example.com" avatar="https://picsum.photos/seed/user1/100/100.jpg"> </user-card> <user-card name="李四" email="lisi@example.com" avatar="https://picsum.photos/seed/user2/100/100.jpg"> </user-card> </div> <div class="container"> <h2>待办事项列表组件</h2> <todo-list></todo-list> </div> <div class="container"> <h2>选项卡组件</h2> <tab-container> <tab-panel title="选项卡1"> <p>这是选项卡1的内容。</p> </tab-panel> <tab-panel title="选项卡2"> <p>这是选项卡2的内容。</p> </tab-panel> <tab-panel title="选项卡3"> <p>这是选项卡3的内容。</p> </tab-panel> </tab-container> </div> <!-- 定义模板 --> <template id="user-card-template"> <style> .card { display: flex; align-items: center; border: 1px solid #ddd; border-radius: 5px; padding: 15px; margin-bottom: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .avatar { width: 80px; height: 80px; border-radius: 50%; margin-right: 15px; object-fit: cover; } .info { flex: 1; } .name { font-weight: bold; font-size: 18px; margin-bottom: 5px; } .email { color: #666; } .actions { display: flex; gap: 10px; } button { padding: 5px 10px; border: none; border-radius: 3px; cursor: pointer; } .edit-btn { background-color: #2196F3; color: white; } .delete-btn { background-color: #F44336; color: white; } </style> <div class="card"> <img class="avatar" src="" alt="用户头像"> <div class="info"> <div class="name"></div> <div class="email"></div> </div> <div class="actions"> <button class="edit-btn">编辑</button> <button class="delete-btn">删除</button> </div> </div> </template> <template id="todo-list-template"> <style> .todo-container { max-width: 500px; margin: 0 auto; } .todo-input-container { display: flex; margin-bottom: 20px; } .todo-input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 3px 0 0 3px; font-size: 16px; } .add-btn { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; border-radius: 0 3px 3px 0; cursor: pointer; } .todo-list { list-style: none; padding: 0; } .todo-item { display: flex; align-items: center; padding: 10px; border-bottom: 1px solid #eee; } .todo-item:last-child { border-bottom: none; } .todo-checkbox { margin-right: 10px; } .todo-text { flex: 1; } .todo-item.completed .todo-text { text-decoration: line-through; color: #999; } .delete-btn { background-color: #F44336; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer; } .todo-stats { margin-top: 20px; text-align: center; color: #666; } </style> <div class="todo-container"> <div class="todo-input-container"> <input type="text" class="todo-input" placeholder="添加新的待办事项..."> <button class="add-btn">添加</button> </div> <ul class="todo-list"></ul> <div class="todo-stats"></div> </div> </template> <template id="tab-container-template"> <style> .tabs { display: flex; border-bottom: 1px solid #ddd; } .tab { padding: 10px 15px; cursor: pointer; border: 1px solid transparent; border-bottom: none; border-radius: 3px 3px 0 0; margin-right: 5px; background-color: #f1f1f1; } .tab.active { background-color: white; border-color: #ddd; border-bottom-color: white; margin-bottom: -1px; font-weight: bold; } .tab-panels { padding: 15px; border: 1px solid #ddd; border-top: none; } .tab-panel { display: none; } .tab-panel.active { display: block; } </style> <div class="tabs"></div> <div class="tab-panels"></div> </template> <script> // 用户卡片组件 class UserCard extends HTMLElement { constructor() { super(); // 获取模板 const template = document.getElementById('user-card-template'); const templateContent = template.content; // 创建Shadow DOM const shadowRoot = this.attachShadow({ mode: 'open' }); // 克隆模板内容 shadowRoot.appendChild(templateContent.cloneNode(true)); // 获取属性 const name = this.getAttribute('name') || '未知用户'; const email = this.getAttribute('email') || ''; const avatar = this.getAttribute('avatar') || 'https://picsum.photos/seed/default/100/100.jpg'; // 设置内容 shadowRoot.querySelector('.avatar').src = avatar; shadowRoot.querySelector('.name').textContent = name; shadowRoot.querySelector('.email').textContent = email; // 添加事件监听器 const editBtn = shadowRoot.querySelector('.edit-btn'); const deleteBtn = shadowRoot.querySelector('.delete-btn'); editBtn.addEventListener('click', () => { alert(`编辑用户: ${name}`); }); deleteBtn.addEventListener('click', () => { if (confirm(`确定要删除用户 ${name} 吗?`)) { this.remove(); } }); } } // 待办事项列表组件 class TodoList extends HTMLElement { constructor() { super(); // 获取模板 const template = document.getElementById('todo-list-template'); const templateContent = template.content; // 创建Shadow DOM const shadowRoot = this.attachShadow({ mode: 'open' }); // 克隆模板内容 shadowRoot.appendChild(templateContent.cloneNode(true)); // 获取元素 this.todoInput = shadowRoot.querySelector('.todo-input'); this.addBtn = shadowRoot.querySelector('.add-btn'); this.todoList = shadowRoot.querySelector('.todo-list'); this.todoStats = shadowRoot.querySelector('.todo-stats'); // 初始化待办事项数组 this.todos = []; // 添加事件监听器 this.addBtn.addEventListener('click', () => this.addTodo()); this.todoInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { this.addTodo(); } }); // 初始渲染 this.updateStats(); } addTodo() { const text = this.todoInput.value.trim(); if (!text) return; const todo = { id: Date.now(), text: text, completed: false }; this.todos.push(todo); this.renderTodo(todo); this.updateStats(); // 清空输入 this.todoInput.value = ''; } renderTodo(todo) { const li = document.createElement('li'); li.className = 'todo-item'; li.dataset.id = todo.id; if (todo.completed) { li.classList.add('completed'); } li.innerHTML = ` <input type="checkbox" class="todo-checkbox" ${todo.completed ? 'checked' : ''}> <span class="todo-text">${todo.text}</span> <button class="delete-btn">删除</button> `; // 添加事件监听器 const checkbox = li.querySelector('.todo-checkbox'); const deleteBtn = li.querySelector('.delete-btn'); checkbox.addEventListener('change', () => { todo.completed = checkbox.checked; li.classList.toggle('completed', todo.completed); this.updateStats(); }); deleteBtn.addEventListener('click', () => { this.todos = this.todos.filter(t => t.id !== todo.id); li.remove(); this.updateStats(); }); this.todoList.appendChild(li); } updateStats() { const total = this.todos.length; const completed = this.todos.filter(todo => todo.completed).length; const remaining = total - completed; this.todoStats.textContent = `总计: ${total} | 已完成: ${completed} | 未完成: ${remaining}`; } } // 选项卡容器组件 class TabContainer extends HTMLElement { constructor() { super(); // 获取模板 const template = document.getElementById('tab-container-template'); const templateContent = template.content; // 创建Shadow DOM const shadowRoot = this.attachShadow({ mode: 'open' }); // 克隆模板内容 shadowRoot.appendChild(templateContent.cloneNode(true)); // 获取元素 this.tabsContainer = shadowRoot.querySelector('.tabs'); this.panelsContainer = shadowRoot.querySelector('.tab-panels'); // 初始化 this.tabs = []; this.panels = []; this.activeIndex = 0; } connectedCallback() { // 获取所有选项卡面板 this.panels = Array.from(this.querySelectorAll('tab-panel')); // 为每个面板创建选项卡 this.panels.forEach((panel, index) => { const title = panel.getAttribute('title') || `选项卡 ${index + 1}`; // 创建选项卡 const tab = document.createElement('div'); tab.className = 'tab'; tab.textContent = title; tab.dataset.index = index; // 添加点击事件 tab.addEventListener('click', () => { this.setActiveTab(index); }); this.tabsContainer.appendChild(tab); this.tabs.push(tab); // 设置面板ID panel.id = `panel-${index}`; // 克隆面板内容到Shadow DOM const panelClone = document.createElement('div'); panelClone.className = 'tab-panel'; panelClone.id = `shadow-panel-${index}`; panelClone.innerHTML = panel.innerHTML; this.panelsContainer.appendChild(panelClone); }); // 激活第一个选项卡 if (this.tabs.length > 0) { this.setActiveTab(0); } } setActiveTab(index) { if (index < 0 || index >= this.tabs.length) return; // 更新活动状态 this.tabs.forEach((tab, i) => { tab.classList.toggle('active', i === index); }); const shadowPanels = this.panelsContainer.querySelectorAll('.tab-panel'); shadowPanels.forEach((panel, i) => { panel.classList.toggle('active', i === index); }); this.activeIndex = index; // 触发自定义事件 this.dispatchEvent(new CustomEvent('tab-change', { detail: { index, title: this.panels[index].getAttribute('title') } })); } } // 选项卡面板组件 class TabPanel extends HTMLElement { constructor() { super(); } } // 注册自定义元素 customElements.define('user-card', UserCard); customElements.define('todo-list', TodoList); customElements.define('tab-container', TabContainer); customElements.define('tab-panel', TabPanel); </script> </body> </html> 

与现代框架的集成

虽然现代前端框架(如React、Vue、Angular)提供了自己的抽象来处理DOM操作,但了解原生DOM操作仍然很重要,特别是在以下场景:

  1. 性能优化:直接操作DOM有时比框架的虚拟DOM更高效。
  2. 第三方库集成:许多第三方库直接操作DOM。
  3. 框架之外的交互:有时需要在框架之外与DOM交互。

以下是一个展示如何在React中集成原生DOM操作的例子:

import React, { useRef, useEffect, useState } from 'react'; function DOMIntegrationExample() { const [items, setItems] = useState([ { id: 1, text: '项目1' }, { id: 2, text: '项目2' }, { id: 3, text: '项目3' } ]); const listRef = useRef(null); const newItemRef = useRef(null); // 使用原生DOM添加项目 const addItemWithDOM = () => { if (!listRef.current) return; const newItemId = Date.now(); const newItemText = `项目${items.length + 1}`; // 创建新列表项 const li = document.createElement('li'); li.className = 'list-item'; li.dataset.id = newItemId; li.textContent = newItemText; // 添加点击事件 li.addEventListener('click', function() { alert(`点击了: ${this.textContent}`); }); // 添加到列表 listRef.current.appendChild(li); // 更新React状态 setItems(prev => [...prev, { id: newItemId, text: newItemText }]); }; // 使用React方式添加项目 const addItemWithReact = () => { const newItemId = Date.now(); const newItemText = `项目${items.length + 1}`; setItems(prev => [...prev, { id: newItemId, text: newItemText }]); }; // 使用原生DOM删除项目 const removeItemWithDOM = (id) => { if (!listRef.current) return; const itemToRemove = listRef.current.querySelector(`li[data-id="${id}"]`); if (itemToRemove) { itemToRemove.remove(); } // 更新React状态 setItems(prev => prev.filter(item => item.id !== id)); }; // 使用原生DOM滚动到新项目 useEffect(() => { if (newItemRef.current) { newItemRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [items]); return ( <div className="dom-integration"> <h2>DOM集成示例</h2> <div className="controls"> <button onClick={addItemWithReact}>使用React添加项目</button> <button onClick={addItemWithDOM}>使用DOM添加项目</button> </div> <ul ref={listRef} className="item-list"> {items.map((item, index) => ( <li key={item.id} data-id={item.id} ref={index === items.length - 1 ? newItemRef : null} className="list-item" onClick={() => alert(`点击了: ${item.text}`)} > {item.text} <button className="delete-btn" onClick={(e) => { e.stopPropagation(); removeItemWithDOM(item.id); }} > 删除 </button> </li> ))} </ul> </div> ); } export default DOMIntegrationExample; 

最佳实践和常见陷阱

最佳实践

  1. 最小化DOM操作: “javascript // 不好的做法:多次修改DOM for (let i = 0; i < 100; i++) { document.getElementById('list').innerHTML +=
  2. 项目 ${i}
  3. `; }

// 好的做法:使用文档片段 const fragment = document.createDocumentFragment(); for (let i = 0; i < 100; i++) {

 const li = document.createElement('li'); li.textContent = `项目 ${i}`; fragment.appendChild(li); 

} document.getElementById(‘list’).appendChild(fragment);

 2. **使用事件委托**: ```javascript // 不好的做法:为每个项目添加事件监听器 document.querySelectorAll('.item').forEach(item => { item.addEventListener('click', function() { console.log('项目被点击'); }); }); // 好的做法:使用事件委托 document.getElementById('itemContainer').addEventListener('click', function(e) { if (e.target.classList.contains('item')) { console.log('项目被点击'); } }); 
  1. 缓存DOM查询: “`javascript // 不好的做法:重复查询DOM function updateItems() { const items = document.querySelectorAll(‘.item’); items.forEach(item => {

     item.classList.add('updated'); 

    });

    // 其他操作…

    const itemsAgain = document.querySelectorAll(‘.item’); itemsAgain.forEach(item => {

     item.style.color = 'blue'; 

    }); }

// 好的做法:缓存DOM查询结果 function updateItems() {

 const items = document.querySelectorAll('.item'); items.forEach(item => { item.classList.add('updated'); }); // 其他操作... items.forEach(item => { item.style.color = 'blue'; }); 

}

 4. **使用语义化HTML**: ```html <!-- 不好的做法:使用非语义化标签 --> <div id="header"> <div id="nav"> <div class="nav-item">首页</div> <div class="nav-item">关于</div> </div> </div> <!-- 好的做法:使用语义化标签 --> <header> <nav> <a href="#" class="nav-item">首页</a> <a href="#" class="nav-item">关于</a> </nav> </header> 
  1. 分离关注点: “`javascript // 不好的做法:混合HTML、CSS和JavaScript const button = document.createElement(‘button’); button.textContent = ‘点击我’; button.style.color = ‘white’; button.style.backgroundColor = ‘blue’; button.style.padding = ‘10px’; button.addEventListener(‘click’, function() { alert(‘按钮被点击’); }); document.body.appendChild(button);

// 好的做法:分离关注点 // HTML结构 const button = document.createElement(‘button’); button.className = ‘btn-primary’; button.textContent = ‘点击我’; document.body.appendChild(button);

// CSS样式(在样式表中) /* .btn-primary {

 color: white; background-color: blue; padding: 10px; 

} */

// JavaScript行为 button.addEventListener(‘click’, function() {

 alert('按钮被点击'); 

});

 ### 常见陷阱 1. **内存泄漏**: ```javascript // 不好的做法:没有清理事件监听器,导致内存泄漏 function setupButtons() { const buttons = document.querySelectorAll('.btn'); buttons.forEach(button => { button.addEventListener('click', handleClick); }); } // 好的做法:在不需要时清理事件监听器 function setupButtons() { const buttons = document.querySelectorAll('.btn'); const handlers = []; buttons.forEach(button => { const handler = function() { handleClick(button); }; button.addEventListener('click', handler); handlers.push({ button, handler }); }); // 返回清理函数 return function cleanup() { handlers.forEach(({ button, handler }) => { button.removeEventListener('click', handler); }); }; } // 使用示例 const cleanup = setupButtons(); // 当不再需要时 // cleanup(); 
  1. 布局抖动: “`javascript // 不好的做法:导致布局抖动 function updateElements() { const elements = document.querySelectorAll(‘.element’); elements.forEach(element => {

     // 读取样式(强制回流) const width = element.offsetWidth; // 写入样式(强制回流) element.style.height = (width * 2) + 'px'; 

    }); }

// 好的做法:批量读取和写入,避免布局抖动 function updateElements() {

 const elements = document.querySelectorAll('.element'); // 先读取所有样式 const widths = Array.from(elements).map(element => element.offsetWidth); // 然后写入所有样式 elements.forEach((element, index) => { element.style.height = (widths[index] * 2) + 'px'; }); 

}

 3. **过度使用innerHTML**: ```javascript // 不好的做法:使用innerHTML可能导致XSS攻击 function setUserContent(content) { document.getElementById('userContent').innerHTML = content; } // 好的做法:使用textContent或创建DOM元素 function setUserContent(content) { // 如果确定内容是纯文本 document.getElementById('userContent').textContent = content; // 如果内容需要包含HTML,确保它是安全的 const div = document.createElement('div'); div.textContent = content; // 转义HTML document.getElementById('userContent').innerHTML = div.innerHTML; } 
  1. 忽略浏览器兼容性: “`javascript // 不好的做法:不考虑浏览器兼容性 const elements = document.querySelectorAll(‘.my-class’); elements.forEach(element => { element.classList.add(‘active’); });

// 好的做法:考虑浏览器兼容性 const elements = document.querySelectorAll(‘.my-class’);

// 检查forEach是否可用 if (NodeList.prototype.forEach) {

 elements.forEach(element => { element.classList.add('active'); }); 

} else {

 // 回退方案 for (let i = 0; i < elements.length; i++) { elements[i].classList.add('active'); } 

}

 5. **同步操作阻塞UI**: ```javascript // 不好的做法:长时间运行的同步操作阻塞UI function processLargeData(data) { for (let i = 0; i < data.length; i++) { // 处理每个数据项 processDataItem(data[i]); // 更新UI document.getElementById('progress').textContent = `处理进度: ${i + 1}/${data.length}`; } } // 好的做法:使用异步操作避免阻塞UI function processLargeData(data) { let i = 0; function processChunk() { const chunkSize = 100; // 每次处理的数据项数量 const end = Math.min(i + chunkSize, data.length); for (; i < end; i++) { // 处理每个数据项 processDataItem(data[i]); } // 更新UI document.getElementById('progress').textContent = `处理进度: ${i}/${data.length}`; // 如果还有数据要处理,继续下一块 if (i < data.length) { setTimeout(processChunk, 0); // 让UI有机会更新 } } // 开始处理 processChunk(); } 

结论和进一步学习资源

HTML DOM是前端开发的核心技术,掌握DOM操作对于构建现代、交互式网页应用至关重要。通过本教程,我们从基础概念开始,逐步深入到高级主题,并通过实际案例分析展示了如何应用这些技术来构建复杂的页面结构。

关键要点回顾

  1. DOM基础:DOM是HTML和XML文档的编程接口,它将文档表示为节点树。
  2. DOM操作:包括选择元素、修改内容、创建和删除元素等基本操作。
  3. 高级技术:如事件处理、动态内容加载、性能优化等。
  4. 现代Web技术:如Shadow DOM和Web Components,提供了封装和重用的能力。
  5. 最佳实践:最小化DOM操作、使用事件委托、缓存DOM查询等。
  6. 常见陷阱:如内存泄漏、布局抖动、过度使用innerHTML等。

进一步学习资源

  1. MDN Web Docs

    • Document Object Model (DOM)
    • Web APIs
  2. 书籍

    • 《JavaScript高级程序设计》(第4版)- Nicholas C. Zakas
    • 《DOM编程艺术》 - Jeremy Keith
    • 《JavaScript DOM编程艺术》(第2版)- Jeremy Keith, Jeffrey Sambells
  3. 在线课程

    • JavaScript DOM Manipulation - Udemy
    • JavaScript: Understanding the Weird Parts - Udemy
  4. 实践项目

    • 构建一个交互式待办事项应用
    • 开发一个动态数据可视化仪表板
    • 创建一个自定义Web组件库
  5. 性能优化工具

    • Chrome DevTools
    • Lighthouse
    • WebPageTest

通过不断实践和学习,你将能够熟练掌握HTML DOM操作,并能够构建出高性能、可维护的现代Web应用。记住,DOM操作是前端开发的基础技能,无论你使用什么框架或库,深入理解DOM都将使你成为更优秀的前端开发者。