JavaScript 事件处理与 DOM 操作:从基础到高级实践

361 阅读15分钟

JavaScript 事件处理与 DOM 操作:从基础到高级实践

一、DOM 基础与核心概念

1. DOM 树结构解析

DOM(文档对象模型)是 HTML 和 XML 文档的编程接口,它将文档表示为节点树:

<!DOCTYPE html>
<html>
<head>
    <title>DOM 示例</title>
</head>
<body>
    <h1>欢迎学习DOM</h1>
    <p class="content">这是一个段落</p>
</body>
</html>

对应的 DOM 树结构:

document
├─ html
   ├─ head
   │   └─ title
   │       └─ "DOM 示例"
   └─ body
       ├─ h1
       │   └─ "欢迎学习DOM"
       └─ p
           └─ "这是一个段落"

2. 节点类型详解

节点类型nodeType示例
元素节点 (Element)1<div> <p> <span>
属性节点 (Attribute)2class="example"
文本节点 (Text)3"Hello World"
注释节点 (Comment)8<!--- 注释 ---->
文档节点 (Document)9document
文档类型节点 (DocType)10 < !DOCTYPE html>
轻量级文档片段 (DocumentFragment)11不在主 DOM 中渲染,操作不会触发重排/重绘

3. DOM 查询方法

1. 经典查询API
// 获取单个元素
document.getElementById('header'); 

// 获取元素集合(动态)
const divs = document.getElementsByTagName('div');

// 获取类名元素集合(动态)
const items = document.getElementsByClassName('item');
2. 现代选择器API
// 查询单个元素
document.querySelector('#main .article:first-child');

// 查询所有匹配元素(静态)
document.querySelectorAll('button[data-action]');
3. 节点关系导航
属性描述
parentNode父节点
childNodes所有子节点(包含文本节点)
children仅元素子节点
firstChild/lastChild首/末子节点(含文本节点)
firstElementChild首个子元素节点
nextSibling下一个兄弟节点(含文本节点)
previousElementSibling上一个元素兄弟节点
// 父节点
const parent = element.parentNode;

// 子节点
const firstChild = element.firstChild;        // 包含文本节点
const firstElementChild = element.children[0]; // 仅元素节点

// 兄弟节点
const nextSibling = element.nextSibling;        // 包含文本节点
const nextElementSibling = element.nextElementSibling; // 仅元素节点

// 子节点列表
const children = element.children; // HTMLCollection (仅元素节点)
const childNodes = element.childNodes; // NodeList (所有类型节点)

4. DOM 操作方法

1. 内容操作方法
  1. 文本内容

    // 获取/设置文本(忽略HTML标签)
    element.textContent = "新文本内容";
    const text = element.textContent;
    
  2. HTML内容

    // 获取/设置包含HTML的内容
    element.innerHTML = "<strong>加粗文本</strong>";
    const htmlContent = element.innerHTML;
    
  3. 表单值

    // 获取/设置表单元素值
    input.value = "新值";
    const inputValue = input.value;
    
  4. 拓展

    textContent 与 innertext 区别:

特性textContentinnerText
标准DOM Level 1 (所有浏览器支持)非标准(但现代浏览器已实现)
获取内容返回所有文本(包括隐藏元素)返回「可见文本」(忽略隐藏元素)
性能更高(不触发重排计算)较低(需计算布局)
换行处理保留源码中的换行和空格合并空白符,按渲染结果返回

实际行为对比

示例代码:

<div id="example">
  Hello   World!
  <span style="display:none">Hidden Text</span>
  <p>Another Line</p>
</div>
const div = document.getElementById('example');

console.log(div.textContent);
// 输出(保留所有内容,包括隐藏元素和格式):
// "Hello   World!
//  Hidden Text
//  Another Line"

console.log(div.innerText);
// 输出(仅可见文本,合并空白符):
// "Hello World!
// Another Line"
2.属性操作方法
// 获取属性
const href = link.getAttribute('href');

// 设置属性
img.setAttribute('src', 'new-image.jpg');

// 移除属性
div.removeAttribute('data-temp');

// class 操作(推荐)
div.classList.add('active');      // 添加类
div.classList.remove('old');      // 移除类
div.classList.toggle('hidden');   // 切换类
div.classList.contains('active'); // 检查类
3. 样式操作方法

1.单个样式属性修改

// 语法:element.style.property = value
const div = document.querySelector('div');

