MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript批量操作DOM节点的技巧

2021-03-165.6k 阅读

JavaScript批量操作DOM节点的技巧

在前端开发中,操作文档对象模型(DOM)是一项核心任务。当涉及到批量操作DOM节点时,掌握一些高效的技巧不仅能提升代码的性能,还能让代码更加简洁易读。下面我们就来深入探讨JavaScript中批量操作DOM节点的各种技巧。

一、选择DOM节点

在进行批量操作之前,首先要准确地选择需要操作的DOM节点。JavaScript提供了多种选择DOM节点的方法,不同的方法在适用性和性能上各有差异。

1. getElementsByTagName

getElementsByTagName 方法通过标签名来选择元素。它返回一个实时的 HTMLCollection 对象,意味着如果文档结构发生变化,这个集合也会自动更新。例如,要选择页面中所有的 <li> 元素:

const lis = document.getElementsByTagName('li');
for (let i = 0; i < lis.length; i++) {
    // 在这里可以对每个li元素进行操作
    lis[i].style.color = 'red';
}

不过,由于 HTMLCollection 是实时的,在某些情况下可能会导致性能问题,特别是在循环中对文档结构进行修改时。

2. getElementsByClassName

getElementsByClassName 方法通过类名来选择元素,同样返回一个实时的 HTMLCollection。比如,选择所有具有 active 类的元素:

const activeElements = document.getElementsByClassName('active');
for (let i = 0; i < activeElements.length; i++) {
    activeElements[i].classList.add('highlight');
}

3. querySelectorAll

querySelectorAll 方法是最常用的选择器之一,它接受一个CSS选择器字符串作为参数,并返回一个静态的 NodeList。这意味着即使文档结构发生变化,这个 NodeList 也不会自动更新。例如,选择所有 <div> 元素内的 <p> 元素:

const pInDivs = document.querySelectorAll('div p');
pInDivs.forEach(p => {
    p.textContent = '新的文本';
});

querySelectorAll 的优势在于它支持复杂的CSS选择器,能够满足各种精确选择的需求。而且由于返回的是静态 NodeList,在性能上相对更优,尤其是在需要遍历和操作节点集合的场景下。

4. getElementById

getElementById 方法通过元素的 id 属性来选择单个元素。因为 id 在文档中应该是唯一的,所以它总是返回一个单一的元素或者 null。例如:

const myDiv = document.getElementById('myDiv');
if (myDiv) {
    myDiv.style.backgroundColor = 'blue';
}

虽然它主要用于选择单个元素,但在批量操作中,如果多个元素的 id 有一定规律,也可以通过循环结合字符串拼接等方式来选择多个元素。比如,假设有 div1div2div3 等元素:

for (let i = 1; i <= 3; i++) {
    const div = document.getElementById(`div${i}`);
    if (div) {
        div.textContent = `这是第 ${i} 个div`;
    }
}

二、批量创建DOM节点

有时候我们需要批量创建多个DOM节点并添加到文档中。手动逐个创建节点效率较低,并且代码冗长。以下是一些高效的批量创建节点的方法。

1. 循环创建并添加节点

最基本的方式是通过循环来创建和添加节点。例如,要创建5个 <li> 元素并添加到一个 <ul> 元素中:

const ul = document.getElementById('myUl');
for (let i = 1; i <= 5; i++) {
    const li = document.createElement('li');
    li.textContent = `列表项 ${i}`;
    ul.appendChild(li);
}

2. 使用文档片段(DocumentFragment)

文档片段是一种轻量级的DOM容器,它存在于内存中,不会直接影响文档的渲染。我们可以先在文档片段中批量创建和操作节点,最后将文档片段添加到文档中,这样可以显著减少重排和重绘的次数,提高性能。

const ul = document.getElementById('myUl');
const fragment = document.createDocumentFragment();
for (let i = 1; i <= 5; i++) {
    const li = document.createElement('li');
    li.textContent = `列表项 ${i}`;
    fragment.appendChild(li);
}
ul.appendChild(fragment);

