给addEventListener 添加resize 事件

129 阅读4分钟

相信开发者们,在日常的业务开发中,可能会遇到需要对 DOM, 或者 Window 进行resize 监听。例如我最近就需要这样的一个需求,需要监听DOM 元素的UI改变,进行重渲染。

ResizeObserver

本质上底层实现 就是依赖于 ResizeObserver,这个 Web API.

const resizeObserverIns = new ResizeObserver((entries, observer) => {
    // entries 所有监听 ResizeObserverEntry https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserverEntry
    // observer 自身引用
})

主要有三个 属性方法

  • observe(ele: Element) 监听 element 的resize
// 监听 body
const ele = document.body
resizeObserverIns.oberve(ele as Element)
  • unobserve 取消element 的resize
// 取消监听 body
const ele = document.body
resizeObserverIns.unobserve(ele as Element)
  • disconnect 取消该 实例上所有的 element 监听
// 取消所有监听
resizeObserverIns.disconnect()

详细的使用可以 查看 MDN

二次构造

根据MDN 的介绍,本质上 是 通过 observe来监听 Element 再通过 构造函数 的 entries 来获取 resize callback。

这样子有点绕,不符合 我们 addEventListener 的用法.

正常简单的 addEventListener 用法 应该是这样. EvenListener 是直接添加到 EventTarget 上的: addEventListener(type, listener);

所以,咱就来简单改造下吧

// 为了兼容性这里就用的 polyfill (npm i resize-observer-polyfill)
import ResizeObserver from 'resize-observer-polyfill';

// 定义 EventListener
type OberverCallback = (entry: ResizeObserverEntry) => any;

class ResizeObserverDispatcher {
  private resizeObserver: ResizeObserver; // ResizeObserver 实例
  private observerEntryMap: WeakMap<Element, Set<OberverCallback>> = new WeakMap(); // Element + Set<EventListener> 的Map, 这里用Set 对 listener进行处理,防止重复添加
  constructor() {
    this.resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
      // console.log('ResizeObserver:', entries);
      for (let entry of entries) {
        // 获取当前 element (target) 下的 所有 EventListner
        const setCb = this.observerEntryMap.get(entry.target);

        if (!setCb) return;
        // 拷贝一份,并且转为Array
        const cloneArraySet = Array.from(setCb);
        cloneArraySet.forEach((cb) => {
          cb(entry);
        });
      }
    });
  }
  // 实现 addEventListener 将监听器 注册和 Element 上,内部是用Map 做映射
  observe(target: Element, callback: OberverCallback) {
    this.resizeObserver.observe(target);
    let targetSet = this.observerEntryMap.get(target);
    if (!targetSet) {
      targetSet = new Set();
      this.observerEntryMap.set(target, targetSet);
    }
    targetSet.add(callback);
  }
  // 实现removeEventListener
  unobserve(target: Element, callback?: OberverCallback) {
    if (!target) return;
    // 没用 callback 的话,就全部清除
    if (!callback) {
      this.resizeObserver.unobserve(target);
      this.observerEntryMap.delete(target);
    } else {
      let targetSet = this.observerEntryMap.get(target);
      if (!targetSet) return;
      targetSet.delete(callback);
      // 校验是否还有 EventListener 没有的话,就直接 清除 unobserve
      if (targetSet.size === 0) {
        this.resizeObserver.unobserve(target);
        this.observerEntryMap.delete(target);
      }
    }
  }
}

const ResizeObserverIns = new ResizeObserverDispatcher();

PS: 这里是用TS 写的,在 浏览器控制台,可以直接复制进去,去掉ts 的申明和 这里的 polyfill

class ResizeObserverDispatcher {
    resizeObserver // ResizeObserver 实例
    observerEntryMap = new WeakMap(); // Element + Set<EventListener> 的Map, 这里用Set 对 listener进行处理,防止重复添加
  constructor() {
    this.resizeObserver = new ResizeObserver((entries) => {
      // console.log('ResizeObserver:', entries);
      for (let entry of entries) {
        // 获取当前 element (target) 下的 所有 EventListner
        const setCb = this.observerEntryMap.get(entry.target);

        if (!setCb) return;
        // 拷贝一份,并且转为Array
        const cloneArraySet = Array.from(setCb);
        cloneArraySet.forEach((cb) => {
          cb(entry);
        });
      }
    });
  }
  // 实现 addEventListener 将监听器 注册和 Element 上,内部是用Map 做映射
  observe(target, callback) {
    this.resizeObserver.observe(target);
    let targetSet = this.observerEntryMap.get(target);
    if (!targetSet) {
      targetSet = new Set();
      this.observerEntryMap.set(target, targetSet);
    }
    targetSet.add(callback);
  }
  // 实现removeEventListener
  unobserve(target, callback) {
    if (!target) return;
    // 没用 callback 的话,就全部清除
    if (!callback) {
      this.resizeObserver.unobserve(target);
      this.observerEntryMap.delete(target);
    } else {
      let targetSet = this.observerEntryMap.get(target);
      if (!targetSet) return;
      targetSet.delete(callback);
      // 校验是否还有 EventListener 没有的话,就直接 清除 unobserve
      if (targetSet.size === 0) {
        this.resizeObserver.unobserve(target);
        this.observerEntryMap.delete(target);
      }
    }
  }
}

const ResizeObserverIns = new ResizeObserverDispatcher();

// 监听 body, log offsetWidth
ResizeObserverIns.observe(document.body, (e) => {
    console.log(e.target.offsetWidth)
})

注册 addEventListener resize 事件。

在开始直接可以看下上面 MDN 的介绍,或者,我这边大致讲下。 本质上 addEventListener 是将指定的监听器注册到 EventTarget 的 事件上。 到该 对象出发指定的事件,这个EventListener 就会被执行。 但是这里并没有限制事件的定义, 并不是一定要是 click, mouseenter 等等等,这些只不过是 浏览器自己支持的事件,就比如,我们也可以定义 myEvent 这个事件

document.body.addEventListener('myEvent', (e) => {
  console.log('===========myEvent==========\n', e)
})

document.body.dispatchEvent(new Event('myEvent'))

执行如下

image.png

这里是我们主动去触发 事件,当让和原生的不同,MDN 上面也讲得很清晰 dispatchEvent (和经由浏览器触发,并通过事件循环异步调用事件处理程序的“原生”事件不同,dispatchEvent() 会同步调用事件处理函数)

但是这个和我们实现使用上, 暂不考虑。

所以综上所述,我们也可以 自己定义这个 resize 事件, 底层逻辑就是 注册了 ResizeObserverIns.observe

// 由于我们 将事件直接 注册到了 EventTarget 上面了,所以本质上并不需要 二次改造的内容。
const RESIZEEVENTKEY = 'resize'
const _ResizeOverserIns = new ResizeObserver((entries) => {
  for (let entry of entries) {
    const target = entry.target
    target.dispatchEvent(new Event(RESIZEEVENTKEY))
  }
})
const originAddEventListener = EventTarget.prototype.addEventListener
EventTarget.prototype.addEventListener = function _addEventListener(type, callback, options) {
  originAddEventListener.call(this, type, callback, options)
  if (type === RESIZEEVENTKEY) {
    if (this instanceof Element) {
      _ResizeOverserIns.observe(this)
    }
  }
}
document.body.addEventListener('resize', (e) => {
    console.log(e)
})

image.png

回顾

逻辑不多就 不回顾了。 简单说下好了 如果不想污染 addEventListener 添加resize 事件,就直接用 二次改造的代码 逻辑就好了。 如果为了更少的 去兼容,或者代码统一,就有 第三节的内容好了。