🔮

ResizeObserver の使い方と無限ループ対策

2023/01/29に公開約2,300字

特徴

  • 特定要素のサイズ変更を検知できる
  • ポーリングし続けたりしているのではない
  • 標準機能だった

コード

Vue.js 2 用
export const mod_resize_observer = {
  data() {
    return {
      base_w: 1,
      base_h: 1,
    }
  },
  mounted() {
    this.ro_start()
  },
  beforeDestroy() {
    this.ro_stop()
  },
  methods: {
    ro_start() {
      this.ro_stop()
      this.$ro = new ResizeObserver((entries, observer) => {
        entries.forEach(entry => {
          this.base_w = entry.contentRect.width
          this.base_h = entry.contentRect.height
        })
      })
      this.$ro.observe(this.$el.querySelector("#target"))
    },
    ro_stop() {
      if (this.$ro) {
        this.$ro.disconnect()
        this.$ro = null
      }
    },
  },
}

beforeDestroy のタイミングで disconnect しないといけないのかどうかはよくわかってない

CSSに渡すには?

JavaScript 側の変数がそのままCSS変数になるような実験的な仕組みもあったような気もするけどとりあえずこれがわかりやすい

AnyComponent(:style="{'--base_w': `${base_w}px`, '--base_h': `${base_h}px`}")

内部で何かするときに数値として持っておいた方が便利かもしれないのでCSSに渡すタイミングで単位をつけるのがいいかもしれない
CSS側で calc(var(--base_w) * 1px) としてもいいけどCSS側で数値のままにしておくメリットはとくにない

padding を知るには?

entry.contentRect.left
entry.contentRect.top

left top が padding 相当になっている

何個も監視できる

observe で何個も監視対象を登録できる

無限ループ対策

画面が震えている場合、無限ループに陥っている
width, height は単位が px だけど小数になっている(小数点以下5桁)
これをそのままCSSに反映すると誤差によって再度リサイズが起きる
とりあえず Math.round(width) としたら無限ループは起きにくくなった
それでも震える場合は次のように差分と閾値で判断する

const w = entry.contentRect.width
const h = entry.contentRect.height
const dw = Math.abs(this.base_w - w)
const dh = Math.abs(this.base_h - h)
const threshold = 1.5
if (dw > threshold || dh > threshold) {
  this.base_w = w
  this.base_h = h
}

対象が消えるとどうなる?

ありがたいことにいきなり消えるのではなくサイズが 0, 0 の状態でコールバックされる
なので存在の有無で処理を分けられる
例えば関連要素を巻き込まないようにするなら 0, 0 は除外する

const w = entry.contentRect.width
const h = entry.contentRect.height
if (w === 0 && h === 0) {
  // 対象が消えた場合
}
if (w > 0 && h > 0) {
  // 関連要素を巻き込まないようにする場合
}

テストが書きにくい件

jest で ReferenceError: ResizeObserver is not defined となるので次のようにしてモックを用意する

window.ResizeObserver = window.ResizeObserver || jest.fn().mockImplementation(() => ({
  disconnect: jest.fn(),
  observe: jest.fn(),
  unobserve: jest.fn(),
}))

https://stackoverflow.com/questions/68679993/referenceerror-resizeobserver-is-not-defined

Discussion

ログインするとコメントできます