在这个例子中,所有的 <li> 元素都先添加到文档片段 fragment 中,最后一次性将 fragment 添加到 <ul> 中。这样只触发一次重排和重绘,而不是每次添加一个 <li> 元素都触发。

3. 使用innerHTML

innerHTML 属性可以用来设置或获取元素的HTML内容。通过拼接字符串的方式,可以一次性创建多个节点。例如:

const ul = document.getElementById('myUl');
let html = '';
for (let i = 1; i <= 5; i++) {
    html += `<li>列表项 ${i}</li>`;
}
ul.innerHTML = html;

虽然 innerHTML 方式简洁高效,但需要注意安全问题,尤其是在插入用户输入内容时,容易引发跨站脚本攻击(XSS)。因此,在使用 innerHTML 插入内容时,一定要对输入进行严格的过滤和转义。

三、批量修改DOM节点属性

批量修改DOM节点的属性是常见的操作之一,以下介绍几种有效的方法。

1. 循环遍历修改属性

通过循环遍历节点集合,逐个修改节点的属性。例如,要将所有图片的 alt 属性设置为图片的文件名:

const images = document.getElementsByTagName('img');
for (let i = 0; i < images.length; i++) {
    const img = images[i];
    const filename = img.src.split('/').pop();
    img.alt = filename;
}

2. 使用类名来批量修改样式

通过修改类名,可以利用CSS的规则来批量改变元素的样式。首先在CSS中定义好不同的样式类:

.highlight {
    background-color: yellow;
}
.bold {
    font-weight: bold;
}

然后在JavaScript中,通过操作类名来批量应用样式。例如,将所有具有 active 类的元素添加 highlightbold 类:

const activeElements = document.getElementsByClassName('active');
for (let i = 0; i < activeElements.length; i++) {
    activeElements[i].classList.add('highlight', 'bold');
}

3. 使用dataset属性批量传递数据

dataset 属性允许我们在HTML元素上自定义数据属性,并在JavaScript中方便地访问和修改这些数据。假设我们有一些按钮,每个按钮都有一个自定义的 data - action 属性,用来表示按钮的操作类型:

<button data - action="save">保存</button>
<button data - action="delete">删除</button>

在JavaScript中,可以批量获取和处理这些数据:

const buttons = document.querySelectorAll('button');
buttons.forEach(button => {
    const action = button.dataset.action;
    if (action ==='save') {
        // 执行保存操作的逻辑
        console.log('执行保存操作');
    } else if (action === 'delete') {
        // 执行删除操作的逻辑
        console.log('执行删除操作');
    }
});

四、批量事件绑定

为多个DOM节点绑定事件是前端开发中的常见需求。以下是几种批量绑定事件的技巧。

1. 循环遍历绑定事件

这是最基本的方法,通过循环遍历节点集合,为每个节点绑定相同的事件处理函数。例如,为所有的 <button> 元素绑定点击事件:

const buttons = document.getElementsByTagName('button');
for (let i = 0; i < buttons.length; i++) {
    buttons[i].addEventListener('click', function() {
        console.log('按钮被点击了');
    });
}

2. 事件委托

事件委托是一种更高效的批量绑定事件的方法。它利用了事件冒泡的原理,将事件处理函数绑定到父元素上,而不是每个子元素。例如,有一个 <ul> 元素,其中包含多个 <li> 元素,为每个 <li> 元素绑定点击事件:

<ul id="myUl">
    <li>列表项1</li>
    <li>列表项2</li>
    <li>列表项3</li>
</ul>
const ul = document.getElementById('myUl');
ul.addEventListener('click', function(event) {
    if (event.target.tagName === 'LI') {
        console.log(`点击了列表项:${event.target.textContent}`);
    }
});

事件委托不仅减少了事件处理函数的数量,提高了性能,还能自动处理动态添加的子元素的事件。

3. 使用事件代理库

