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

JavaScript操作DOM的高效方法

2021-04-123.3k 阅读

1. 理解 DOM

在深入探讨 JavaScript 操作 DOM 的高效方法之前,我们首先要对 DOM(文档对象模型)有一个清晰的理解。DOM 是一种用于 HTML 和 XML 文档的编程接口,它将文档呈现为一个由节点和对象(包含属性和方法)组成的逻辑树。例如,一个简单的 HTML 文档:

<!DOCTYPE html>
<html>
<head>
    <title>DOM 示例</title>
</head>
<body>
    <h1 id="main-heading">欢迎来到我的网站</h1>
    <p class="intro">这是一段介绍性的文字。</p>
    <ul id="list">
        <li>项目 1</li>
        <li>项目 2</li>
        <li>项目 3</li>
    </ul>
</body>
</html>

在这个文档中,html 元素是根节点,headbody 是它的子节点,h1pul 又是 body 的子节点,以此类推。每个节点都可以通过 JavaScript 进行访问、修改或删除。

2. 获取 DOM 元素

2.1 使用 getElementById

getElementById 方法是获取单个 DOM 元素最常用且高效的方式之一,前提是该元素有唯一的 id 属性。例如,要获取上面 HTML 中的 h1 元素:

const mainHeading = document.getElementById('main-heading');
console.log(mainHeading.textContent);

在这个例子中,getElementById 方法返回具有 idmain - heading 的元素,然后我们可以访问它的 textContent 属性来获取其文本内容。

2.2 使用 querySelector

querySelector 方法使用 CSS 选择器来获取文档中匹配的第一个元素。例如,要获取 classintrop 元素:

const introParagraph = document.querySelector('p.intro');
console.log(introParagraph.textContent);

querySelector 非常强大,它可以使用复杂的 CSS 选择器,比如 document.querySelector('ul#list li:nth-child(2)') 可以获取 idlistul 中的第二个 li 元素。

2.3 使用 getElementsByClassName

getElementsByClassName 方法返回一个实时的 HTMLCollection,包含所有具有指定 class 的元素。例如,假设我们有多个具有 classitem 的元素:

<!DOCTYPE html>
<html>
<body>
    <div class="item">第一个项目</div>
    <div class="item">第二个项目</div>
    <div class="item">第三个项目</div>
    <script>
        const items = document.getElementsByClassName('item');
        for (let i = 0; i < items.length; i++) {
            console.log(items[i].textContent);
        }
    </script>
</body>
</html>

需要注意的是,getElementsByClassName 返回的是实时集合,意味着如果文档结构发生变化,该集合也会相应更新。

2.4 使用 getElementsByTagName

getElementsByTagName 方法返回一个实时的 HTMLCollection,包含所有指定标签名的元素。例如,要获取文档中的所有 li 元素:

const listItems = document.getElementsByTagName('li');
for (let i = 0; i < listItems.length; i++) {
    console.log(listItems[i].textContent);
}

同样,这个集合也是实时更新的。

3. 创建和插入 DOM 元素

3.1 创建元素

使用 document.createElement 方法可以创建一个新的 DOM 元素。例如,要创建一个新的 li 元素:

const newListItem = document.createElement('li');
newListItem.textContent = '新的项目';

这里我们创建了一个 li 元素,并设置了它的文本内容。

3.2 插入元素

3.2.1 使用 appendChild

appendChild 方法将一个节点添加到指定父节点的子节点列表的末尾。例如,要将上面创建的 li 元素添加到 idlistul 中:

const list = document.getElementById('list');
list.appendChild(newListItem);
3.2.2 使用 insertBefore

insertBefore 方法在指定的参考节点之前插入一个新节点。假设我们有以下 HTML:

<ul id="list">
    <li>项目 1</li>
    <li id="ref-item">项目 2</li>
    <li>项目 3</li>
</ul>

要在 idref - itemli 元素之前插入新的 li 元素:

const list = document.getElementById('list');
const refItem = document.getElementById('ref-item');
list.insertBefore(newListItem, refItem);

4. 修改 DOM 元素

4.1 修改属性

要修改 DOM 元素的属性,可以直接访问和设置该属性。例如,要修改 h1 元素的 id 属性:

const mainHeading = document.getElementById('main-heading');
mainHeading.id = 'new - main - heading';