// 设置样式(注意驼峰命名)
div.style.color = 'red';
div.style.backgroundColor = '#f0f0f0';
div.style.fontSize = '16px';  // 不是 font-size
div.style.marginTop = '10px'; // 不是 margin-top

// 获取样式(只能获取内联样式)
const color = div.style.color; // 仅返回通过JS设置或HTML内联的样式
  1. 批量设置样式
// 方法1:cssText(会覆盖全部内联样式)
element.style.cssText = 'color: red; background: yellow; font-size: 16px;';

// 方法2:setAttribute
element.setAttribute('style', 'color: red; background: yellow;');
//setAttribute('style', ...) 会重写整个 style 属性,导致重绘

拓展

  • style.setProperty() 只修改单个样式属性,性能更好
  • setAttribute('style', ...) 会重写整个 style 属性,导致重绘
特性element.style.setProperty()element.setAttribute()
作用对象专门操作元素的CSS 样式属性操作元素的任意 HTML 属性
所属 APICSSStyleDeclaration 接口Element 接口
CSS 属性名格式使用 CSS 标准命名 (background-color)使用 DOM 属性命名 (backgroundColor)
优先级修改内联样式(高优先级)修改 HTML 属性(不影响计算样式优先级)
适用场景动态修改样式修改/添加任意 HTML 属性
// 方法1:使用 style.setProperty (推荐)
div.style.setProperty('background-color', 'red');
div.style.setProperty('font-size', '16px', 'important');

// 方法2:使用 setAttribute
div.setAttribute('style', 'background-color: red; font-size: 16px;');

3.设置class类名

className 属性(直接操作类名字符串)

// 完全替换所有类
element.className = 'active box highlighted';

// 追加类(需手动处理字符串)
element.className += ' new-class';

classList API(推荐方式)

const div = document.querySelector('div');

// 添加类
div.classList.add('active', 'highlight'); // 可一次添加多个

// 移除类
div.classList.remove('old-class');

// 切换类(无则添加,有则移除)
div.classList.toggle('hidden');

// 检查类是否存在
if (div.classList.contains('active')) {
  // 执行操作
}

// 替换类
div.classList.replace('old-class', 'new-class');

4.获取计算样式

// 获取元素最终计算样式(包括内联、样式表和浏览器默认样式)
const style = window.getComputedStyle(element);

// 获取特定属性值
const width = style.getPropertyValue('width');
const fontSize = style.fontSize;

// 注意:返回的值是计算后的绝对值
// 如 margin 返回 "10px" 而不是百分比或 em
4. 元素创建与操作
// 创建新元素
const newDiv = document.createElement('div');
newDiv.className = "box";

// 添加文本节点
const textNode = document.createTextNode("Hello World");
newDiv.appendChild(textNode);

// 插入元素
parent.appendChild(newDiv);             // 末尾插入
parent.insertBefore(newDiv, reference); // 指定位置插入

// 克隆元素
const clonedElement = original.cloneNode(true); // true 表示深克隆

// 替换元素
parent.replaceChild(newElement, oldElement);

// 删除元素
parent.removeChild(element); // 传统方法
element.remove();            // 现代方法(推荐)
特性element.append()element.appendChild()
参数类型接受多个节点或字符串只接受单个节点
返回值无返回值 (undefined)返回追加的节点
字符串处理自动将字符串转为文本节点不接受字符串参数
浏览器支持较新方法 (IE 不支持)所有浏览器支持
链式调用不支持 (无返回值)支持 (返回节点可继续操作)
特性replaceChildreplaceWithremoveChildremove
调用对象父节点要被替换的节点父节点要被移除的节点
参数(新节点, 旧节点)多个节点或字符串要移除的子节点
返回值被替换的旧节点undefined被移除的节点undefined
字符串支持不支持支持不适用不适用
浏览器支持所有浏览器IE不支持所有浏览器IE不支持
链式调用可以(返回旧节点)不可以可以(返回被移除节点)不可以
// 创建新元素
const newDiv = document.createElement('div');
newDiv.className = 'alert';
newDiv.innerHTML = '<strong>提示!</strong> 重要消息';

// 添加样式
newDiv.style.cssText = `
    position: fixed;
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
    padding: 10px 20px;
    background: #ffeb3b;
    border-radius: 4px;
`;

// 添加属性
newDiv.setAttribute('role', 'alert');
newDiv.dataset.type = 'warning'; // 自定义数据属性