除了原生的事件委托方式,还可以使用一些库来简化事件代理的操作。例如,jQuery的 on 方法就支持事件委托:

<script src="https://code.jquery.com/jquery - 3.6.0.min.js"></script>
<ul id="myUl">
    <li>列表项1</li>
    <li>列表项2</li>
    <li>列表项3</li>
</ul>
$(document).ready(function() {
    $('#myUl').on('click', 'li', function() {
        console.log(`点击了列表项:${$(this).text()}`);
    });
});

虽然使用库可以简化代码,但也要考虑引入库带来的额外开销,尤其是在对性能要求较高的场景下。

五、批量操作DOM节点的性能优化

在批量操作DOM节点时,性能优化至关重要。以下是一些关键的性能优化点。

1. 减少重排和重绘

重排(reflow)和重绘(repaint)是浏览器渲染过程中的两个重要步骤。重排会重新计算元素的位置和大小,重绘则是重新绘制元素的外观。频繁的重排和重绘会严重影响性能。

  • 使用文档片段:如前文所述,先在文档片段中进行节点的创建、修改和添加,最后一次性将文档片段添加到文档中,这样可以将多次重排和重绘合并为一次。
  • 批量修改样式:不要在循环中逐个修改元素的样式,而是先将所有需要修改的样式定义在一个CSS类中,然后通过操作类名来批量应用样式。例如:
.newStyle {
    color: red;
    font - size: 16px;
}
const elements = document.querySelectorAll('.targetElements');
elements.forEach(element => {
    element.classList.add('newStyle');
});

2. 缓存节点引用

在循环中,如果多次访问相同的DOM节点或者调用 querySelectorAll 等方法获取节点集合,会增加性能开销。因此,应该缓存节点引用。例如:

const ul = document.getElementById('myUl');
const lis = ul.getElementsByTagName('li');
for (let i = 0; i < lis.length; i++) {
    lis[i].style.color = 'blue';
}

在这个例子中,先获取了 <ul> 元素的引用并缓存,然后通过这个缓存的引用获取 <li> 元素集合,避免了多次查询DOM树。

3. 避免频繁访问布局属性

当访问元素的布局属性(如 offsetTopclientWidth 等)时,浏览器可能需要重新计算布局,导致重排。在循环中尽量避免频繁访问这些属性。如果确实需要使用,可以先将这些属性值缓存起来。例如:

const div = document.getElementById('myDiv');
let top = div.offsetTop;
for (let i = 0; i < 100; i++) {
    // 使用缓存的top值,而不是每次都访问div.offsetTop
    console.log(top);
}

六、处理动态DOM节点

在实际开发中,DOM节点可能是动态生成或删除的。在进行批量操作时,需要考虑如何处理这些动态变化。

1. 动态添加节点的批量操作

当动态添加节点时,可以在添加节点的同时进行批量操作,或者使用事件委托来处理新添加节点的事件。例如,通过点击按钮动态添加 <li> 元素,并为新添加的 <li> 元素绑定点击事件:

<button id="addButton">添加列表项</button>
<ul id="myUl"></ul>
const addButton = document.getElementById('addButton');
const ul = document.getElementById('myUl');
addButton.addEventListener('click', function() {
    const li = document.createElement('li');
    li.textContent = '新的列表项';
    ul.appendChild(li);
    // 为新添加的li绑定点击事件
    li.addEventListener('click', function() {
        console.log('新列表项被点击');
    });
});

或者使用事件委托的方式:

const addButton = document.getElementById('addButton');
const ul = document.getElementById('myUl');
addButton.addEventListener('click', function() {
    const li = document.createElement('li');
    li.textContent = '新的列表项';
    ul.appendChild(li);
});
ul.addEventListener('click', function(event) {
    if (event.target.tagName === 'LI') {
        console.log('列表项被点击');
    }
});

2. 动态删除节点的批量操作

当动态删除节点时,要注意更新相关的节点集合引用。例如,有一个删除按钮,点击后删除对应的 <li> 元素,并更新相关的操作逻辑:

