ResizeObserver保持俩个div大小一致或者同时监听多个元素

前言

大家在遇到需要监测DOM元素尺寸大小的需求时,可能第一时间想到的都是使用window.addEventListener来监听resize 事件,

但是reize事件会在一秒内触发将近60次,所以很容易在改变窗口大小时导致性能问题。因为它会监听我们页面每个元素的大小变化,而不是具体到某个元素的变化。如果我们只想监听某个元素的变化的话,这种操作就很浪费性能了。 并且只有在window对象才有resize事件(其他hack方法,比如resize事件,scroll事件,requestAnimationFrame等等)

而ResizeObserver API就可以帮助我们:监听一个DOM节点的变化,这种变化包括但不仅限于:

  1. 某个节点的出现和隐藏
  2. 某个节点的大小变化

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元素的大小信息,我们可以通过widthheight属性来获取dom元素的宽度和高度,我们也可以通过paddingBoxSize属性来获取dom元素的padding的大小。

2. ResizeObserverEntry

上面的关键点在于回调函数中的entries参数,它是一个ResizeObserverEntry对象的数组,我们可以通过它来获取dom元素的大小信息。

ResizeObserverEntry对象包含了dom元素的大小信息,它的属性如下:

  • borderBoxSizedom元素的border的大小。
  • contentBoxSizedom元素的content的大小。
  • contentRectdom元素的大小信息,它是一个DOMRectReadOnly对象,它包含了dom元素的大小信息,我们可以通过widthheight属性来获取dom元素的宽度和高度,我们也可以通过paddingBoxSize属性来获取dom元素的padding的大小。
  • devicePixelContentBoxSizedom元素的devicePixelContent的大小。
  • targetdom元素。
  • timedom元素大小变化的时间。
  • transformBoxdom元素的transform的大小。
  • xdom元素的x轴的大小。
  • ydom元素的y轴的大小。
  • constructorResizeObserverEntry对象的构造函数。
  • isBoxSizeSynceddom元素的大小是否同步。

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>

浏览器兼容性

作为一个新兴的apiResizeObserver的兼容性还不是很好,目前只有ChromeFirefox支持它,SafariEdge还不支持,所以我们在使用的时候需要注意一下。

ResizeObserver的兼容性如下:

  • Chrome:v64及以上。
  • Firefox:v63及以上。
  • Safari:v13及以上。
  • Edge:v79及以上。
  • Opera:v51及以上。

可以去caniuse查看更多的兼容性信息。

总结

ResizeObserver解决了我们没用监听dom大小变化的难题,作为新一代的标准api,它的兼容性虽然还不是很好,但是还是可以添加polyfill来解决兼容性的问题,所以我们可以放心的使用它。

最后使用之后记得清除ResizeObserver对象,否则会造成内存泄漏。

参考:developer.mozilla.org/zh-CN/docs/…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

eadela

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值