// 添加到DOM
document.body.append(newDiv);

// 克隆元素
const clonedDiv = newDiv.cloneNode(true); // 深度克隆

// 替换元素
const oldDiv = document.querySelector('.old');
oldDiv.replaceWith(newDiv);

// 移除元素
newDiv.remove();

二、事件处理机制深度解析

1. 事件流:捕获与冒泡

<div id="grandparent">
    <div id="parent">
        <button id="child">点击我</button>
    </div>
</div>
const elements = ['grandparent', 'parent', 'child'].map(id => document.getElementById(id));

// 捕获阶段 (从外向内)
elements.forEach(el => {
    el.addEventListener('click', () => {
        console.log(`捕获: ${el.id}`);
    }, true); // 第三个参数为true表示捕获阶段
});

// 冒泡阶段 (从内向外)
elements.forEach(el => {
    el.addEventListener('click', () => {
        console.log(`冒泡: ${el.id}`);
    }); // 默认冒泡阶段
});

点击按钮后的输出顺序:

捕获: grandparent
捕获: parent
捕获: child
冒泡: child
冒泡: parent
冒泡: grandparent

2. 事件对象详解

事件处理函数接收的事件对象包含丰富属性和方法:

document.getElementById('myBtn').addEventListener('click', function(event) {
    console.log('事件类型:', event.type); // "click"
    console.log('目标元素:', event.target); // 实际触发元素
    console.log('当前元素:', event.currentTarget); // 处理事件的元素
    console.log('事件阶段:', event.eventPhase); // 2 (目标阶段)
    console.log('时间戳:', event.timeStamp);
    console.log('坐标:', {x: event.clientX, y: event.clientY});
  
    // 阻止默认行为
    event.preventDefault();
  
    // 停止冒泡
    event.stopPropagation();
  
    // 立即停止其他监听器执行
    event.stopImmediatePropagation();
});

3. 常用事件类型分类

事件类别常见事件
鼠标事件click, dblclick, mouseenter, mouseleave, mousemove, contextmenu
键盘事件keydown, keyup, keypress
表单事件submit, change, input, focus, blur, reset
窗口事件load, resize, scroll, hashchange, beforeunload
触摸事件touchstart, touchmove, touchend
多媒体事件play, pause, volumechange, ended
剪切板事件copy, cut, paste

4.事件注册

传统 HTML 事件属性(不推荐)

<button onclick="handleClick()">点击</button>
<script>
  function handleClick() {
    console.log("按钮被点击");
  }
</script>

缺点

  • HTML 与 JavaScript 强耦合
  • 只能绑定一个处理函数
  • 不易维护

** DOM 属性事件绑定**

const btn = document.querySelector("button");
btn.onclick = function() {
  console.log("第一次点击");
};

// 会覆盖之前的事件
btn.onclick = function() {
  console.log("第二次点击");
};

缺点

  • 同类型事件只能绑定一个处理函数
  • 容易被覆盖

** addEventListener(推荐)**

const btn = document.querySelector("button");

function firstClick() {
  console.log("第一次点击");
}

function secondClick() {
  console.log("第二次点击");
}

// 可以绑定多个事件
btn.addEventListener("click", firstClick);
btn.addEventListener("click", secondClick,{
  capture: false,    // 是否捕获阶段触发
  once: true,        // 只执行一次
  passive: true      // 声明不会调用preventDefault()
});

事件移除

// 必须引用同一个函数
btn.removeEventListener("click", firstClick);

一次性事件

btn.addEventListener("click", function() {
  console.log("只触发一次");
}, { once: true });

// 或
function handleOnce(){
  console.log("只执行一次 ");
  btn.removeEventListener('click', handleOnce)
}
btn.addEventListener('click', handleOnce)

三、高级 DOM 操作技巧

1. 元素创建与修改

// 创建新元素
const newDiv = document.createElement('div');
newDiv.className = 'alert';
newDiv.innerHTML = '<strong>提示!</strong> 重要消息';

// 添加样式
newDiv.style.cssText = `
    position: fixed;
    top: 20px;
    left: 50%;
    transform: translateX(-50%);
    padding: 10px 20px;
    background: #ffeb3b;
    border-radius: 4px;
`;

// 添加属性
newDiv.setAttribute('role', 'alert');
newDiv.dataset.type = 'warning'; // 自定义数据属性

// 添加到DOM
document.body.append(newDiv);

