浏览器中好用的 Observer Api
简介
网页开发中经常会处理用户交互相关的事件。随着我们的需求越来越复杂,有很多的场景浏览的事件机制不能很好,或者说不能很快速的实现我们的需求。比如:
-
页面水印需要防止水印元素被篡改。
-
在实现图片懒加载的时候,我们需要判断元素是否出现在了可视区域内。
-
在一些复杂的交互中,我们需要判断元素的大小是否发生了变化。
目前浏览器提供了 5 种很好用的 Observer Api
,可以很方便的实现上面的需求。
-
MutationObserver:用来监听
DOM
树的变动 -
IntersectionObserver:用来监听两个元素是否相交
-
ResizeObserver:用来监听元素的大小更改
-
PerformanceObserver:浏览器渲染页面过程中有一些关键的渲染时间点,可以用这个
api
监听到 -
ReportingObserver:浏览器使用过时的
api
或者浏览器对我们api
执行有干预的时候会触发监听
Observer Api
属于微任务 ,优先级小于 Promise
,每一个Observer
在创建的时候会调用一次,然后每次监听的相关事件触发的时候会执行回调。
接下来我会对这几个 API
的使用、兼容性、polyfill
做一些介绍。
IntersectionObserver
相交观察者,可以很方便的检测一个元素是否可见或者两个元素是否相交
兼容性
polyfill
具体见链接readme
说明,官方的polyfill
,good
小案例
图片懒加载案例,核心逻辑是将src
使用data-src
进行替换,当图片进入视口的时候赋值src
属性。
代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IntersectionObserver</title>
<style>
.content {
width: 200px;
height: 1000px;
border: 1px solid red;
}
</style>
</head>
<body>
<div class="content">文字内容</div>
<img data-src="./img/1.png" width="1000" />
<div class="content">文字内容</div>
<img data-src="./img/2.jpg" width="1000" />
<div class="content">文字内容</div>
<img data-src="./img/3.jpg" width="1000" />
<script>
const imgs = document.querySelectorAll('img');
imgs.forEach((img) => {
// 1. 创建观察者
const observer = new IntersectionObserver((entry) => {
if (entry[0].isIntersecting) {
// 图片出现在视口区域了,加载图片
img.src = img.getAttribute('data-src');
img.removeAttribute('data-src');
observer.unobserve(img); // 3. 取消监听
}
});
observer.observe(img); // 2. 开始监听
});
</script>
</body>
</html>
预览
详细使用
IntersectionObserver API
是一个相交观察器,用于监听 目标元素 与指定的 root 元素(祖先元素或视窗) 的相交状态(可见性)。
- 目标元素进入
root
元素的时候会触发回调 - 目标元素离开
root
元素的时候会触发回调
构造函数
js
const observer = new IntersectionObserver(callback, options);
options
js
let options = {
root: document.querySelector('#app'),
rootMargin: '0px',
threshold: 1.0,
};
root
:指定root
元素,用于检查目标元素的可见性。必须是目标元素的父级元素。如果未指定或者为null
,则默认为浏览器视窗。rootMargin
: 配置root
元素的margin
值,写法同css margin
写法。用来扩大检查相交的范围。默认值是 0。threshold
:相交门槛。可以是单一数值,也可以是数字数组。目标元素和 root 元素相交的范围达到该值的时候,触发回调,默认值是 0。- 0 表示只要有一个像素的相交就会触发回调函数
- 1 表示完全相交才会触发回调函数
- [0, 0.25, 0.5, 0.75, 1.0] 表示会触发 5 次回调函数,分别是,刚相交、相交范围达到 25%、相交范围达到 50%、相交范围达到 75%、完全相交的时候会触发回调函数。
callback
发生相交
的回调
entries
参数:返回当前已监听
的目标元素
的相交信息
集合observer
参数:当前的观察者
js
let callback = (entries, observer) => {
entries.forEach((entry) => {
console.log(entry);
});
};
每一个 entry(相交信息)
有如下属性:
time
:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒target
:被观察的目标元素rootBounds
:root
元素的矩形区域的信息,和getBoundingClientRect()
方法的返回值一致boundingClientRect
:目标元素的矩形区域的信息intersectionRect
:目标元素与视口(或根元素)的交叉区域的信息intersectionRatio
:目标元素的可见比例 ,即intersectionRect
占boundingClientRect
的比例,完全可见时为1
,完全不可见时小于等于0
isIntersecting
:是否发生相交 ,true
表示相交,false
表示没有相交
对 entries
是一个集合做一些补充:
上面案例中,监听的写法可以有下面两种:
- 对每一个
img
创建一个observer
js
const imgs = document.querySelectorAll('img');
imgs.forEach((img) => {
const observer = new IntersectionObserver((entry) => {});
observer.observe(img);
});
- 创建一个
observer
,监听每一个img
js
const imgs = document.querySelectorAll('img');
const observer = new IntersectionObserver((entry) => {});
imgs.forEach((img) => {
observer.observe(img);
});
当第二种写法的时候,元素是横向排列的,并且同时间相交的元素有多个,entries
就会返回每一个相交的元素,如下图
observer 实例
实例的observe
方法可以指定观察哪个 DOM
节点。
javascript
// 开始观察 el元素
observer.observe(el);
// 停止观察 el元素
observer.unobserve(el);
// 销毁观察器
observer.disconnect();
// 返回所有观察目标的 entry 对象数组
observer.takeRecords();
MutationObserver
变化观察者,可以很方便的 Dom
树的变动,如元素属性的变动,子节点的增删。只能监听到子元素的删除,监听节点的删除监听不到,所以需要监听当前节点删除的时候,需要监听父元素的子元素变化。
兼容性
提一嘴,MutationObserver
是这几个 Observer API
中 兼容性最好的
polyfill
具体见链接readme
说明,官方的polyfill
,good
小案例
删不掉的水印,使用 MutationObserver
实现,对 body
进行监听,处理水印元素被删除的场景,监听水印元素的所有 dom
操作,用来防止被篡改。
代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MutationObserver</title>
<style>
#watermark {
width: 500px;
height: 500px;
border: 1px solid #000;
}
</style>
</head>
<body>
<div id="watermark">假设我是一个水印节点</div>
<script>
const watermarkEl = document.getElementById('watermark');
// 判断水印有没有被篡改
const checkChange = (mutation) => {
let flag = false;
// 判断是不是删除了水印元素
if (mutation.removedNodes.length) {
flag = Array.from(mutation.removedNodes).some((node) => node === watermarkEl);
}
// 判断是不是修改了水印元素的属性
if (mutation.type === 'attributes' && mutation.target === watermarkEl) {
flag = true;
}
return flag;
};
const mo = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (checkChange(mutation)) {
alert('水印被篡改');
// todo 重新生成水印
}
});
});
// 只监听 document 的子节点变化,用来处理水印节点被删除的场景
mo.observe(document.body, {
childList: true,
});
// 监听水印的属性、子节点、后代节点的变化
mo.observe(watermarkEl, {
childList: true, // 监听子节点 变化
attributes: true, // 监听后代节点 变化
subtree: true, // 监听属性 变化
attributeFilter: ['style', 'class'], // 声明只监听这两属性
});
</script>
</body>
</html>
预览
详细使用
js
const observer = new MutationObserver(callback);
const targetNode = document.querySelector('#someElement');
const observerOptions = {
childList: true, // 观察目标子节点的变化,是否有添加或者删除
attributes: true, // 观察属性变动
subtree: true, // 观察后代节点,默认为 false
};
observer.observe(targetNode, observerOptions);
构造函数
js
const observer = new MutationObserver(callback);
observer 实例
js
// 开始观察 el元素
observer.observe(el, observeOptions);
// 销毁观察器
observer.disconnect();
// 返回所有观察目标的 entry 对象数组
observer.takeRecords();
observeOptions:可选的配置对象,用来描述 DOM 的哪些变化应该触发回调。childList
、attributes
和 characterData
中,必须有一个参数为 true
。否则会抛出 TypeError
异常。
-
subtree
可选,当为true
时,将会监听以target
为根节点的整个子树 。包括子树中所有节点的属性。默认值为false
。 -
childList
可选,当为true
时,监听target
节点中发生的节点 的新增与删除。默认值为false
。 -
attributes
可选,当为true
时观察所有监听的节点属性值的变化。默认值为true
,当声明了attributeFilter
或attributeOldValue
,默认值则为false
。 -
attributeFilter
可选,用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。 -
attributeOldValue
可选,当为true
时,记录上一次被监听的节点的属性变化; -
characterData
可选,当为true
时,监听声明的target
节点上所有字符的变化。默认值为true
,如果声明了characterDataOldValue
,默认值则为false
。 -
characterOldValue
可选,当为true
时,记录前一个被监听的节点中发生的文本变化。,默认值则为false
。
callback
js
function callback(mutationList, observer) {
// ...
}
回调函数有两个参数
mutationList
: 描述所有被触发改动的MutationRecord
对象数组observer
:当前的观察者
MutationRecord
含有的属性介绍:
-
type
: 变化的类型,attributes
表示属性变化,characterData
表示节点所有字符变化,childList
表示子节点树变化。 -
target
:发生变化的节点 -
addedNodes
:如果是添加了节点,添加的节点会在这个属性表示 -
removedNodes
:如果是移出了节点,移出的节点会在这个属性表示 -
previousSibling
:返回被添加或移除的节点之前的兄弟节点,或者null
。 -
nextSibling
: 返回被添加或移除的节点之后的兄弟节点,或者null
。 -
attributeName
:返回被修改的属性的属性名,或者null
。 -
attributeNamespace
:返回被修改属性的命名空间,或者null
。 -
oldValue
:对于属性attributes
变化,返回变化之前的属性值。对于characterData
变化,返回变化之前的数据。对于子节点树childList
变化,返回null
。
ResizeObserver
尺寸变化观察者,可以很方便的监听元素大小的变化,元素display:none
进行隐藏的时候,也是会触发监听的
兼容性
polyfill
具体见链接readme
说明,社区实现的polyfill
小案例
代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ResizeObserver</title>
<style>
#roEl {
width: 200px;
height: 100px;
border-image: linear-gradient(deepskyblue, deeppink) 1;
}
</style>
</head>
<body>
<textarea id="roEl">酷炫边框</textarea>
<script>
const roEl = document.getElementById('roEl');
const observe = new ResizeObserver(function (entries) {
const entry = entries[0];
const cr = entry.contentRect;
const target = entry.target;
const angle = cr.width - 200 + (cr.height - 100);
target.style.borderImageSource = 'linear-gradient(' + angle + 'deg, deepskyblue, deeppink)';
});
observe.observe(roEl); // 观察文本域元素
</script>
</body>
</html>
预览
详细使用
js
const observer = new ResizeObserver(callback);
const targetNode = document.querySelector('#someElement');
const observerOptions = {
box: 'border-box', // 设置监听的盒模型
};
observer.observe(targetNode, observerOptions);
构造函数
js
const observer = new ResizeObserver(callback);
observer 实例
js
// 开始观察 el元素
observer.observe(el, observeOptions);
// 停止观察 el元素
observer.unobserve(el);
// 销毁观察器
observer.disconnect();
observeOptions:可选的配置对象,用来描述 DOM 的哪些变化应该触发回调。目前只支持box
一个属性配置。
box
:设置 observer 将监听的盒模型。可能的值是:content-box
(默认),CSS 中定义的内容区域的大小。border-box
,CSS 中定义的边框区域的大小。device-pixel-content-box
,在对元素或其祖先应用任何 CSS 转换之前,CSS 中定义的内容区域的大小,以设备像素为单位。
content box
示意图:
如果指定的 box
为content-box
,那么我们修改padding
或者 border-width
,是不会触发回调函数的
callback
发生大小变化的回调
entrie
参数:真正在观察的Element
最新的大小。类型是ResizeObserverEntry
。
ResizeObserverEntry
类型介绍:
-
borderBoxSize
:正在观察元素的新边框盒的大小。 -
contentBoxSize
:正在观察元素的新内容盒的大小。 -
devicePixelContentBoxSize
:正在观察元素的新内容盒的大小(以设备像素为单位)。 -
contentRect
:正在观察元素新大小的DOMRectReadOnly
对象。这是一个遗留属性,并且在未来的版本中可能被弃用。 -
target
:对正在观察的 Element 。
PerformanceObserver
性能报告的观察者,用来采集页面的性能的,可以做性能上报,暂时没有涉及到相关代码,以后有机会在补充
ReportingObserver
浏览器使用过时的 api
或者浏览器对我们 api
执行有干预的时候会触发监听,暂时没有涉及到相关代码,以后有机会在补充