JavaScript中5個重要的Observer函數(shù)小結(jié)
一、前言
瀏覽器為開發(fā)者提供了功能豐富的Observer,在這篇文章中,我們將深入研究這些常見的瀏覽器 Observer,剖析它們的作用、用法以及它們在 Web 開發(fā)中的應(yīng)用場景。
二、MutationObserver
MutationObserver用于監(jiān)聽DOM對象的變更(包括子節(jié)點),當節(jié)點屬性發(fā)生變化,或執(zhí)行增刪改操作時執(zhí)行對應(yīng)的callback。
MutationObserver為我們提供了一種十分方便的監(jiān)聽DOM變化的方式。
2.1、基本使用
// Observer需要一個用于監(jiān)聽的目標DOM
const targetNode = document.getElementById("app");
//用于確定mutation監(jiān)聽變化的范圍
const config = {
attributes: true, // 監(jiān)聽目標節(jié)點的屬性變化,例如id,class等屬性
childList: true, // 除目標節(jié)點外還要監(jiān)聽目標節(jié)點的直接子節(jié)點
subtree: true, // subtree的范圍大于childList,還包括子節(jié)點children
characterData: true // 監(jiān)聽TextNode需要額外配置,默認TextNode變化不會觸發(fā)callback
};
// 當觀察到變動時執(zhí)行的回調(diào)函數(shù),mutationsList包含本次變更的信息
const callback = function (mutationsList, observer) {
console.log(mutationsList)
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
2.2、API介紹
2.2.1、observe
observe用于開啟對某個DOM的監(jiān)聽,一個MutationObserver可以通過多次調(diào)用observe監(jiān)聽多個DOM的變化。
當變化發(fā)生時MutationObserver會將一個或多個mutation對象傳給callback的第一個參數(shù),mutation對象內(nèi)包含本次變更的相關(guān)信息下面看一下mutation的結(jié)構(gòu)
{
addedNodes: [], //新增DOM時會包含被新增的DOM
attributeName: "id", //本次變更的屬性名
attributeNamespace: null, //命名空間URI,一般用不到
nextSibling: null, //當存在添加/刪除節(jié)點的操作時會存在nextSibling/previousSibling, 引用上一個/下一個兄弟節(jié)點
previousSibling: null,
oldValue: null,
removedNodes: [],
target: Text,
type: "characterData" //變更類型,如characterData,childList等
}
2.2.2、disconnect
observer.disconnect()
調(diào)用observer.disconnect后Observer將不再監(jiān)聽target,如果不需要監(jiān)聽請及時調(diào)用該方法,以免產(chǎn)生預(yù)期之外的行為以及內(nèi)存泄漏。
2.2.3、takeRecords
takeRecords用于獲取在事件隊列中但還未傳遞給callback的mutation對象,通常使用在調(diào)用disconnect時又不想丟失之前的mutationRecords(如果mutation連續(xù)觸發(fā),可能出現(xiàn)mutation還在隊列中但未傳遞給callback的情況)。
2.3、常見場景
對于需要監(jiān)聽DOM變化的場景可考慮使用MutationObserver,利于用于Tag group內(nèi)元素的動態(tài)渲染,下面使用MutationObserver實現(xiàn)一個簡單的Todo List
2.3.1、源碼
<!DOCTYPE html>
<html>
<head>
<title>MutationObserver To-Do List Demo</title>
<style>
#todo-list {
list-style-type: none;
}
</style>
</head>
<body>
<h1>待辦事項列表</h1>
<ul id="todo-list">
<li>完成作業(yè)</li>
<li>購物</li>
</ul>
<button id="addTask">添加任務(wù)</button>
<button id="removeTask">移除任務(wù)</button>
<p id="taskCount">任務(wù)數(shù)量:2</p>
<script>
const todoList = document.getElementById('todo-list');
const taskCount = document.getElementById('taskCount');
const observer = new MutationObserver((mutationsList, observer) => {
mutationsList.forEach((mutation) => {
if (mutation.type === 'childList') {
updateTaskCount();
}
});
});
const config = { childList: true };
observer.observe(todoList, config);
document.getElementById('addTask').addEventListener('click', () => {
const newTask = document.createElement('li');
newTask.textContent = '新任務(wù)';
todoList.appendChild(newTask);
});
document.getElementById('removeTask').addEventListener('click', () => {
const tasks = todoList.getElementsByTagName('li');
if (tasks.length > 0) {
todoList.removeChild(tasks[0]);
}
});
function updateTaskCount() {
const tasks = todoList.getElementsByTagName('li');
taskCount.textContent = `任務(wù)數(shù)量:${tasks.length}`;
}
</script>
</body>
</html>
2.3.2、預(yù)覽
在線源碼請點擊【前往】
2.3.3、效果