// 克隆元素
const clonedDiv = newDiv.cloneNode(true); // 深度克隆

// 替换元素
const oldDiv = document.querySelector('.old');
oldDiv.replaceWith(newDiv);

// 移除元素
newDiv.remove();

2. 性能优化技巧

// 1. 批量DOM修改使用文档片段 -- document.createDocumentFragment创建的元素在浏览器中不显示,类似于<><>
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    const item = document.createElement('div');
    item.textContent = `Item ${i}`;
    fragment.appendChild(item);
}
document.getElementById('container').appendChild(fragment);

// 2. 使用requestAnimationFrame优化动画
function animate() {
    const element = document.getElementById('animated');
    let pos = 0;
  
    function frame() {
        if (pos === 300) {
            cancelAnimationFrame(animationId);
        } else {
            pos++;
            element.style.left = pos + 'px';
            animationId = requestAnimationFrame(frame);
        }
    }
  
    let animationId = requestAnimationFrame(frame);
}

// 3. 事件委托减少监听器数量
document.getElementById('list').addEventListener('click', function(event) {
    if (event.target.tagName === 'LI') {
        console.log('点击了:', event.target.textContent);
    }
});

// 4. 使用IntersectionObserver实现懒加载
const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            observer.unobserve(img);
        }
    });
}, { threshold: 0.1 });

document.querySelectorAll('img.lazy').forEach(img => {
    observer.observe(img);
});

四、现代事件处理模式

1. 自定义事件系统

// 创建自定义事件
const myEvent = new CustomEvent('myevent', {
    detail: { message: '自定义事件数据' },
    bubbles: true,
    cancelable: true
});

// 监听自定义事件
document.addEventListener('myevent', (e) => {
    console.log('收到自定义事件:', e.detail.message);
});

// 触发事件
document.dispatchEvent(myEvent);

// 元素特定事件
const button = document.getElementById('myButton');
button.addEventListener('specialClick', (e) => {
    console.log('特殊点击:', e.detail);
});

button.dispatchEvent(new CustomEvent('specialClick', {
    detail: { count: 5 }
}));

2. 事件委托高级模式

// 动态过滤委托
document.getElementById('gallery').addEventListener('click', function(e) {
    // 查找最近的匹配元素
    const thumbnail = e.target.closest('.thumbnail');
    if (!thumbnail) return;
  
    if (e.ctrlKey) {
        thumbnail.classList.toggle('selected');
    } else {
        showFullImage(thumbnail.dataset.imageId);
    }
});

// 多事件类型委托
const eventTypes = ['click', 'touchstart'];
const handler = function(e) {
    console.log(`${e.type}事件在`, e.target);
};

eventTypes.forEach(type => {
    document.getElementById('controls').addEventListener(type, handler);
});

// 移除多个事件
function removeEvents() {
    eventTypes.forEach(type => {
        document.getElementById('controls').removeEventListener(type, handler);
    });
}

3. Promise 与事件结合

function waitForEvent(element, eventType) {
    return new Promise((resolve) => {
        const listener = (e) => {
            element.removeEventListener(eventType, listener);
            resolve(e);
        };
        element.addEventListener(eventType, listener);
    });
}

// 使用示例
async function handleButton() {
    const button = document.getElementById('asyncBtn');
    button.textContent = '等待点击...';
  
    const event = await waitForEvent(button, 'click');
    console.log('按钮被点击了!', event);
  
    button.textContent = '已点击!';
    setTimeout(() => {
        button.textContent = '点击我';
    }, 2000);
}

五、实战应用案例

1. 可拖拽排序列表

class DraggableList {
    constructor(listElement) {
        this.list = listElement;
        this.setupEvents();
    }
  
    setupEvents() {
        this.list.addEventListener('mousedown', this.handleMouseDown.bind(this));
        document.addEventListener('mousemove', this.handleMouseMove.bind(this));
        document.addEventListener('mouseup', this.handleMouseUp.bind(this));
  
        // 触摸支持
        this.list.addEventListener('touchstart', this.handleTouchStart.bind(this));
        document.addEventListener('touchmove', this.handleTouchMove.bind(this));
        document.addEventListener('touchend', this.handleTouchEnd.bind(this));
    }
  