对于 class 属性,由于它在 JavaScript 中有特殊含义,我们可以使用 className 属性来设置或修改 class。例如:

const introParagraph = document.querySelector('p.intro');
introParagraph.className = 'new - intro - class';

4.2 修改文本内容

如前文所述,使用 textContent 属性可以方便地修改元素的文本内容。例如:

const introParagraph = document.querySelector('p.intro');
introParagraph.textContent = '这是修改后的介绍性文字。';

4.3 修改样式

可以通过 style 属性来直接修改元素的内联样式。例如,要将 h1 元素的颜色改为红色:

const mainHeading = document.getElementById('main-heading');
mainHeading.style.color ='red';

也可以通过操作 classList 属性来添加、移除或切换 CSS 类,从而间接修改样式。例如:

const introParagraph = document.querySelector('p.intro');
introParagraph.classList.add('highlight');
introParagraph.classList.remove('old - class');
introParagraph.classList.toggle('active');

5. 删除 DOM 元素

5.1 使用 removeChild

要删除一个子元素,可以使用父元素的 removeChild 方法。例如,要删除 idlistul 中的第一个 li 元素:

<ul id="list">
    <li>项目 1</li>
    <li>项目 2</li>
    <li>项目 3</li>
</ul>
<script>
    const list = document.getElementById('list');
    const firstItem = list.children[0];
    list.removeChild(firstItem);
</script>

5.2 使用 remove

从 DOM 规范 Level 4 开始,元素本身新增了 remove 方法,使用起来更加简洁。例如,要删除上面 ul 中的第二个 li 元素:

<ul id="list">
    <li>项目 1</li>
    <li id="item - to - remove">项目 2</li>
    <li>项目 3</li>
</ul>
<script>
    const itemToRemove = document.getElementById('item - to - remove');
    itemToRemove.remove();
</script>

6. 事件处理与 DOM 操作

6.1 绑定事件

在 JavaScript 中,我们常常需要为 DOM 元素绑定事件处理函数。例如,为一个按钮添加点击事件:

<button id="myButton">点击我</button>
<script>
    const myButton = document.getElementById('myButton');
    myButton.addEventListener('click', function() {
        console.log('按钮被点击了');
    });
</script>

在事件处理函数中,我们可以进行各种 DOM 操作。比如,点击按钮后修改一个段落的文本内容:

<p id="targetParagraph">原始文本</p>
<button id="myButton">点击修改文本</button>
<script>
    const myButton = document.getElementById('myButton');
    const targetParagraph = document.getElementById('targetParagraph');
    myButton.addEventListener('click', function() {
        targetParagraph.textContent = '文本已被修改';
    });
</script>

6.2 事件委托

事件委托是一种高效的事件处理技巧,它利用事件冒泡的特性,将事件处理程序绑定到父元素上,而不是每个子元素。例如,有一个包含多个 li 元素的 ul

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

这样,无论点击哪个 li 元素,都会触发父元素 ul 上的点击事件,通过检查 event.target 来确定具体点击的是哪个子元素。事件委托不仅减少了事件处理程序的数量,提高了性能,还便于动态添加或删除子元素,因为不需要为新添加的子元素重新绑定事件。

7. 性能优化技巧

7.1 批量操作

在对 DOM 进行多次修改时,尽量将这些修改合并为一次操作。例如,如果要同时修改一个元素的多个样式属性:

const element = document.getElementById('myElement');
// 不好的做法:多次操作
element.style.color ='red';
element.style.fontSize = '16px';
element.style.backgroundColor = 'yellow';
// 好的做法:批量操作
const newStyles = {
    color:'red',
    fontSize: '16px',
    backgroundColor: 'yellow'
};
for (let style in newStyles) {
    element.style[style] = newStyles[style];
}

同样,在添加多个子元素时,先创建一个文档片段(document.createDocumentFragment),将所有新元素添加到文档片段中,然后一次性将文档片段添加到 DOM 中。例如:

<ul id="list"></ul>
<script>
    const list = document.getElementById('list');
    const fragment = document.createDocumentFragment();
    const items = ['项目 1', '项目 2', '项目 3'];
    items.forEach(function(item) {
        const li = document.createElement('li');
        li.textContent = item;
        fragment.appendChild(li);
    });
    list.appendChild(fragment);
</script>

7.2 减少重排与重绘

