关于ResizeObserver,你还可以这样做性能优化...
先说说ResizeObserver
ResizeObserver
的作用主要是用来监听DOM元素的尺寸变化,在这个API还没有诞生时,通常需要通过CSS媒体查询,或者通过window上的resize事件间接监听整个视窗的变化。
但在有些情况下,视窗尺寸的改变不一定导致监测元素尺寸的变化,或者,监测元素尺寸的变化并不由视窗尺寸元素的变化导致。例如,自身支持resize的元素在响应式布局中影响其他元素,支持动态增删改的列表也会导致父元素的尺寸变化。这种时候,监听视窗的尺寸变化,不仅浪费性能,还有可能起不到想要的监听效果。
ResizeObserver
可以做到,不管什么层级的DOM元素,调用它的observer方法,即可精准监听尺寸变化。不用再去憨憨地监听整个视窗的变化了。
同时,这也是一种无侵入的观察方式,可以在完全不影响目标元素业务逻辑的同时,嵌入需要响应变化的功能逻辑,例如发送埋点数据。减轻了开发者的心智负担和维护成本。
目前基本是现代浏览器全线支持:
ResizeObserver的基本使用
ResizeObserver
本身是一个构造函数,通过调用它创建一个实例。
同时需要传给它一个callback,在callback中写好当目标元素尺寸发生变化时,需要相响应的逻辑。
callback本身接收一个变化对象数组参数,每个对象中包含了尺寸变化的相关信息。
const observer = new ResizeObserver(callback);
function callback(entries){
for(const entry of entries){
const rect = entry.contentRect;
// ...
}
}
其中,entry.contentRect
返回的是一个DOMRect对象,其中包含了目标元素的x, y, width, height, top, bottom, left, right等属性。
ResizeObserver
有三个方法,observe()
,unobserve()
,disconnect()
,分别对应了开始监听,结束监听,以及取消所有对目标元素的的监听。
// 获取目标元素
const ele = document.getElementsByClassName('list-item');
// 开始监听
observer.observe(ele);
// 结束监听
observer.unobserve(ele);
// 取消监听所有目标元素
observer.disconnect();
通常,在完成监听功能后,我们需要及时调用unobserve()
,disconnect()
方法,防止产生内存泄漏问题。
全局唯一实例的性能优化
其实,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
会在组件初始化和卸载阶段被调用,执行绑定和解绑逻辑,不失为一种优雅调用方法~
有类似场景的同学不妨给自己的项目封装一个试试吧!
转载自:https://juejin.cn/post/7254853023288557628