相信开发者们,在日常的业务开发中,可能会遇到需要对 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'))
执行如下
这里是我们主动去触发 事件,当让和原生的不同,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)
})
回顾
逻辑不多就 不回顾了。 简单说下好了 如果不想污染 addEventListener 添加resize 事件,就直接用 二次改造的代码 逻辑就好了。 如果为了更少的 去兼容,或者代码统一,就有 第三节的内容好了。