    handleMouseDown(e) {
        if (!e.target.classList.contains('draggable')) return;
  
        this.draggedItem = e.target;
        this.startY = e.clientY;
        this.draggedItem.classList.add('dragging');
  
        // 创建占位符
        this.placeholder = document.createElement('div');
        this.placeholder.className = 'placeholder';
        this.draggedItem.parentNode.insertBefore(
            this.placeholder,
            this.draggedItem.nextSibling
        );
    }
  
    handleMouseMove(e) {
        if (!this.draggedItem) return;
  
        e.preventDefault();
        const y = e.clientY;
        const deltaY = y - this.startY;
  
        // 移动元素
        this.draggedItem.style.transform = `translateY(${deltaY}px)`;
  
        // 检查是否需要交换位置
        const items = [...this.list.children];
        const draggedIndex = items.indexOf(this.draggedItem);
        const overIndex = items.findIndex(item => {
            if (item === this.draggedItem || item === this.placeholder) return false;
            const rect = item.getBoundingClientRect();
            return y > rect.top + rect.height / 2;
        });
  
        if (overIndex !== -1 && overIndex !== draggedIndex) {
            if (overIndex > draggedIndex) {
                this.list.insertBefore(this.placeholder, items[overIndex + 1]);
            } else {
                this.list.insertBefore(this.placeholder, items[overIndex]);
            }
        }
    }
  
    handleMouseUp() {
        if (!this.draggedItem) return;
  
        // 放置元素到占位符位置
        this.list.insertBefore(this.draggedItem, this.placeholder);
        this.placeholder.remove();
        this.draggedItem.style.transform = '';
        this.draggedItem.classList.remove('dragging');
  
        this.draggedItem = null;
        this.placeholder = null;
    }
  
    // 触摸事件处理 (类似鼠标事件)
    handleTouchStart(e) {
        this.handleMouseDown({
            target: e.target,
            clientY: e.touches[0].clientY,
            preventDefault: () => e.preventDefault()
        });
    }
  
    handleTouchMove(e) {
        this.handleMouseMove({
            clientY: e.touches[0].clientY,
            preventDefault: () => e.preventDefault()
        });
    }
  
    handleTouchEnd() {
        this.handleMouseUp();
    }
}

// 使用
new DraggableList(document.getElementById('sortable-list'));

2. 表单实时验证系统

class FormValidator {
    constructor(form, config) {
        this.form = form;
        this.config = config;
        this.errors = {};
        this.init();
    }
  
    init() {
        // 设置初始状态
        Object.keys(this.config.fields).forEach(field => {
            this.errors[field] = [];
            const input = this.form.querySelector(`[name="${field}"]`);
            this.setupFieldValidation(input, field);
        });
  
        // 表单提交验证
        this.form.addEventListener('submit', (e) => {
            if (!this.validateAll()) {
                e.preventDefault();
                this.showGlobalErrors();
            }
        });
    }
  
    setupFieldValidation(input, field) {
        const rules = this.config.fields[field];
  
        // 实时验证
        input.addEventListener('input', () => {
            this.validateField(input, field, rules);
        });
  
        // 失去焦点验证
        input.addEventListener('blur', () => {
            this.validateField(input, field, rules, true);
        });
    }
  
    validateField(input, field, rules, showErrors = false) {
        const value = input.value.trim();
        this.errors[field] = [];
  
        // 必填验证
        if (rules.required && !value) {
            this.errors[field].push(this.config.messages.required);
        }
  
        // 正则验证
        if (value && rules.pattern && !rules.pattern.test(value)) {
            this.errors[field].push(this.config.messages.pattern);
        }
  
        // 自定义验证
        if (value && rules.validate) {
            const customError = rules.validate(value, this.form);
            if (customError) {
                this.errors[field].push(customError);
            }
        }
  
        // 更新UI
        this.updateFieldStatus(input, field, showErrors);
    }
  
    updateFieldStatus(input, field, showErrors) {
        const errorContainer = input.nextElementSibling;
  
        if (this.errors[field].length > 0) {
            input.classList.add('invalid');
            if (showErrors) {
                errorContainer.textContent = this.errors[field][0];
                errorContainer.style.display = 'block';
            }
        } else {
            input.classList.remove('invalid');
            input.classList.add('valid');
            errorContainer.style.display = 'none';
        }
    }
  
    validateAll() {
        let isValid = true;
  
        Object.keys(this.config.fields).forEach(field => {
            const input = this.form.querySelector(`[name="${field}"]`);
            const rules = this.config.fields[field];
  
            this.validateField(input, field, rules, true);
            if (this.errors[field].length > 0) {
                isValid = false;
            }
        });
  
        return isValid;
    }
  
