JavaScript中的ResizeObserver

发布于 大漠

这几天看@Anton Kosykh写的使用ResizeObserver实现响应式Vue的组件,让我刷新了对响应式组件的认识,也在这里第一次接触到ResizeObserver这个词。个人有一个习惯,对于任何新东西,我都充满好奇,有一股探知欲,要了解其中的原委。今天这篇文章就是自己对ResizeObserver的探知。

什么是观察者(Observer)

要了解ResizeObserver,就很有必要先了解观察者(Observer)这一概念。

观察者是一个观察或注意事物的程序。观察者可以观察浏览器中发生的某些事情并做出相应的响应。如果打个比方来说,观察者就好比一条狗,他在帮你守家,当有人进入你的家的时候或者家中发生异常的时候,他会提醒你发生的事情。当狗向你为一些事情发出警告的时候,你会做出相应的行动。

使用观察者,我们可以观察浏览器中发生的不同类型的活动,并采取必要的行动。比如,我们可以观察一个视频是否在浏览器中显示,并启用自动播放;或者从DOM元素中添加或删除一子元素,甚至其他的一些事情。

事实上在JavaScript的设计模式中一种模式叫做:观察者模式(Observer Pattern)

观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。

简单点说,观察者模式是一种设计模式,其中一个对象(Subject)维持一系列依赖于它(Observer)的对象,将有关状态的任何变更自动通知给它们。当一个目标需要告诉观察者发生了什么事情,它会向观察者广播一个通知。以下我们采用JavaScript来实现观察者模式。

  • Subject(目标):维护一系列的观察者,方便添加或删除观察者。
  • Observer(观察者):为那些在目标状态发生改变时需获得通知的对象提供一个更新接口。

被依赖对象通常被称为主体(Subject),将其称为被观察对象(Observable),其相关依赖对象被称为观察者(Observer),即被观察对象会主动地向观察者“推送(push)”消息,而不是观察者向被观察对象“拉取(pull)”消息,实现的是一种 Push 模式的消息系统。

从被观察对象和观察者的类图可以明确地看到两者之间的消息传递关系:

JavaScript中的观察者模式又叫发布订阅模式(Publish/Subscribe),使用观察者模式的好处:

  • 支持简单的广播通信,自动通知所有已经订阅过的对象。
  • 页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性。
  • 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。

这里只是简单的介绍了JavaScript中的观察者模式,有关于这方面更详细的介绍,可以参阅读:

浏览器的观察者

ResizeObserver是继MutationObserverperformanceObserverIntersectionObserver三者之后的另一个观察者。ResizeObserver对任何观察到的元素的大小的变化作出反应,与导致变化的原因无关。

ResizeObserver的解释

开发过程当中经常遇到的一个问题就是如何监听一个div的尺寸变化。但众所周知,为了监听div的尺寸变化,都将侦听器附加到window中的resize事件。但这很容易导致性能问题,因为大量的触发事件。换句话说,使用window.resize通常是浪费的,因为它告诉我们每个视窗大小的变化,而不仅仅是当一个元素的大小发生变化。

使用ResizeObserver的API的另一个用例就是视窗resize事件不能帮助我们:当元素被动态地添加或删除时,会影响父元素的大小。这也是现代单页应用程序越来越频繁使用ResizeObserver原因之一。

也就是说,通过window.resize事件的监听,可以调用一个回调函数。在这个回调函数中做我们需要做的事情。

// define a callback
function callback() {
    // something cool here
}

// add resize listener to window object
window.addEventListener('resize', callback)

比如说,你要调整一个元素的大小,那就需要在resize的回调函数callback()中调用getBoundingClientRectgetComputerStyle。不过你要是不小心处理所有的读和写操作,就会导致布局混乱。比如下面这个小示例:

当你改变浏览器视窗大小的时候,就可以看到相应的变化:

这也就是为什么ResizeObserver是一个有用的API。它对所观察到的任何元素的大小的变化做出反应,而不依赖于所引起的变化。它还提供了对所观察元素的新大小的访问。那接下来让我们直接切入正题。

简单总结一下:

ResizeObserver允许我们观察DOM元素的内容矩形大小(宽度、高度)的变化,并做出相应的响应。它就像给元素添加document.onresize()window.resize()事件(但在JavaScript中,只有window才有resize事件)。当元素改变大小而不调整视窗大小时,它是有用的。 下面描述一些调整观察者的行为:

  • 当观察到的元素被插入或从DOM中删除时,观察将会触发
  • 当观察到的元素display值为none时,观察都会触发
  • 观察不会对未替换的内联元素(non-replaced inline element)触发
  • 观察不会由CSS的transform触发
  • 如果元素有显示,而且元素大小不是0,0,观察将会触发

ResizeObserver通知内容框的大小,如下图所示:

ResizeObserver基本用法

使用ResizeObserver就像实例化一个new ResizeObserver对象和一个回调函数一样简单:

const myObserver = new ResizeObserver(entries => {
    // 遍历条目,做一些事情
});

然后,我们可以在实例中调用observe,并通过一个元素来观察:

const someEl = document.querySelector('.some-element');
const someOtherEl = document.querySelector('.some-other-element');

myObserver.observe(someEl);
myObserver.observe(someOtherEl);

对于每个条目,我们得到一个带有contentRecttarget属性的对象。target是DOM元素本身,contentRect是一个具有以下属性的对象:widthheightxytoprightbottomleft

与元素的getBoundingClientRect不同,contentRectwidthheight值不包含padding的值。contentRect.top是元素的顶部paddingcontentRect.left是元素左边的padding

如果我们想要在元素的大小变化时记录观察到的元素的宽度和高度,我们可以这样做:

const myObserver = new ResizeObserver(entries => {
    entries.forEach(entry => {
        console.log('width', entry.contentRect.width);
        console.log('height', entry.contentRect.height);
    });
});

const someEl = document.querySelector('.some-element');
myObserver.observe(someEl);

来看一个简单的示例,调整浏览器视窗的大小时,元素的大小会受到影响,而且渐变的角度和文本内容也会有变化。

<div class="box">
    <h3 class="info"></h3>
</div>
<div class="box small">
    <h3 class="info"></h3>
</div>

添加点样式:

body {
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 2vw;
    box-sizing: border-box;
}
.box {
    text-align: center;
    height: 20vh;
    border-radius: 8px;
    box-shadow: 0 0 4px rgba(0,0,0,.25);

    display: flex;
    justify-content: center;
    align-items: center;
    padding: 1vw
}

.box h3 {
    color: #fff;
    margin: 0;
    font-size: 5vmin;
    text-shadow: 0 0 10px rgba(0,0,0,0.4);
}

.box.small {
    max-width: 550px;
    margin: 1rem auto;
}

注意,我们不要把渐变背景应用到.box元素上。当页面第一次加载时,ResizeObserver将被调用一次,然后渐变背景才会被应用。

下面添加对应的JavaScript代码:

const boxes = document.querySelectorAll('.box');
let callbackFired = 0;

const myObserver = new ResizeObserver(entries => {
    for (let entry of entries) {
        callbackFired++
        const infoEl = entry.target.querySelector('.info');
        const width = Math.floor(entry.contentRect.width);
        const height = Math.floor(entry.contentRect.height);

        const angle = Math.floor(width / 360 * 100);
        const gradient = `linear-gradient(${ angle }deg, rgba(0,143,104,1) 50%, rgba(250,224,66,1) 50%)`;

        entry.target.style.background = gradient;

        infoEl.innerText = `
        I'm ${ width }px and ${ height }px tall

        Callback fired: ${callbackFired}
        `;
    }
});

boxes.forEach(box => {
    myObserver.observe(box);
});

效果如下:

当你拖动浏览器窗口,改变其大小时,看到的效果如下:

上面的示例中,使用了for ... of循环来遍历观察者的回调中的entries,其实在entries上使用forEach可以得到相同的效果。

浏览器的兼容性

ResizeObserver目前仅在Chrome 64+浏览器得到了支持。但是有一种简单的方法来检查浏览器是否支持它。如果支持的话,就使用它,如果不支持就使用window.onresize

if ('ResizeObserver' in window) {
    // new ResizeObserver( callback );
}
else {
    // window.addEventListener('resize', callback)
}

除了上面的检测方法之外,还可以使用基于MutationObserver API写的Polyfill

扩展阅读

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/javascript/ResizeObserver-api.htmljordans for sale crimson