三、IntersectionObserver
IntersectionObserver用于監(jiān)聽一個元素的可見比例(一個DOM元素被另一個DOM元素遮擋百分比)變化。
3.1、基本使用
const target = document.getElementById('app');
const options = {
root: rootTarget, // 相對于某個元素進行遮擋計算
rootMargin: '0px', // 進行計算的邊界范圍,通過rootMargin可以實現(xiàn)提前計算或延遲計算(相對于root原本尺寸)的效果
threshold: 0.5 // 觸發(fā)callback時的遮擋比例,0.5代表元素被遮擋50%時觸發(fā)callback。由于瀏覽器事件循環(huán)機制的影響,callback觸發(fā)時遮擋比例通常不會是精確的50%。
};
const intersectionObserver = new IntersectionObserver((entries, observer) => {
//和MutationObserver相同,也是產(chǎn)生一個array
entries.forEach(entry => {
console.log(entry)
});
}, options);
intersectionObserver.observe(target);
3.2、API介紹
3.2.1、observe & options
observe方法用于啟動一個Observer對DOM元素的監(jiān)聽。在創(chuàng)建IntersectionObserver時可以通過傳入option改變監(jiān)聽的行為。
const options = {
root: root,
rootMargin: '100px',
threshold: 0.7
};
在上面的配置中,通過配置rootMargin為100px在target距離root元素100px時即可判定為被遮擋,通過threshold設(shè)置為0.7,當遮擋比例查過70%時執(zhí)行callback。
3.2.2、entry
callback第一個param是entry對象構(gòu)成的array,entry包含了觸發(fā)callback時DOM的位置信息
//被監(jiān)聽DOM元素的Rect信息
boundingClientRect: {
bottom: 208
height: 200
left: 8
right: 208
top: 8
width: 200
x: 8
y: 8
}
intersectionRatio: 1 //交叉比例
// 被監(jiān)聽元素與Root元素交叉部分矩形的Rect信息。
intersectionRect: {
bottom: 208,
height: 200,
left: 8,
right: 208,
top: 8,
width: 200,
x: 8,
y: 8
},
// 是否處于交叉狀態(tài)
isIntersecting: true,
isVisible: false,
// Root元素的Rect信息
rootBounds: {
bottom: 606,
height: 606,
left: 0,
right: 476,
top: 0,
width: 476,
x: 0,
y: 0
},
// root元素
target: div#target,
time: 49.09999990463257
3.3、常見場景
乍一看IntersectionObserver好像沒啥用,單這個Api在某些場景下十分好用。
比如我們有一個通過sticky固定在屏幕頂部的header元素,我們希望在觸發(fā)sticky時給header加一個shadow(很多table都有這樣的功能)
一種很常見的做法是監(jiān)聽scroll,當滾動一定距離時加上shadow即可。但是監(jiān)聽scroll本身會早成一定的渲染壓力(scroll觸發(fā)非常頻繁),同時如果使用React這樣的框架又會造成額外的render,在用戶的視角看來更卡了。
此時使用IntersectionObserver就很合適了,因為我們需要監(jiān)聽的只是觸發(fā)sticky的一瞬間,其他的滾動都是無效的,沒必要進行計算。
3.3.1、源碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sticky Header with Shadow on Intersection</title>
<style>
body {
margin: 0;
padding: 0;
}
header {
height: 80px;
background-color: #3498db;
color: white;
text-align: center;
line-height: 80px;
position: sticky;
top: 0;
z-index: 100;
}
.header-shadow {
transition: box-shadow 0.3s ease;
}
.header-shadow.shadow {
box-shadow: 0 2px 5px black;
}
section {
height: 1000px;
background-color: #ecf0f1;
padding: 20px;
}
</style>
</head>
<body>
<div id="guard"></div>
<header id="sticky-header" class="header-shadow">Sticky Header</header>
<section>
<p>向下滾動觸發(fā)sticky時展示shadow</p>
</section>
<script>
const header = document.getElementById('sticky-header');
const section = document.querySelector('section');
const options = {
threshold: 1
};
//guard滾動到可視區(qū)域以外時認為觸發(fā)了shadow
const intersectionObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
header.classList.remove('shadow');
} else {
header.classList.add('shadow');
}
});
}, options);
intersectionObserver.observe(document.getElementById('guard'));
</script>
</body>
</html>
3.3.2、預(yù)覽
在線源碼請點擊【前往】
3.3.3、效果