重排(reflow)是指浏览器重新计算元素的几何属性(如位置、大小等),重绘(repaint)是指浏览器重新绘制元素。频繁的重排和重绘会严重影响性能。例如,每次修改元素的 width 属性都会触发重排。为了减少重排和重绘,可以先修改元素的 display 属性为 none,进行所有需要的修改后,再将 display 改回原来的值。例如:

const element = document.getElementById('myElement');
element.style.display = 'none';
// 进行多个修改操作
element.style.width = '200px';
element.style.height = '100px';
element.style.color = 'blue';
element.style.display = 'block';

另外,使用 CSS 过渡(transitions)和动画(animations)时,尽量使用 transformopacity 属性,因为它们不会触发重排,只会触发重绘,性能相对较好。例如:

/* CSS */
.element {
    transition: transform 0.3s ease - in - out;
}
/* JavaScript */
const element = document.querySelector('.element');
element.style.transform = 'translateX(100px)';

7.3 缓存 DOM 查询结果

如果在代码中多次使用相同的 DOM 查询,应该缓存查询结果。例如:

// 不好的做法:多次查询
for (let i = 0; i < 10; i++) {
    const element = document.getElementById('myElement');
    console.log(element.textContent);
}
// 好的做法:缓存查询结果
const element = document.getElementById('myElement');
for (let i = 0; i < 10; i++) {
    console.log(element.textContent);
}

7.4 使用 requestAnimationFrame

当需要进行动画相关的 DOM 操作时,使用 requestAnimationFrame 可以优化性能。requestAnimationFrame 会在浏览器下一次重绘之前调用指定的回调函数,它会根据浏览器的刷新频率来调整执行时机,从而避免不必要的计算和重绘。例如,实现一个简单的元素移动动画:

<div id="box" style="width: 100px; height: 100px; background - color: red; position: relative;"></div>
<script>
    const box = document.getElementById('box');
    let x = 0;
    function moveBox() {
        x += 5;
        box.style.transform = `translateX(${x}px)`;
        if (x < 300) {
            requestAnimationFrame(moveBox);
        }
    }
    requestAnimationFrame(moveBox);
</script>

通过 requestAnimationFrame,动画会更加流畅,同时也能减少性能开销。

8. 跨浏览器兼容性

在操作 DOM 时,需要注意跨浏览器兼容性问题。虽然现代浏览器在 DOM 标准的支持上已经相当一致,但仍然存在一些差异。

8.1 事件处理兼容性

在旧版本的 Internet Explorer 中,事件处理的方式与标准浏览器有所不同。例如,在标准浏览器中使用 addEventListener 来绑定事件,而在 IE8 及更早版本中需要使用 attachEvent。为了实现跨浏览器兼容,可以编写一个兼容函数:

function addEvent(element, eventType, handler) {
    if (element.addEventListener) {
        element.addEventListener(eventType, handler);
    } else if (element.attachEvent) {
        element.attachEvent('on' + eventType, handler);
    }
}
// 使用示例
const myButton = document.getElementById('myButton');
addEvent(myButton, 'click', function() {
    console.log('按钮被点击了');
});

8.2 选择器兼容性

querySelectorquerySelectorAll 在旧版本浏览器(如 IE8 及更早版本)中不被支持。为了在这些浏览器中实现类似功能,可以使用一些 polyfill(填充代码)。例如,对于 querySelector 的 polyfill:

if (!document.querySelector) {
    document.querySelector = function(selector) {
        const elements = document.querySelectorAll(selector);
        return elements.length > 0? elements[0] : null;
    };
}
if (!document.querySelectorAll) {
    document.querySelectorAll = function(selector) {
        const elements = [];
        const allElements = document.getElementsByTagName('*');
        for (let i = 0; i < allElements.length; i++) {
            const element = allElements[i];
            if (element.matchesSelector(selector)) {
                elements.push(element);
            }
        }
        return elements;
    };
    // 为元素添加 matchesSelector 方法的 polyfill
    Element.prototype.matchesSelector = Element.prototype.matchesSelector ||
        Element.prototype.webkitMatchesSelector ||
        Element.prototype.mozMatchesSelector ||
        Element.prototype.msMatchesSelector;
}

通过这些 polyfill,我们可以在不支持原生 querySelectorquerySelectorAll 的浏览器中使用类似功能。

8.3 其他兼容性问题

