|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
1. DOM基础概念
文档对象模型(Document Object Model,简称DOM)是HTML和XML文档的编程接口,它将文档表示为一个节点树,允许程序和脚本动态地访问和更新文档的内容、结构和样式。DOM是前端开发的核心技术之一,理解并熟练掌握DOM操作对于构建交互式网页至关重要。
在DOM中,每个HTML元素都被视为一个节点,整个HTML文档构成一个树形结构。文档节点是树的根节点,每个HTML元素是一个元素节点,元素内的文本是文本节点,元素的属性是属性节点。
- <!DOCTYPE html>
- <html>
- <head>
- <title>DOM示例</title>
- </head>
- <body>
- <h1>DOM树形结构</h1>
- <p>这是一个段落。</p>
- </body>
- </html>
复制代码
在上述HTML结构中,DOM树形结构如下:
• 文档节点(Document)html元素节点head元素节点title元素节点“DOM示例”文本节点body元素节点h1元素节点“DOM树形结构”文本节点p元素节点“这是一个段落。”文本节点
• html元素节点head元素节点title元素节点“DOM示例”文本节点body元素节点h1元素节点“DOM树形结构”文本节点p元素节点“这是一个段落。”文本节点
• head元素节点title元素节点“DOM示例”文本节点
• title元素节点
• “DOM示例”文本节点
• body元素节点h1元素节点“DOM树形结构”文本节点p元素节点“这是一个段落。”文本节点
• h1元素节点
• “DOM树形结构”文本节点
• p元素节点
• “这是一个段落。”文本节点
• html元素节点head元素节点title元素节点“DOM示例”文本节点body元素节点h1元素节点“DOM树形结构”文本节点p元素节点“这是一个段落。”文本节点
• head元素节点title元素节点“DOM示例”文本节点
• title元素节点
• “DOM示例”文本节点
• body元素节点h1元素节点“DOM树形结构”文本节点p元素节点“这是一个段落。”文本节点
• h1元素节点
• “DOM树形结构”文本节点
• p元素节点
• “这是一个段落。”文本节点
• head元素节点title元素节点“DOM示例”文本节点
• title元素节点
• “DOM示例”文本节点
• body元素节点h1元素节点“DOM树形结构”文本节点p元素节点“这是一个段落。”文本节点
• h1元素节点
• “DOM树形结构”文本节点
• p元素节点
• “这是一个段落。”文本节点
• title元素节点
• “DOM示例”文本节点
• h1元素节点
• “DOM树形结构”文本节点
• p元素节点
• “这是一个段落。”文本节点
2. 基础选择器
2.1 getElementById
getElementById是最基础也是最常用的DOM查询方法,它通过元素的ID属性返回对应的元素节点。由于ID在HTML文档中是唯一的,所以这个方法总是返回单个元素或null(如果不存在)。
- // 获取ID为"header"的元素
- const headerElement = document.getElementById('header');
- // 修改元素内容
- if (headerElement) {
- headerElement.textContent = '新的标题';
- }
复制代码
2.2 getElementsByTagName
getElementsByTagName方法通过元素的标签名返回一个HTMLCollection(类似数组的集合),包含文档中所有匹配的元素。
- // 获取所有的段落元素
- const paragraphs = document.getElementsByTagName('p');
- // 遍历所有段落并修改样式
- for (let i = 0; i < paragraphs.length; i++) {
- paragraphs[i].style.color = 'blue';
- }
- // 注意:HTMLCollection是动态的,当文档变化时会自动更新
- // 这意味着如果你在循环中添加新的p元素,循环可能会无限进行
- // 解决方法是将HTMLCollection转换为数组
- const paragraphsArray = Array.from(paragraphs);
复制代码
2.3 getElementsByClassName
getElementsByClassName方法通过元素的class属性返回一个HTMLCollection,包含文档中所有具有指定类名的元素。
- // 获取所有类名为"highlight"的元素
- const highlightedElements = document.getElementsByClassName('highlight');
- // 为所有高亮元素添加边框
- Array.from(highlightedElements).forEach(element => {
- element.style.border = '1px solid yellow';
- });
复制代码
2.4 getElementsByName
getElementsByName方法通过元素的name属性返回一个NodeList,包含文档中所有具有指定名称的元素。这个方法主要用于表单元素。
- // 获取所有name为"gender"的元素(通常是单选按钮)
- const genderInputs = document.getElementsByName('gender');
- // 检查哪个单选按钮被选中
- let selectedGender;
- for (let i = 0; i < genderInputs.length; i++) {
- if (genderInputs[i].checked) {
- selectedGender = genderInputs[i].value;
- break;
- }
- }
- console.log('选中的性别是:', selectedGender);
复制代码
3. 高级选择器
3.1 querySelector
querySelector方法接受一个CSS选择器字符串,返回文档中匹配该选择器的第一个元素。如果没有匹配的元素,则返回null。
- // 获取第一个类名为"container"的div元素
- const container = document.querySelector('div.container');
- // 获取ID为"main"的元素下的第一个p元素
- const firstParagraph = document.querySelector('#main p');
- // 获取具有data-id属性且值为"123"的元素
- const specificElement = document.querySelector('[data-id="123"]');
复制代码
3.2 querySelectorAll
querySelectorAll方法与querySelector类似,但它返回一个NodeList,包含文档中所有匹配该选择器的元素。NodeList不是动态的,它是一个快照,即使文档发生变化也不会更新。
- // 获取所有类名为"item"的li元素
- const items = document.querySelectorAll('li.item');
- // 为所有项目添加点击事件
- items.forEach(item => {
- item.addEventListener('click', function() {
- console.log('点击了项目:', this.textContent);
- });
- });
- // 使用更复杂的选择器
- // 获取类名为"active"的div元素下的所有直接子元素p
- const activeParagraphs = document.querySelectorAll('div.active > p');
复制代码
3.3 matches方法
matches方法检查元素是否匹配指定的CSS选择器,返回true或false。这个方法对于元素验证非常有用。
- // 检查元素是否具有特定类
- const element = document.querySelector('#myElement');
- if (element.matches('.highlight')) {
- console.log('元素具有highlight类');
- }
- // 在事件处理中使用matches
- document.addEventListener('click', function(event) {
- // 检查点击的元素是否是按钮
- if (event.target.matches('button')) {
- console.log('点击了按钮');
- }
- });
复制代码
3.4 closest方法
closest方法从当前元素开始,沿DOM树向上查找匹配指定CSS选择器的最近的祖先元素(包括当前元素本身)。如果没有找到,则返回null。
- // 查找最近的具有"container"类的祖先元素
- const element = document.querySelector('.item');
- const container = element.closest('.container');
- if (container) {
- console.log('找到了容器元素');
- }
- // 在事件委托中使用closest
- document.addEventListener('click', function(event) {
- // 查找最近的具有"delete-button"类的元素
- const deleteButton = event.target.closest('.delete-button');
-
- if (deleteButton) {
- const itemToDelete = deleteButton.closest('.item');
- if (itemToDelete) {
- itemToDelete.remove();
- console.log('删除了项目');
- }
- }
- });
复制代码
4. DOM节点关系与遍历
4.1 节点类型
在DOM中,有几种不同类型的节点,每种类型都有一个对应的数值常量:
- // 节点类型常量
- console.log(Node.ELEMENT_NODE); // 1 - 元素节点
- console.log(Node.TEXT_NODE); // 3 - 文本节点
- console.log(Node.COMMENT_NODE); // 8 - 注释节点
- console.log(Node.DOCUMENT_NODE); // 9 - 文档节点
- console.log(Node.DOCUMENT_FRAGMENT_NODE); // 11 - 文档片段节点
- // 检查节点类型
- const element = document.querySelector('div');
- console.log(element.nodeType === Node.ELEMENT_NODE); // true
复制代码
4.2 节点关系属性
DOM节点提供了多个属性来访问相关节点:
- // 获取元素
- const element = document.querySelector('#myElement');
- // 父节点
- const parent = element.parentNode; // 或 parentElement
- // 子节点
- const firstChild = element.firstChild; // 第一个子节点(可能是文本节点)
- const lastChild = element.lastChild; // 最后一个子节点(可能是文本节点)
- const childNodes = element.childNodes; // 所有子节点的NodeList
- // 兄弟节点
- const previousSibling = element.previousSibling; // 前一个兄弟节点(可能是文本节点)
- const nextSibling = element.nextSibling; // 后一个兄弟节点(可能是文本节点)
- // 只考虑元素节点的属性
- const firstElementChild = element.firstElementChild; // 第一个元素子节点
- const lastElementChild = element.lastElementChild; // 最后一个元素子节点
- const previousElementSibling = element.previousElementSibling; // 前一个元素兄弟节点
- const nextElementSibling = element.nextElementSibling; // 后一个元素兄弟节点
- const children = element.children; // 所有元素子节点的HTMLCollection
复制代码
4.3 遍历DOM树
使用节点关系属性,我们可以编写函数来遍历DOM树:
- // 深度优先遍历DOM树
- function traverseDOM(node, callback) {
- // 处理当前节点
- if (callback) {
- callback(node);
- }
-
- // 递归处理子节点
- for (let i = 0; i < node.childNodes.length; i++) {
- traverseDOM(node.childNodes[i], callback);
- }
- }
- // 使用示例
- traverseDOM(document.body, function(node) {
- if (node.nodeType === Node.ELEMENT_NODE) {
- console.log('元素节点:', node.tagName.toLowerCase());
- }
- });
- // 广度优先遍历DOM树
- function traverseDOMWidthFirst(startNode, callback) {
- const queue = [startNode];
-
- while (queue.length > 0) {
- const node = queue.shift();
-
- // 处理当前节点
- if (callback) {
- callback(node);
- }
-
- // 将子节点加入队列
- for (let i = 0; i < node.childNodes.length; i++) {
- queue.push(node.childNodes[i]);
- }
- }
- }
- // 使用示例
- traverseDOMWidthFirst(document.body, function(node) {
- if (node.nodeType === Node.ELEMENT_NODE) {
- console.log('元素节点:', node.tagName.toLowerCase());
- }
- });
复制代码
4.4 TreeWalker API
TreeWalker API提供了一个更高级的方式来遍历DOM树,它允许我们定义遍历的过滤条件。
- // 创建TreeWalker
- const walker = document.createTreeWalker(
- document.body, // 根节点
- NodeFilter.SHOW_ELEMENT, // 只显示元素节点
- { // 过滤器对象
- acceptNode: function(node) {
- // 只接受具有"active"类的元素
- if (node.classList.contains('active')) {
- return NodeFilter.FILTER_ACCEPT;
- }
- return NodeFilter.FILTER_SKIP;
- }
- }
- );
- // 遍历所有符合条件的节点
- let currentNode;
- while (currentNode = walker.nextNode()) {
- console.log('找到活动元素:', currentNode);
- }
复制代码
4.5 NodeIterator API
NodeIterator是另一个遍历DOM树的API,它比TreeWalker简单一些。
- // 创建NodeIterator
- const iterator = document.createNodeIterator(
- document.body,
- NodeFilter.SHOW_TEXT, // 只显示文本节点
- {
- acceptNode: function(node) {
- // 忽略只包含空白字符的文本节点
- if (node.nodeValue.trim() === '') {
- return NodeFilter.FILTER_REJECT;
- }
- return NodeFilter.FILTER_ACCEPT;
- }
- }
- );
- // 遍历所有符合条件的节点
- let currentNode;
- while (currentNode = iterator.nextNode()) {
- console.log('找到文本节点:', currentNode.nodeValue.trim());
- }
复制代码
5. 树形结构操作
5.1 创建节点
DOM提供了多种方法来创建新节点:
- // 创建元素节点
- const newDiv = document.createElement('div');
- newDiv.className = 'container';
- newDiv.id = 'main-container';
- // 创建文本节点
- const text = document.createTextNode('这是一个文本节点');
- // 创建注释节点
- const comment = document.createComment('这是一个注释');
- // 创建文档片段
- const fragment = document.createDocumentFragment();
- // 创建属性节点
- const attr = document.createAttribute('data-id');
- attr.value = '123';
- newDiv.setAttributeNode(attr);
复制代码
5.2 插入节点
有多种方法可以将新节点插入到DOM树中:
- // 获取父元素和子元素
- const parent = document.getElementById('parent');
- const child = document.getElementById('child');
- // appendChild - 在父元素的末尾添加子元素
- const newElement = document.createElement('div');
- newElement.textContent = '新元素';
- parent.appendChild(newElement);
- // insertBefore - 在指定子元素之前插入新元素
- const anotherNewElement = document.createElement('div');
- anotherNewElement.textContent = '另一个新元素';
- parent.insertBefore(anotherNewElement, child);
- // insertAdjacentHTML - 在元素的指定位置插入HTML字符串
- const referenceElement = document.getElementById('reference');
- referenceElement.insertAdjacentHTML('beforebegin', '<div>在元素之前</div>');
- referenceElement.insertAdjacentHTML('afterbegin', '<div>在元素内部开始处</div>');
- referenceElement.insertAdjacentHTML('beforeend', '<div>在元素内部结束处</div>');
- referenceElement.insertAdjacentHTML('afterend', '<div>在元素之后</div>');
- // insertAdjacentElement - 类似于insertAdjacentHTML,但插入的是DOM元素
- const newElement1 = document.createElement('div');
- newElement1.textContent = '新元素1';
- referenceElement.insertAdjacentElement('beforebegin', newElement1);
- // 使用文档片段批量插入,减少重排
- const fragment = document.createDocumentFragment();
- for (let i = 0; i < 10; i++) {
- const li = document.createElement('li');
- li.textContent = `项目 ${i + 1}`;
- fragment.appendChild(li);
- }
- document.getElementById('list').appendChild(fragment);
复制代码
5.3 替换和删除节点
- // 获取元素
- const parent = document.getElementById('parent');
- const oldChild = document.getElementById('old-child');
- // replaceChild - 替换子元素
- const newChild = document.createElement('div');
- newChild.textContent = '新子元素';
- parent.replaceChild(newChild, oldChild);
- // removeChild - 删除子元素
- const childToRemove = document.getElementById('child-to-remove');
- parent.removeChild(childToRemove);
- // 现代方法:直接在元素上调用remove
- const elementToRemove = document.getElementById('element-to-remove');
- elementToRemove.remove();
复制代码
5.4 克隆节点
- // 获取元素
- const original = document.getElementById('original');
- // cloneNode - 克隆节点
- // 参数为true时进行深度克隆(包括所有后代节点)
- // 参数为false时进行浅克隆(不包括后代节点)
- const shallowClone = original.cloneNode(false);
- const deepClone = original.cloneNode(true);
- // 将克隆的节点添加到文档中
- document.body.appendChild(deepClone);
复制代码
5.5 操作属性
- // 获取元素
- const element = document.getElementById('myElement');
- // getAttribute - 获取属性值
- const id = element.getAttribute('id');
- const className = element.getAttribute('class');
- // setAttribute - 设置属性值
- element.setAttribute('class', 'new-class');
- element.setAttribute('data-id', '123');
- // hasAttribute - 检查元素是否有指定属性
- if (element.hasAttribute('data-id')) {
- console.log('元素有data-id属性');
- }
- // removeAttribute - 移除属性
- element.removeAttribute('data-id');
- // 直接属性访问(对于标准属性)
- element.id = 'new-id';
- element.className = 'another-class';
- // dataset属性 - 访问data-*属性
- element.dataset.id = '456'; // 设置data-id
- console.log(element.dataset.id); // 获取data-id
- console.log(element.dataset.someValue); // 获取data-some-value
复制代码
6. 性能优化技巧
6.1 减少DOM访问
DOM访问是相对昂贵的操作,频繁访问DOM会影响性能。应该尽量减少DOM访问次数:
- // 不好的做法 - 在循环中多次访问DOM
- const items = document.querySelectorAll('.item');
- for (let i = 0; i < items.length; i++) {
- items[i].style.color = 'red';
- items[i].style.backgroundColor = 'yellow';
- items[i].style.border = '1px solid black';
- }
- // 好的做法 - 减少DOM访问
- const itemsArray = Array.from(document.querySelectorAll('.item'));
- const styles = {
- color: 'red',
- backgroundColor: 'yellow',
- border: '1px solid black'
- };
- itemsArray.forEach(item => {
- Object.assign(item.style, styles);
- });
复制代码
6.2 使用文档片段
文档片段(DocumentFragment)是一个轻量级的DOM对象,可以在内存中操作DOM,然后一次性插入到文档中,减少重排和重绘:
- // 不好的做法 - 多次插入导致多次重排
- const list = document.getElementById('list');
- for (let i = 0; i < 100; i++) {
- const li = document.createElement('li');
- li.textContent = `项目 ${i + 1}`;
- list.appendChild(li); // 每次appendChild都会导致重排
- }
- // 好的做法 - 使用文档片段
- const list = document.getElementById('list');
- const fragment = document.createDocumentFragment();
- for (let i = 0; i < 100; i++) {
- const li = document.createElement('li');
- li.textContent = `项目 ${i + 1}`;
- fragment.appendChild(li); // 在内存中操作,不会导致重排
- }
- list.appendChild(fragment); // 只导致一次重排
复制代码
6.3 批量修改样式
修改元素样式会导致重排和重绘,应该尽量批量修改样式:
- // 不好的做法 - 多次修改样式
- const element = document.getElementById('myElement');
- element.style.width = '100px';
- element.style.height = '100px';
- element.style.backgroundColor = 'red';
- element.style.color = 'white';
- // 好的做法 - 批量修改样式
- const element = document.getElementById('myElement');
- element.style.cssText = 'width: 100px; height: 100px; background-color: red; color: white;';
- // 或者使用类
- const element = document.getElementById('myElement');
- element.className = 'highlighted-box';
复制代码
6.4 使用事件委托
事件委托利用事件冒泡机制,将事件监听器添加到父元素上,而不是每个子元素上:
- // 不好的做法 - 为每个按钮添加事件监听器
- const buttons = document.querySelectorAll('.button');
- buttons.forEach(button => {
- button.addEventListener('click', function() {
- console.log('按钮被点击:', this.textContent);
- });
- });
- // 好的做法 - 使用事件委托
- document.getElementById('button-container').addEventListener('click', function(event) {
- if (event.target.matches('.button')) {
- console.log('按钮被点击:', event.target.textContent);
- }
- });
复制代码
6.5 使用requestAnimationFrame
对于需要频繁更新DOM的操作(如动画),使用requestAnimationFrame可以优化性能:
- // 不好的做法 - 使用setTimeout或setInterval
- let position = 0;
- function animate() {
- position += 1;
- element.style.transform = `translateX(${position}px)`;
- setTimeout(animate, 16); // 大约60fps
- }
- animate();
- // 好的做法 - 使用requestAnimationFrame
- let position = 0;
- function animate() {
- position += 1;
- element.style.transform = `translateX(${position}px)`;
- requestAnimationFrame(animate);
- }
- requestAnimationFrame(animate);
复制代码
6.6 虚拟滚动
对于包含大量数据的列表,使用虚拟滚动可以显著提高性能:
- // 虚拟滚动实现示例
- class VirtualScroll {
- constructor(container, itemHeight, totalItems) {
- this.container = container;
- this.itemHeight = itemHeight;
- this.totalItems = totalItems;
- this.visibleItems = Math.ceil(container.clientHeight / itemHeight) + 2;
- this.scrollTop = 0;
-
- // 创建占位元素以设置滚动高度
- this.placeholder = document.createElement('div');
- this.placeholder.style.height = `${totalItems * itemHeight}px`;
- container.appendChild(this.placeholder);
-
- // 创建可见项容器
- this.itemsContainer = document.createElement('div');
- container.appendChild(this.itemsContainer);
-
- // 监听滚动事件
- container.addEventListener('scroll', () => this.handleScroll());
-
- // 初始渲染
- this.renderItems();
- }
-
- handleScroll() {
- this.scrollTop = this.container.scrollTop;
- this.renderItems();
- }
-
- renderItems() {
- // 计算起始索引
- const startIndex = Math.floor(this.scrollTop / this.itemHeight);
-
- // 清空容器
- this.itemsContainer.innerHTML = '';
-
- // 设置容器位置
- this.itemsContainer.style.transform = `translateY(${startIndex * this.itemHeight}px)`;
-
- // 渲染可见项
- for (let i = 0; i < this.visibleItems; i++) {
- const index = startIndex + i;
- if (index >= this.totalItems) break;
-
- const item = document.createElement('div');
- item.style.height = `${this.itemHeight}px`;
- item.textContent = `项目 ${index + 1}`;
- this.itemsContainer.appendChild(item);
- }
- }
- }
- // 使用示例
- const container = document.getElementById('scroll-container');
- const virtualScroll = new VirtualScroll(container, 30, 1000);
复制代码
7. 实际应用场景和示例
7.1 动态表格排序
- class TableSorter {
- constructor(tableId) {
- this.table = document.getElementById(tableId);
- this.headers = this.table.querySelectorAll('th');
- this.tbody = this.table.querySelector('tbody');
- this.rows = Array.from(this.tbody.querySelectorAll('tr'));
- this.sortColumn = -1;
- this.sortDirection = 1;
-
- // 为表头添加点击事件
- this.headers.forEach((header, index) => {
- header.addEventListener('click', () => this.sort(index));
- });
- }
-
- sort(columnIndex) {
- // 如果点击的是同一列,则切换排序方向
- if (this.sortColumn === columnIndex) {
- this.sortDirection *= -1;
- } else {
- this.sortColumn = columnIndex;
- this.sortDirection = 1;
- }
-
- // 排序行
- this.rows.sort((a, b) => {
- const aValue = a.cells[columnIndex].textContent;
- const bValue = b.cells[columnIndex].textContent;
-
- // 尝试将值转换为数字
- const aNum = parseFloat(aValue);
- const bNum = parseFloat(bValue);
-
- let comparison;
- if (!isNaN(aNum) && !isNaN(bNum)) {
- comparison = aNum - bNum;
- } else {
- comparison = aValue.localeCompare(bValue);
- }
-
- return comparison * this.sortDirection;
- });
-
- // 重新插入排序后的行
- this.rows.forEach(row => this.tbody.appendChild(row));
-
- // 更新表头样式
- this.updateHeaderStyles(columnIndex);
- }
-
- updateHeaderStyles(activeColumn) {
- // 移除所有表头的样式
- this.headers.forEach(header => {
- header.classList.remove('sort-asc', 'sort-desc');
- });
-
- // 为活动列添加样式
- if (this.sortDirection === 1) {
- this.headers[activeColumn].classList.add('sort-asc');
- } else {
- this.headers[activeColumn].classList.add('sort-desc');
- }
- }
- }
- // 使用示例
- const tableSorter = new TableSorter('data-table');
复制代码
7.2 树形菜单组件
- class TreeMenu {
- constructor(containerId, data) {
- this.container = document.getElementById(containerId);
- this.data = data;
- this.init();
- }
-
- init() {
- this.container.innerHTML = '';
- this.renderTree(this.data, this.container);
- }
-
- renderTree(items, parentElement) {
- const ul = document.createElement('ul');
-
- items.forEach(item => {
- const li = document.createElement('li');
-
- // 创建标题
- const title = document.createElement('div');
- title.className = 'tree-item-title';
- title.textContent = item.title;
-
- // 如果有子项,添加展开/折叠图标
- if (item.children && item.children.length > 0) {
- const icon = document.createElement('span');
- icon.className = 'tree-icon';
- icon.textContent = '▶';
- title.insertBefore(icon, title.firstChild);
-
- // 添加点击事件
- title.addEventListener('click', () => {
- icon.textContent = icon.textContent === '▶' ? '▼' : '▶';
- const childrenContainer = li.querySelector('.tree-children');
- if (childrenContainer) {
- childrenContainer.style.display =
- childrenContainer.style.display === 'none' ? 'block' : 'none';
- }
- });
- }
-
- li.appendChild(title);
-
- // 如果有子项,递归渲染
- if (item.children && item.children.length > 0) {
- const childrenContainer = document.createElement('div');
- childrenContainer.className = 'tree-children';
- this.renderTree(item.children, childrenContainer);
- li.appendChild(childrenContainer);
- }
-
- ul.appendChild(li);
- });
-
- parentElement.appendChild(ul);
- }
- }
- // 使用示例
- const treeData = [
- {
- title: '项目1',
- children: [
- {
- title: '子项目1.1',
- children: [
- { title: '子项目1.1.1' },
- { title: '子项目1.1.2' }
- ]
- },
- { title: '子项目1.2' }
- ]
- },
- {
- title: '项目2',
- children: [
- { title: '子项目2.1' },
- { title: '子项目2.2' }
- ]
- }
- ];
- const treeMenu = new TreeMenu('tree-container', treeData);
复制代码
7.3 拖放排序列表
- class DragDropList {
- constructor(listId) {
- this.list = document.getElementById(listId);
- this.items = Array.from(this.list.querySelectorAll('.draggable-item'));
- this.draggedElement = null;
-
- this.init();
- }
-
- init() {
- // 为每个可拖拽项添加事件监听器
- this.items.forEach(item => {
- item.draggable = true;
-
- // 拖拽开始
- item.addEventListener('dragstart', (e) => {
- this.draggedElement = item;
- item.classList.add('dragging');
- e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData('text/html', item.innerHTML);
- });
-
- // 拖拽结束
- item.addEventListener('dragend', () => {
- item.classList.remove('dragging');
- });
-
- // 拖拽经过
- item.addEventListener('dragover', (e) => {
- if (e.preventDefault) {
- e.preventDefault(); // 允许放置
- }
- e.dataTransfer.dropEffect = 'move';
-
- // 确定插入位置
- const afterElement = this.getDragAfterElement(this.list, e.clientY);
- if (afterElement == null) {
- this.list.appendChild(this.draggedElement);
- } else {
- this.list.insertBefore(this.draggedElement, afterElement);
- }
-
- return false;
- });
- });
- }
-
- getDragAfterElement(container, y) {
- const draggableElements = [...container.querySelectorAll('.draggable-item:not(.dragging)')];
-
- 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: offset, element: child };
- } else {
- return closest;
- }
- }, { offset: Number.NEGATIVE_INFINITY }).element;
- }
- }
- // 使用示例
- const dragDropList = new DragDropList('sortable-list');
复制代码
7.4 动态表单生成器
- class DynamicFormGenerator {
- constructor(containerId, fields) {
- this.container = document.getElementById(containerId);
- this.fields = fields;
- this.form = document.createElement('form');
-
- this.init();
- }
-
- init() {
- this.container.innerHTML = '';
- this.container.appendChild(this.form);
-
- // 生成表单字段
- this.fields.forEach(field => {
- const formGroup = this.createFormGroup(field);
- this.form.appendChild(formGroup);
- });
-
- // 添加提交按钮
- const submitButton = document.createElement('button');
- submitButton.type = 'submit';
- submitButton.textContent = '提交';
- this.form.appendChild(submitButton);
-
- // 添加表单提交事件
- this.form.addEventListener('submit', (e) => this.handleSubmit(e));
- }
-
- createFormGroup(field) {
- const formGroup = document.createElement('div');
- formGroup.className = 'form-group';
-
- // 创建标签
- const label = document.createElement('label');
- label.textContent = field.label;
- label.htmlFor = field.name;
- formGroup.appendChild(label);
-
- // 根据字段类型创建输入元素
- let input;
- switch (field.type) {
- case 'text':
- case 'email':
- case 'password':
- case 'number':
- input = document.createElement('input');
- input.type = field.type;
- input.name = field.name;
- input.id = field.name;
- if (field.required) input.required = true;
- if (field.placeholder) input.placeholder = field.placeholder;
- break;
-
- case 'textarea':
- input = document.createElement('textarea');
- input.name = field.name;
- input.id = field.name;
- if (field.required) input.required = true;
- if (field.placeholder) input.placeholder = field.placeholder;
- if (field.rows) input.rows = field.rows;
- break;
-
- case 'select':
- input = document.createElement('select');
- input.name = field.name;
- input.id = field.name;
- if (field.required) input.required = true;
-
- // 添加选项
- field.options.forEach(option => {
- const optionElement = document.createElement('option');
- optionElement.value = option.value;
- optionElement.textContent = option.label;
- input.appendChild(optionElement);
- });
- break;
-
- case 'checkbox':
- case 'radio':
- input = document.createElement('input');
- input.type = field.type;
- input.name = field.name;
- input.id = field.name;
- if (field.checked) input.checked = true;
-
- // 为复选框和单选按钮添加标签文本
- const span = document.createElement('span');
- span.textContent = field.label;
- label.textContent = '';
- label.appendChild(input);
- label.appendChild(span);
- break;
- }
-
- if (input && field.type !== 'checkbox' && field.type !== 'radio') {
- formGroup.appendChild(input);
- }
-
- return formGroup;
- }
-
- handleSubmit(e) {
- e.preventDefault();
-
- // 收集表单数据
- const formData = new FormData(this.form);
- const data = {};
-
- for (let [key, value] of formData.entries()) {
- data[key] = value;
- }
-
- console.log('表单数据:', data);
-
- // 这里可以添加AJAX请求来提交表单数据
- // fetch('/api/submit', {
- // method: 'POST',
- // headers: {
- // 'Content-Type': 'application/json'
- // },
- // body: JSON.stringify(data)
- // })
- // .then(response => response.json())
- // .then(result => {
- // console.log('提交成功:', result);
- // })
- // .catch(error => {
- // console.error('提交失败:', error);
- // });
- }
- }
- // 使用示例
- const formFields = [
- { name: 'name', label: '姓名', type: 'text', required: true, placeholder: '请输入您的姓名' },
- { name: 'email', label: '邮箱', type: 'email', required: true, placeholder: '请输入您的邮箱' },
- { name: 'password', label: '密码', type: 'password', required: true },
- { name: 'gender', label: '性别', type: 'radio', checked: true },
- { name: 'message', label: '留言', type: 'textarea', placeholder: '请输入您的留言', rows: 4 },
- {
- name: 'country',
- label: '国家',
- type: 'select',
- options: [
- { value: 'cn', label: '中国' },
- { value: 'us', label: '美国' },
- { value: 'uk', label: '英国' }
- ]
- },
- { name: 'terms', label: '我同意服务条款', type: 'checkbox', required: true }
- ];
- const formGenerator = new DynamicFormGenerator('form-container', formFields);
复制代码
总结
本文全面介绍了HTML DOM查询与遍历的相关知识,从基础选择器到高级树形结构操作,涵盖了前端开发中必备的DOM操作技能。我们学习了:
1. DOM基础概念,理解了DOM树形结构的组成和节点类型。
2. 基础选择器方法,如getElementById、getElementsByTagName、getElementsByClassName和getElementsByName。
3. 高级选择器方法,如querySelector、querySelectorAll、matches和closest,它们提供了更灵活的元素选择方式。
4. DOM节点关系与遍历,包括节点类型、节点关系属性、遍历DOM树的方法,以及TreeWalker和NodeIterator API。
5. 树形结构操作,包括创建、插入、替换、删除和克隆节点,以及操作元素属性。
6. 性能优化技巧,如减少DOM访问、使用文档片段、批量修改样式、使用事件委托、使用requestAnimationFrame和实现虚拟滚动。
7. 实际应用场景和示例,包括动态表格排序、树形菜单组件、拖放排序列表和动态表单生成器。
掌握这些DOM操作技能对于前端开发至关重要,它们不仅能够帮助我们构建交互性强的网页应用,还能通过优化技巧提升网页性能,提供更好的用户体验。随着前端技术的不断发展,DOM操作仍然是前端开发的基础,深入理解并熟练运用这些技能,将有助于我们在前端开发道路上走得更远。 |
|