<ul id="myUl">
    <li>列表项1 <button class="deleteButton">删除</button></li>
    <li>列表项2 <button class="deleteButton">删除</button></li>
</ul>
const ul = document.getElementById('myUl');
const deleteButtons = ul.querySelectorAll('.deleteButton');
deleteButtons.forEach(button => {
    button.addEventListener('click', function() {
        const li = this.parentNode;
        ul.removeChild(li);
        // 更新节点集合引用(如果后续还需要操作相关节点)
        const newDeleteButtons = ul.querySelectorAll('.deleteButton');
        // 可以在这里对新的节点集合进行重新绑定事件等操作
    });
});

七、跨浏览器兼容性

在进行批量操作DOM节点时,还需要考虑跨浏览器兼容性。不同的浏览器在实现DOM操作的细节上可能存在差异。

1. 特性检测

特性检测是处理跨浏览器兼容性的常用方法。例如,在使用 classList 属性时,并不是所有的浏览器都支持。可以通过以下方式进行特性检测:

if ('classList' in document.documentElement) {
    const elements = document.querySelectorAll('.targetElements');
    elements.forEach(element => {
        element.classList.add('newClass');
    });
} else {
    // 不支持classList时的替代方案
    const elements = document.querySelectorAll('.targetElements');
    for (let i = 0; i < elements.length; i++) {
        const className = elements[i].className;
        if (className.indexOf('newClass') === -1) {
            elements[i].className += 'newClass';
        }
    }
}

2. 使用Polyfill

对于一些新的DOM操作特性,可以使用Polyfill来实现跨浏览器兼容。例如,querySelectorAll 在较老的浏览器中可能不支持,这时可以引入一个 querySelectorAll 的Polyfill库,使得代码在不支持该特性的浏览器中也能正常工作。

八、与框架结合使用

在现代前端开发中,常常会使用各种JavaScript框架,如React、Vue.js等。虽然这些框架提供了自己的方式来管理DOM,但了解原生JavaScript的批量DOM操作技巧仍然很有帮助。

1. 在React中结合原生DOM操作

在React中,一般通过状态和虚拟DOM来管理UI。但在某些情况下,可能需要直接操作DOM,比如操作第三方插件的DOM。可以使用 ref 来获取DOM节点引用,然后结合原生JavaScript进行批量操作。例如:

import React, { useRef, useEffect } from'react';

const MyComponent = () => {
    const ulRef = useRef(null);

    useEffect(() => {
        if (ulRef.current) {
            const lis = ulRef.current.getElementsByTagName('li');
            for (let i = 0; i < lis.length; i++) {
                lis[i].style.color = 'green';
            }
        }
    }, []);

    return (
        <ul ref={ulRef}>
            <li>列表项1</li>
            <li>列表项2</li>
        </ul>
    );
};

export default MyComponent;

2. 在Vue.js中结合原生DOM操作

在Vue.js中,可以使用 ref 来访问DOM元素。例如,在模板中定义 ref

<template>
    <ul ref="myUl">
        <li v - for="(item, index) in items" :key="index">{{ item }}</li>
    </ul>
</template>

然后在Vue实例中通过 $refs 来获取DOM节点并进行批量操作:

export default {
    data() {
        return {
            items: ['列表项1', '列表项2']
        };
    },
    mounted() {
        const ul = this.$refs.myUl;
        const lis = ul.getElementsByTagName('li');
        for (let i = 0; i < lis.length; i++) {
            lis[i].style.backgroundColor = 'yellow';
        }
    }
};

通过掌握这些JavaScript批量操作DOM节点的技巧,无论是在原生JavaScript项目还是结合框架的项目中,都能够更高效地处理DOM相关的任务,提升前端应用的性能和用户体验。在实际开发中,要根据具体的需求和场景,选择最合适的方法来实现批量操作DOM节点。同时,要始终关注性能优化和跨浏览器兼容性,确保代码在各种环境下都能稳定运行。