    showGlobalErrors() {
        const globalError = this.form.querySelector('.global-error');
        if (!globalError) return;
  
        const errorCount = Object.values(this.errors)
            .reduce((sum, errors) => sum + errors.length, 0);
  
        if (errorCount > 0) {
            globalError.textContent = this.config.messages.global;
            globalError.style.display = 'block';
        } else {
            globalError.style.display = 'none';
        }
    }
}

// 使用示例
const validator = new FormValidator(document.getElementById('signup-form'), {
    fields: {
        username: {
            required: true,
            pattern: /^[a-zA-Z0-9_]{4,16}$/,
            validate: (value) => {
                if (value.toLowerCase() === 'admin') {
                    return '不能使用保留用户名';
                }
            }
        },
        email: {
            required: true,
            pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
        },
        password: {
            required: true,
            pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/
        }
    },
    messages: {
        required: '必填字段',
        pattern: '格式不正确',
        global: '请修正表单中的错误'
    }
});

六、性能优化与最佳实践

1. 事件处理优化策略

策略说明代码示例
事件委托减少事件监听器数量container.addEventListener('click', (e) => { if(e.target.matches('.btn')) {...} });
被动事件监听提高滚动性能,告诉浏览器不会调用preventDefault()window.addEventListener('scroll', onScroll, { passive: true });
防抖与节流控制事件触发频率见下方详细实现
及时移除监听器避免内存泄漏element.removeEventListener('click', handler);
requestIdleCallback在浏览器空闲时处理非关键任务requestIdleCallback(() => { processLowPriorityData(); });

2. 防抖与节流实现

// 防抖 (最后一次操作后等待一段时间执行)
function debounce(fn, delay) {
    let timeoutId;
    return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            fn.apply(this, args);
        }, delay);
    };
}

// 节流 (固定时间间隔执行)
function throttle(fn, interval) {
    let lastTime = 0;
    return function(...args) {
        const now = Date.now();
        if (now - lastTime >= interval) {
            fn.apply(this, args);
            lastTime = now;
        }
    };
}

// 使用示例
window.addEventListener('resize', debounce(() => {
    console.log('调整窗口大小结束');
}, 300));

document.addEventListener('mousemove', throttle((e) => {
    console.log('鼠标位置:', e.clientX, e.clientY);
}, 100));

3. DOM 操作优化建议

  1. 批量读取/写入:避免布局抖动

    // 不好 - 导致多次重排
    element.style.width = '100px';
    element.style.height = '200px';
    
    // 好 - 使用cssText或class
    element.style.cssText = 'width:100px; height:200px;';
    // 或
    element.classList.add('new-size');
    
  2. 使用文档片段:减少重绘次数

DocumentFragment 是一个轻量级的文档对象,它不属于主 DOM 树的一部分,但可以像普通 DOM 一样存储和操作节点。它是批量 DOM 操作时的性能优化利器。

** 核心特性**

  1. 内存中的文档片段:不在主 DOM 中渲染,操作不会触发重排/重绘
  2. 批量插入节点:一次性将多个节点添加到 DOM,减少页面回流
  3. 临时容器:作为节点操作的临时工作区
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    const item = document.createElement('div');
    item.textContent = `Item ${i}`;
    fragment.appendChild(item);
}
document.body.appendChild(fragment);
  1. 缓存DOM查询结果:避免重复查询

    // 不好
    for (let i = 0; i < 10; i++) {
        document.getElementById('list').appendChild(createItem(i));
    }
    
    // 好
    const list = document.getElementById('list');
    for (let i = 0; i < 10; i++) {
        list.appendChild(createItem(i));
    }
    
  2. 使用虚拟DOM:对于复杂UI

    // 使用React等库或小型虚拟DOM实现
    function updateDOM(newVTree, oldVTree, parent) {
        // 比较虚拟DOM差异并最小化更新
    }
    

七、现代浏览器 API 进阶

1. MutationObserver

用于异步监测DOM树的变化

// 1. 创建观察者
const observer = new MutationObserver((mutations) => {
    mutations.forEach(mutation => {
        if (mutation.type === 'childList') {
            console.log('子节点变化:', mutation.addedNodes, mutation.removedNodes);
        } else if (mutation.type === 'attributes') {
            console.log(`属性 ${mutation.attributeName} 变化`);
        }
    });
});