在操作 DOM 的属性和方法时,还可能遇到其他兼容性问题。例如,classList 属性在旧版本浏览器中不被支持。可以为 Element.prototype 添加一个 classList 的 polyfill:

if (!('classList' in document.createElement('div'))) {
    Object.defineProperty(Element.prototype, 'classList', {
        get: function() {
            const self = this;
            function update(fn) {
                return function(value) {
                    const classes = self.className.split(/\s+/);
                    const index = classes.indexOf(value);
                    fn(classes, index, value);
                    self.className = classes.join(' ');
                };
            }
            return {
                add: update(function(classes, index, value) {
                    if (index === -1) classes.push(value);
                }),
                remove: update(function(classes, index) {
                    if (index!== -1) classes.splice(index, 1);
                }),
                toggle: update(function(classes, index, value) {
                    if (index === -1) classes.push(value);
                    else classes.splice(index, 1);
                }),
                contains: function(value) {
                    return this.className.split(/\s+/).indexOf(value)!== -1;
                },
                item: function(index) {
                    return this.className.split(/\s+/)[index];
                }
            };
        }
    });
}

通过这样的 polyfill,即使在不支持原生 classList 的浏览器中,也能像在现代浏览器中一样操作元素的 classList

9. 常见问题及解决方法

9.1 元素未加载完成

在 JavaScript 代码执行时,如果尝试获取或操作尚未加载完成的 DOM 元素,会导致错误。例如,在页面的 head 部分编写 JavaScript 代码获取 body 中的元素:

<head>
    <script>
        const element = document.getElementById('myElement');
        // 这里 myElement 可能为 null,因为页面还未加载到该元素
    </script>
</head>
<body>
    <div id="myElement">内容</div>
</body>

解决方法是确保在 DOM 加载完成后再执行相关操作。可以使用 DOMContentLoaded 事件:

<head>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const element = document.getElementById('myElement');
            console.log(element.textContent);
        });
    </script>
</head>
<body>
    <div id="myElement">内容</div>
</body>

9.2 内存泄漏

在动态添加和删除 DOM 元素时,如果处理不当,可能会导致内存泄漏。例如,在为元素绑定事件处理函数后,如果没有正确移除事件处理函数就删除元素,事件处理函数可能仍然持有对该元素的引用,导致元素无法被垃圾回收。

<div id="myElement"></div>
<script>
    const myElement = document.getElementById('myElement');
    myElement.addEventListener('click', function() {
        console.log('点击');
    });
    // 假设这里删除 myElement
    myElement.parentNode.removeChild(myElement);
    // 由于事件处理函数持有对 myElement 的引用,myElement 可能无法被垃圾回收
</script>

解决方法是在删除元素之前,先移除事件处理函数:

<div id="myElement"></div>
<script>
    const myElement = document.getElementById('myElement');
    const clickHandler = function() {
        console.log('点击');
    };
    myElement.addEventListener('click', clickHandler);
    // 在删除元素前移除事件处理函数
    myElement.removeEventListener('click', clickHandler);
    myElement.parentNode.removeChild(myElement);
</script>

9.3 性能瓶颈

如前文所述,频繁的 DOM 操作、重排和重绘等都可能导致性能瓶颈。除了前文提到的性能优化技巧外,还需要注意代码的整体结构和逻辑。例如,避免在循环中进行大量的 DOM 查询和修改操作。如果必须在循环中操作 DOM,可以将需要的 DOM 元素提前查询并缓存,然后在循环中操作缓存的元素。

// 不好的做法
for (let i = 0; i < 100; i++) {
    const element = document.getElementById('myElement');
    element.textContent = `值 ${i}`;
}
// 好的做法
const element = document.getElementById('myElement');
for (let i = 0; i < 100; i++) {
    element.textContent = `值 ${i}`;
}

另外,要合理使用定时器。如果使用 setInterval 频繁触发 DOM 操作,可能会导致性能问题。可以考虑使用 requestAnimationFrame 来替代 setInterval 进行动画相关的 DOM 操作,以提高性能。

通过对以上这些方面的深入理解和掌握,我们能够在 JavaScript 中高效地操作 DOM,创建出性能优良、用户体验良好的网页应用程序。无论是简单的页面交互还是复杂的单页应用,这些方法和技巧都将发挥重要作用。在实际开发中,需要根据具体的需求和场景,灵活运用这些知识,不断优化代码,以达到最佳的效果。