前言
大家在遇到需要监测DOM元素尺寸大小的需求时,可能第一时间想到的都是使用window.addEventListener
来监听resize 事件,
但是reize事件会在一秒内触发将近60次,所以很容易在改变窗口大小时导致性能问题。因为它会监听我们页面每个元素的大小变化,而不是具体到某个元素的变化。如果我们只想监听某个元素的变化的话,这种操作就很浪费性能了。 并且只有在window对象才有resize事件(其他hack方法,比如resize事件,scroll事件,requestAnimationFrame等等)
而ResizeObserver API就可以帮助我们:监听一个DOM节点的变化,这种变化包括但不仅限于:
- 某个节点的出现和隐藏
- 某个节点的大小变化
1. ResizeObserver
ResizeObserver
作为一个标准的web``api
,它的作用是监听dom
元素的大小变化,它的api
非常简单,只有一个observe
方法,它接受一个dom
元素作为参数,然后就可以监听这个dom
元素的大小变化了,当然,我们也可以使用unobserve
方法来停止监听这个dom
元素的大小变化。
我们先来看一下ResizeObserver
的函数签名:
interface ResizeObserver {
constructor(callback: ResizeObserverCallback): void;
observe(target: Element): void;
unobserve(target: Element): void;
disconnect(): void;
}
interface ResizeObserverCallback {
(entries: ResizeObserverEntry[], observer: ResizeObserver): void;
}
ResizeObserver
是一个web
标准的api
,所以我们可以放心的使用它,它的作用是监听dom
元素的大小变化,它的使用方法如下:
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(`Element: ${entry.target}`);
console.log(`Element size: ${width}px x ${height}px`);
console.log(`Element padding: ${entry.paddingBoxSize}`);
}
});
resizeObserver.observe(document.querySelector('.resize'));
上面的代码中,我们创建了一个ResizeObserver
实例,然后调用observe
方法来监听dom
元素的大小变化,当dom
元素的大小发生变化的时候,ResizeObserver
实例会触发回调函数,回调函数的参数是一个entries
数组,数组中的每一项都是一个ResizeObserverEntry
对象,它包含了dom
元素的大小信息,我们可以通过contentRect
属性来获取dom
元素的大小信息,它是一个DOMRectReadOnly
对象,它包含了dom
元素的大小信息,我们可以通过width
和height
属性来获取dom
元素的宽度和高度,我们也可以通过paddingBoxSize
属性来获取dom
元素的padding
的大小。
2. ResizeObserverEntry
上面的关键点在于回调函数中的entries
参数,它是一个ResizeObserverEntry
对象的数组,我们可以通过它来获取dom
元素的大小信息。
ResizeObserverEntry
对象包含了dom
元素的大小信息,它的属性如下:
borderBoxSize
:dom
元素的border
的大小。contentBoxSize
:dom
元素的content
的大小。contentRect
:dom
元素的大小信息,它是一个DOMRectReadOnly
对象,它包含了dom
元素的大小信息,我们可以通过width
和height
属性来获取dom
元素的宽度和高度,我们也可以通过paddingBoxSize
属性来获取dom
元素的padding
的大小。devicePixelContentBoxSize
:dom
元素的devicePixelContent
的大小。target
:dom
元素。time
:dom
元素大小变化的时间。transformBox
:dom
元素的transform
的大小。x
:dom
元素的x
轴的大小。y
:dom
元素的y
轴的大小。constructor
:ResizeObserverEntry
对象的构造函数。isBoxSizeSynced
:dom
元素的大小是否同步。
3.用法
ResizeObserver是个构造函数。在使用new关键字调用构造函数,返回实例对象时,需要传入一个回调函数,这个回调用于监听实例对象某个DOM节点的变化
// HTML
<div class="wrapper" ref="wrapper"></div>
// css
.wrapper {
height: 500px;
background-color: blueviolet;
}
// js
mounted() {
const resizeObserver = new ResizeObserver((entries, observer) => {
entries.forEach(entry => {
const { target, contentRect } = entry;
// 处理元素尺寸变化
// contentRect包含了元素的新尺寸和位置信息
const target = entry.target;
const contentRect = entry.contentRect;
console.log(`目标元素: ${target}`);
console.log(`宽度: ${contentRect.width}px`);
console.log(`高度: ${contentRect.height}px`);
})
})
}
效果展示:
另外我们可以给回调函数添加一个参数,用来获取到该元素的一些信息 然后,实例对象myObserver方法除了有observe方法之外,还有disconnect方法和unobserve方法。
4.Resize方法
1. ResizeObserver.observe()
ResizeObserver.observe()
方法用于开始观察一个 DOM 元素的大小变化。当元素的大小发生变化时,Resize Observer 会调用注册的回调函数。
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
// 处理大小变化
}
});
const elementToObserve = document.getElementById('myElement');
resizeObserver.observe(elementToObserve);
并且我们可以观察多个元素,只需分别使用observe
方法指定不同的目标元素。
const target1 = document.querySelector('#element1');
const target2 = document.querySelector('#element2');
observer.observe(target1);
observer.observe(target2);
2. ResizeObserver.disconnect()
ResizeObserver.disconnect()
方法用于停止观察一个 DOM 元素的大小变化。一旦调用disconnect()
,观察将立即停止,不再触发回调函数。
resizeObserver.disconnect(); // 停止观察所有被观察的元素
3. ResizeObserver.unobserve()
ResizeObserver.unobserve()
方法用于停止观察一个或多个 DOM 元素的大小变化。我们可以多次调用unobserve()
来停止观察不同的元素。
const elementToUnobserve = document.getElementById('elementToStopObserving');
resizeObserver.unobserve(elementToUnobserve); // 停止观察特定元素
5.性能优化
全局唯一实例的性能优化
其实,ResizeObserver
的诞生,相较于MutationObserver
以及传统的resize
事件,本身就是高效的尺寸变化监测。它的精准性避免了很多不必要的性能开销。
但在最近的一个项目中,由于业务需求,需要通过ResizeObserver
监听动态列表项高度,将它加在了一个长列表的每一项。由于每一项都是一个单独的组件,每个组件都拥有自己的 ResizeObserver
实例,这意味着浏览器会为每个实例分配内存和处理事件。当页面上有大量组件时,会对性能产生相当程度的影响。
细心的读者应该发现了,每个ResizeObserver
实例是可以同时监听多个目标元素的,那么借鉴单例模式的思想,尝试封装一个全局的ResizeObserver
,限制整个项目只有一个实例。在需要调用的地方直接GlobalResizeObserver.observe()
!话不多说,上代码:
export const GlobalResizeObserver = (function() {
const ATTR_NAME = 'global-resizeobserver-key';
const attrValueToCallback = {};
const o = new ResizeObserver((entries) => {
for (const entry of entries) {
const resizedElement = entry.target;
const attrValue = resizedElement.getAttribute(ATTR_NAME);
if (attrValue) {
const callback = attrValueToCallback[attrValue];
if (typeof callback === 'function') {
callback(entry);
}
}
}
});
return Object.freeze({
/**
* @param { Element } element
* @param { (ResizeObserverEntry) => {} } callback
*/
observe(element, callback) {
if (!(element instanceof Element)) {
console.error('GlobalResizeObserver, cannot observe non-Element.');
return;
}
let attrValue = element.getAttribute(ATTR_NAME);
if (!attrValue) {
attrValue = String(Math.random());
element.setAttribute(ATTR_NAME, attrValue);
}
attrValueToCallback[attrValue] = callback;
o.observe(element);
},
/**
* @param { Element } element
*/
unobserve(element) {
if (!(element instanceof Element)) {
console.error('GlobalResizeObserver cannot unobserve non-Element.');
return;
}
const attrValue = element.getAttribute(ATTR_NAME);
if (!attrValue) {
console.error('GlobalResizeObserver cannot unobserve element w/o ATTR_NAME.');
return;
}
delete attrValueToCallback[attrValue];
o.unobserve(element);
}
});
})();
主要思路是通过JS的闭包特性,构建对象保存每个被监听元素对应的callback。
通过构建全局唯一的ResizeObserver
实例,页面的fps, Response Time, CPU Usage, Memory Consumption, Event Handling等性能指标都有了提升!
当然,如果监听的目标元素足够多,相应的给callback函数加上防抖节流也是很有必要的,毕竟前端页面性能直接关乎用户体验的好坏。
在React中的使用
不管是原生的生成实例用法,还是封装全局唯一实例,在React
中的使用应该不存在什么最佳实践,仁者见仁,智者见智。这里仅介绍一种回调ref
的做法,供大家参考。
import React, { useCallback } from 'react';
import { GlobalResizeObserver } from 'global-resize-observer';
export const component = () => {
const callbackRef = useCallback((node) => {
if (!node) {
GlobalResizeObserver.unobserve(node);
return;
}
GlobalResizeObserver.observe(node, (entry) => {
// 响应逻辑...
});
}, []);
return <div ref={callbackRef}></div>
}
callbackRef
会将当前ref的值作为函数入参传入,并通过useCallback
封装,并且依赖数组为[]
,所以callbackRef
会在组件初始化和卸载阶段被调用,执行绑定和解绑逻辑,不失为一种优雅调用方法~
6.示例-俩个div大小保持一致
<template>
<div class="list-transfer-wrap">
<SelectList id="selectListRef" ref="selectListRef"/>
<SelectedList id="selectedListRef" ref="selectedListRef" />
</div>
</template>
<script>
import _ from 'lodash';
import SelectList from '';
import SelectedList from '';
export default {
name: 'CascadeTransfer',
components: { SelectList, SelectedList },
provide() {
return {
cascadeTransfer: this
};
},
props: {
products: {
type: Array,
default: () => []
},
},
data() {
return {
resizeObserver: undefined,
lastResizeWidth: 482 // 初值
};
},
mounted() {
// new ResizeObserver
this.observeResizeHandler(
// 同时绑定俩个div,进行监听
[document.getElementById('selectListRef'), document.getElementById('selectedListRef')],
_.throttle(this.upWidth, 400, { trailing: true }) // 节流,400ms内只执行最后一次变动
);
},
beforeUnmount() {
this.observeResizeHandler.disconnect(); // 销毁
},
methods: {
observeResizeHandler(elements, callback) {
if (!this.resizeObserver) {
this.resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
callback(entry);
}
});
}
elements.forEach(element => {
this.resizeObserver.observe(element);
});
},
upWidth(entry) {
const {contentRect} = entry;
// 获取值时有border差异,导致有2px容错
if (Math.abs(this.lastResizeWidth - contentRect.width) < 3) return;
this.lastResizeWidth = contentRect.width;
if (entry.target.getAttribute('class').includes('l-wrap')) {
this.$refs.selectedListRef.width = contentRect.width;
} else {
this.$refs.selectListRef.width = contentRect.width;
}
}
}
};
</script>
浏览器兼容性
作为一个新兴的api
,ResizeObserver
的兼容性还不是很好,目前只有Chrome
和Firefox
支持它,Safari
和Edge
还不支持,所以我们在使用的时候需要注意一下。
ResizeObserver
的兼容性如下:
- Chrome:
v64
及以上。 - Firefox:
v63
及以上。 - Safari:
v13
及以上。 - Edge:
v79
及以上。 - Opera:
v51
及以上。
可以去caniuse查看更多的兼容性信息。
总结
ResizeObserver
解决了我们没用监听dom
大小变化的难题,作为新一代的标准api
,它的兼容性虽然还不是很好,但是还是可以添加polyfill
来解决兼容性的问题,所以我们可以放心的使用它。
最后使用之后记得清除ResizeObserver
对象,否则会造成内存泄漏。