// 2. 配置观察选项
observer.observe(document.getElementById('app'), {
  attributes: true,       // 观察属性变化
  childList: true,       // 观察子节点变化
  subtree: true,         // 观察所有后代节点
  attributeOldValue: true, // 记录属性旧值
  characterData: true    // 观察文本内容变化
});

// 3. 停止观察
// observer.disconnect();

2. IntersectionObserver

IntersectionObserver 是现代浏览器提供的 API,用于高效监测目标元素是否进入或离开视口(Viewport)或某个父容器,替代传统的滚动事件监听 + getBoundingClientRect() 计算方式。

基本用法

1.创建 IntersectionObserver

const observer = new IntersectionObserver(callback, options);
  • callback:当目标元素进入或离开视口时触发的回调函数。
  • options(可选):配置观察行为。

每个 entry 包含以下关键信息:

属性类型说明
targetElement被观察的目标元素
isIntersectingboolean是否进入视口
intersectionRationumber当前可见比例(0~1)
intersectionRectDOMRectReadOnly元素与视口的交叉区域
rootBoundsDOMRectReadOnly根元素(root)的边界
timenumber触发时间(毫秒)

options包含以下信息:

选项类型默认值说明
rootElementnull(视口)指定观察的父容器(默认为浏览器视口)
rootMarginstring"0px"扩展或缩小观察范围(类似 CSSmargin
thresholdnumber[][0]触发回调的可见比例阈值(如[0, 0.5, 1]

示例

2.观察目标元素

const options = {
  root: document.querySelector("#scrollArea"), // 观察某个滚动容器
  rootMargin: "10px", // 提前 10px 触发回调
  threshold: [0, 0.5, 1] // 元素 0%、50%、100% 可见时触发
};
const target = document.querySelector("#target");
observer.observe(target); // 开始观察

3.回调函数

回调函数接收两个参数:

  • entries:一个数组,包含所有被观察元素的信息。
  • observer:当前的 IntersectionObserver 实例。
const callback = (entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log("元素进入视口", entry.target);
    } else {
      console.log("元素离开视口", entry.target);
    }
  });
};
  1. 停止观察
observer.unobserve(target); // 停止观察单个元素
observer.disconnect();      // 停止所有观察
常见应用场景

图片懒加载(Lazy Load)

<img data-src="image.jpg" class="lazy" />
const lazyImages = document.querySelectorAll(".lazy");
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src; // 加载图片
      observer.unobserve(img);   // 停止观察已加载的图片
    }
  });
});

lazyImages.forEach(img => observer.observe(img));

无限滚动(Infinite Scroll)

const sentinel = document.querySelector("#loadMoreTrigger");
const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    loadMoreData(); // 加载更多内容
  }
});

observer.observe(sentinel);

广告曝光统计

const ad = document.querySelector("#ad-banner");
const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    logAdImpression(); // 记录广告曝光
    observer.unobserve(ad);
  }
}, { threshold: 0.5 }); // 至少 50% 可见才触发

observer.observe(ad);

动画触发(Scroll Animation)

const animatedElements = document.querySelectorAll(".animate-on-scroll");
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add("fade-in");
      observer.unobserve(entry.target);
    }
  });
}, { threshold: 0.1 }); // 10% 可见时触发动画

animatedElements.forEach(el => observer.observe(el));

实现懒加载和曝光统计

const io = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            img.onload = () => img.classList.add('loaded');
            io.unobserve(img);
  
            // 曝光统计
            logImpression(img.dataset.id);
        }
    });
}, {
    threshold: 0.1,
    rootMargin: '0px 0px 100px 0px' // 提前100px加载
});

document.querySelectorAll('img.lazy').forEach(img => {
    io.observe(img);
});
与传统方法的对比
方法性能代码复杂度适用场景
IntersectionObserver✅ 高性能(浏览器优化)✅ 简单现代浏览器
scroll 事件 + getBoundingClientRect()❌ 频繁计算,性能差❌ 复杂兼容旧浏览器

3. ResizeObserver

ResizeObserver 是一个浏览器 API,用于高效监测 DOM 元素的尺寸变化(如 widthheight),替代传统的 window.resize 事件或 MutationObserver 监听方式

** 基本用法**

** 创建 ResizeObserver**

const observer = new ResizeObserver(callback);
  • callback:当目标元素尺寸变化时触发的回调函数。

观察目标元素

const target = document.querySelector("#target");
observer.observe(target); // 开始观察

