先有问题再有答案
浏览器有哪些好用的观察者?设计目的是什么?有什么优缺点?微任务&MutationObserver图片懒加载 & IntersectionObserver响应式&ResizeObserver适合哪些应用场景?具体是如何使用的?
MutationObserver
用途
用于监听DOM树的变化,包括元素的添加、删除、属性的改变以及文本节点的修改。它提供了一种异步的方式来监听这些变化,并在发生变化时执行相应的操作。
特点
- 高性能:MutationObserver 在处理大量快速 DOM 变化时更高效。
- 精确控制:提供详细的变化记录,可以监控到子树变化、属性变化和文本变化等。
- 异步执行:回调函数以异步方式调用,减少对主线程的阻塞,提升页面性能。
- 灵活性:支持多种配置选项以精确控制需要监听的变化类型。
注意:
过度使用可能会导致性能问题,因为每次DOM变化都会触发回调函数。
以下是一些可能导致性能问题的场景,以及如何避免这些问题的建议:
-
频繁的 DOM 变化:
当 DOM 元素频繁变动时,MutationObserver 会连续触发回调函数,这可能导致性能瓶颈。例如,在动态内容加载或实时通讯应用中,如果大量节点在短时间内被添加或删除,就会触发多次回调。解决方案:使用防抖(debounce)或节流(throttle)技术来限制回调函数的执行频率。这样可以减少回调函数的触发次数,从而减轻性能压力。
-
过度的观察:
如果设置了过多的观察点或观察了过多的节点变化类型(如同时观察childList、attributes和subtree),MutationObserver 可能会产生大量的回调,影响性能。解决方案:仅配置必要的观察选项,并在不再需要观察时使用
disconnect()方法停止观察。 -
复杂的回调逻辑:
如果回调函数执行复杂的逻辑或执行时间较长的操作,可能会影响页面的响应性。解决方案:优化回调函数中的代码,确保执行的操作尽可能高效。如果可能,将复杂的处理逻辑移出回调函数,或者使用 运行时性能优化方案 来处理计算密集型任务。
-
观察整个文档树:
观察整个文档树的变化(document.body或document)可能会导致大量的变化被捕捉,从而影响性能。解决方案:尽量缩小观察范围,只观察必要的节点。如果需要观察整个文档树,考虑使用更具体的配置选项,如
attributeFilter,来减少不必要的变化捕获。
适用场景
const targetNode = document.getElementById('container');
// 配置观察选项
const config = { attributes: true, childList: true, subtree: true };
// 回调函数
const callback = function(mutationsList, observer) {
for(let mutation of mutationsList) {
if (mutation.type === 'attributes') {
console.log(`The ${mutation.attributeName} attribute was modified.`);
}
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
}
};
// 创建观察者实例
const observer = new MutationObserver(callback);
// 传入目标节点和观察选项
observer.observe(targetNode, config);
// 添加事件监听器
document.getElementById('add').addEventListener('click', () => {
const newElement = document.createElement('p');
newElement.textContent = 'New content added';
targetNode.appendChild(newElement);
});
document.getElementById('change').addEventListener('click', () => {
targetNode.setAttribute('style', 'color: blue;');
});
vue源码中也曾经使用MutationObserver实现nextTick功能: 详解vue nextTick原理
MutationObserver属于浏览器实现 微任务 的一种方式,在JavaScript的事件循环中,微任务是优先级非常高的一类任务。微任务通常是在当前的同步任务(macrotask)之后,下一个渲染之前执行的。常见的微任务包括:Promise的.then处理函数和 MutationObserver 回调函数。
更多实现微任务的方式可以参考: js三座大山之异步六实现微任务的N种方式
IntersectionObserver
用途:
用于异步观察目标元素与其祖先元素或顶级文档视窗(viewport)的交叉状态。当目标元素进入或离开视口,或者与页面上的其他元素发生交叉时,会触发回调函数。
在没有这个api时 我们一般是通过监听scroll事件 不断检查当前元素与视口的坐标 判断是否出现在窗口内。这种方式实现起来较为繁琐而且因为scroll回调频繁容易引起性能问题。 浏览器提供了 IntersectionObserver可以帮助我们简单的实现这一功能。
特点
- 性能优化:IntersectionObserver 在浏览器优化的引擎中运行,定期检查目标元素的可见性,而不需要在每次滚动或尺寸变化时触发计算。这种机制减少了事件的触发频率,因此提升了性能。
- 易于使用:开发者只需定义观察逻辑一次,浏览器会处理所有计算,无需手动计算元素的可见性。
- 灵活的阈值设置:可以设置阈值来确定何时触发回调函数,例如,当元素的一定比例可见时。
注意:
尽管现代浏览器都已支持,但在一些旧的浏览器中不支持,需做额外的兼容性处理。
适用场景:
实现 图片懒加载 时,只有当图片进入视口时,才进行加载,从而优化页面加载速度和减少不必要的带宽消耗
// 获取所有需要懒加载的图片
const images = document.querySelectorAll('.lazy');
// 配置观察选项
const options = {
root: null,
rootMargin: '0px',
threshold: 0.1 // 当元素 10% 可见时触发
};
// 当观察到指定变化时,执行的回调函数
const callback = (entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('visible');
observer.unobserve(img); // 停止观察已经加载的图片
}
});
};
// 创建一个 IntersectionObserver 实例并传入回调函数
const observer = new IntersectionObserver(callback, options);
// 传入目标节点
images.forEach(img => observer.observe(img));
ResizeObserver
用途
ResizeObserver 是一种用于监控元素大小变化的 API。在现代 Web 开发中,许多应用场景需要根据元素大小调整布局或执行其他操作,而 ResizeObserver 提供了一种高效的方式来实时监测这些变化并作出反应。
特点
- 高性能:ResizeObserver 回调是批处理的,能在浏览器一帧内的多个变化合并处理,避免频繁的重新布局和回流。
- 简化代码:提供了一个直接监听元素尺寸变化的方法,避免了使用 window.resize 或者通过定时器轮询的复杂逻辑。
- 灵活性:可以监控任意 DOM 元素的大小变化,而不仅仅是窗口尺寸变化。
注意
使用不当可能会导致性能问题,特别是在监听大量元素时。现代主流浏览器支持良好,但在较旧的浏览器中可能不受支持或需要 polyfill。
适用场景:
在不同设备和窗口尺寸下,自动调整布局和样式。根据内容变化动态调整元素的大小 响应式布局等。
const h1Elem = document.querySelector("h1");
const pElem = document.querySelector("p");
const divElem = document.querySelector("body > div");
const slider = document.querySelector('input[type="range"]');
const checkbox = document.querySelector('input[type="checkbox"]');
divElem.style.width = "600px";
slider.addEventListener("input", () => {
divElem.style.width = `${slider.value}px`;
});
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.contentBoxSize) {
// Firefox implements `contentBoxSize` as a single content rect, rather than an array
const contentBoxSize = Array.isArray(entry.contentBoxSize)
? entry.contentBoxSize[0]
: entry.contentBoxSize;
h1Elem.style.fontSize = `${Math.max(
1.5,
contentBoxSize.inlineSize / 200,
)}rem`;
pElem.style.fontSize = `${Math.max(
1,
contentBoxSize.inlineSize / 600,
)}rem`;
} else {
h1Elem.style.fontSize = `${Math.max(
1.5,
entry.contentRect.width / 200,
)}rem`;
pElem.style.fontSize = `${Math.max(1, entry.contentRect.width / 600)}rem`;
}
}
console.log("Size changed");
});
resizeObserver.observe(divElem);
checkbox.addEventListener("change", () => {
if (checkbox.checked) {
resizeObserver.observe(divElem);
} else {
resizeObserver.unobserve(divElem);
}
});