四、ResizeObserver
ResizeObserver是用于監(jiān)聽DOM尺寸變化的observer,當DOM尺寸變化是執(zhí)行callback
4.1、基本使用
和前面的api用法差不多,這里不過多介紹。
const box = document.getElementById('box');
const resizeObserver = new ResizeObserver(entries => {
entries.forEach(entry => {
console.log(entry)
});
});
resizeObserver.observe(box);
4.2、API介紹
entry對象包含resize相關(guān)的信息,下面看一下entry的結(jié)構(gòu)
{
// 不同box-sizing下的尺寸
borderBoxSize: [{
blockSize: 200,
inlineSize: 200,
}],
contentBoxSize: [{
blockSize: 200,
inlineSize: 200,
}],
contentRect: {
bottom: 200,
height: 200,
left: 0,
right: 200,
top: 0,
width: 200,
x: 0,
y: 0
},
// 在物理設(shè)備像素上的大小, 在不同的屏幕上尺寸不同例如Retina
devicePixelContentBoxSize: [{
blockSize: 300,
inlineSize: 300
}
],
target: div#resizable-box
}
4.3、常見場景
可以基于ResizeObserver實現(xiàn)一個簡單的resize-detector(參考react-resize-detector),在尺寸變化時返回尺寸信息,點擊盒子就算拖拽有效。
4.3.1、源碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ResizeObserver Demo with Resizable Box</title>
<style>
#resizable-box {
width: 200px;
height: 200px;
background-color: #3498db;
color: white;
text-align: center;
line-height: 200px;
font-size: 24px;
transition: background-color 0.5s ease;
resize: both;
overflow: auto;
cursor: pointer;
}
</style>
</head>
<body>
<div id="resizable-box">Resize me!</div>
<script>
const resizableBox = document.getElementById('resizable-box');
let isResizing = false;
let startX, startY, startWidth, startHeight;
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log('寬度:', width, '高度:', height);
}
});
resizeObserver.observe(resizableBox);
resizableBox.addEventListener('mousedown', startResize);
document.addEventListener('mousemove', handleResize);
document.addEventListener('mouseup', stopResize);
function startResize(e) {
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = parseInt(document.defaultView.getComputedStyle(resizableBox).width, 10);
startHeight = parseInt(document.defaultView.getComputedStyle(resizableBox).height, 10);
}
function handleResize(e) {
if (!isResizing) return;
const newWidth = startWidth + (e.clientX - startX);
const newHeight = startHeight + (e.clientY - startY);
resizableBox.style.width = newWidth + 'px';
resizableBox.style.height = newHeight + 'px';
}
function stopResize() {
isResizing = false;
}
</script>
</body>
</html>
4.3.2、預(yù)覽
在線源碼請點擊【前往】
4.3.3、效果

五、PerformanceObserver
PerformanceObserver用于監(jiān)聽瀏覽器的performance事件,方便在performance事件觸發(fā)時作統(tǒng)一處理。
5.1、基本使用
// mdn demo
function perf_observer(list, observer) {
console.log(list)
}
var observer2 = new PerformanceObserver(perf_observer);
// entryTypes用于指定要監(jiān)聽的事件類型
observer2.observe({ entryTypes: ["measure"] });
5.2、API介紹
下面列一下常見的entryTypes
mark:用于標記時間戳的事件measure:performance.measure觸發(fā)的事件frame:網(wǎng)頁渲染的事件navigation:導(dǎo)航的事件,例如頁面加載或重新加載resource:資源加載事件longtask:長任務(wù)事件paint:繪制事件,例如FP,FCPlayout-shift:用于監(jiān)視布局變化的事件
對于對性能比較敏感的項目以及長期性能監(jiān)控來說這個api還是比較方便的。
六、ReportingObserver
ReportingObserver用于監(jiān)聽瀏覽器報告的事件,例如廢棄API,過時特性,網(wǎng)絡(luò)錯誤。做監(jiān)控SDK的同學(xué)應(yīng)該經(jīng)常能用到,日常業(yè)務(wù)代碼用的比較少。
6.1、基本使用
這里就簡單看一下使用方法吧,比較簡單
const observer = new ReportingObserver((reports, observer) => {
reports.forEach(report => {
console.log(report);
});
});
// 監(jiān)聽過時特性
observer.observe({ types: ['deprecation'] });
七、最后
到此這篇關(guān)于JavaScript中5個重要的Observer函數(shù)小結(jié)的文章就介紹到這了,更多相關(guān)JavaScript Observer內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