回调函数

回调函数接收两个参数:

  • entries:一个数组,包含所有被观察元素的尺寸信息。
  • observer:当前的 ResizeObserver 实例。
const callback = (entries) => {
  entries.forEach(entry => {
    console.log("元素尺寸变化:", entry.contentRect);
  });
};

每个 entry 包含以下关键信息:

属性类型说明
targetElement被观察的目标元素
contentRectDOMRectReadOnly元素的尺寸和位置信息
borderBoxSizeResizeObserverSize元素的边框盒尺寸(含borderpadding
contentBoxSizeResizeObserverSize元素的内容盒尺寸(不含borderpadding

contentRect 结构

{
  width: 200,   // 元素宽度(px)
  height: 100,  // 元素高度(px)
  x: 10,        // 相对于视口的 X 坐标
  y: 20,        // 相对于视口的 Y 坐标
  top: 20,      // 等同于 y
  right: 210,   // x + width
  bottom: 120,  // y + height
  left: 10      // 等同于 x
}

borderBoxSizecontentBoxSize

// 可能返回数组(某些浏览器支持多片段布局,如多列)
const { blockSize, inlineSize } = entry.contentBoxSize[0];
console.log("内容宽度:", inlineSize, "内容高度:", blockSize);

停止观察

observer.unobserve(target); // 停止观察单个元素
observer.disconnect();      // 停止所有观察
常见应用场景

响应式布局调整

const ro = new ResizeObserver(entries => {
    for (let entry of entries) {
        const { width, height } = entry.contentRect;
        console.log(`元素尺寸变化: ${width}x${height}`);
  
        if (width < 600) {
            entry.target.classList.add('mobile-layout');
        } else {
            entry.target.classList.remove('mobile-layout');
        }
    }
});

ro.observe(document.getElementById('responsive-container'));

图表/Canvas 自适应

const chartCanvas = document.querySelector("#chart");
const ctx = chartCanvas.getContext("2d");

const observer = new ResizeObserver((entries) => {
  const { width, height } = entries[0].contentRect;
  chartCanvas.width = width;
  chartCanvas.height = height;
  redrawChart(); // 重新绘制图表
});

observer.observe(chartCanvas);

动态调整 iframe 高度

const iframe = document.querySelector("iframe");
const observer = new ResizeObserver((entries) => {
  const { height } = entries[0].contentRect;
  iframe.style.height = `${height}px`;
});

observer.observe(iframe.contentDocument.body);

检测元素隐藏/显示

const element = document.querySelector("#dynamicElement");
const observer = new ResizeObserver((entries) => {
  const { width, height } = entries[0].contentRect;
  if (width === 0 || height === 0) {
    console.log("元素被隐藏");
  } else {
    console.log("元素显示");
  }
});

observer.observe(element);
与传统方法的对比
方法性能适用场景缺点
ResizeObserver✅ 高效(浏览器优化)监测任意元素尺寸变化不兼容 IE
window.resize + getBoundingClientRect()❌ 频繁计算,性能差仅窗口尺寸变化不适用于单个元素
MutationObserver❌ 不适合尺寸监测DOM 结构变化无法精确监测尺寸

总结与提升路径

核心要点回顾

  1. DOM 操作
    • 理解DOM树结构和节点类型
    • 掌握查询、创建、修改和删除元素的API
    • 使用文档片段和批量操作优化性能
  2. 事件处理
    • 理解捕获和冒泡机制
    • 熟练使用事件委托模式
    • 掌握常用事件类型和事件对象属性
  3. 现代实践
    • 使用MutationObserver监控DOM变化
    • 实现懒加载和曝光统计
    • 应用防抖和节流优化性能

进阶学习建议

  1. 深入浏览器工作原理
    • 了解重绘(repaint)和重排(reflow)机制
    • 学习浏览器事件循环(event loop)
  2. 探索现代框架
    • 学习React/Vue的虚拟DOM实现
    • 理解现代框架的事件系统

实战练习建议

  1. 实现一个完整的可排序、可拖拽的看板应用
  2. 开发一个带有实时验证和异步检查的表单系统
  3. 创建一个无限滚动的图片画廊,使用懒加载技术
  4. 实现一个自定义的右键上下文菜单系统
  5. 开发一个使用Web Workers处理大量DOM更新的应用

通过不断实践这些技术和模式,您将能够构建高性能、交互丰富的现代Web应用程序。