resize-observer-profill【github】
一、思维导图
本工具主要使用的就是比较熟悉的 发布-订阅设计模式。
1.图中 ResizeObserver 就是对外提供的一个工具类,可以理解为 ResizeObserverSPI 的代理类,使用 Map 对象维护这层代理关系,通过使用ResizeObserver 工具类暴露的实例方法来使用 ResizeObserverSPI 内部方法。
2.ResizeObserverSPI 可以当成是中介,每个中介可能给的优惠不同(callback)。每个中介又维护各类房屋需求的客户信息(ResizeObservation 类)(observations_:[]维护客户关系)。
-
ResizeObserverController 可以当成房地产商(先只考虑一个房地产商),提供各种房型信息,需要维护中介的关系(observers_:[] 维护中介关系)。
-
ResizeObservation 类 可以当做是订阅客户的需求信息(期望房型、面积、福利...)
-
ResizeObserverEntry 类 可以当作是房屋的包装信息(比如套内面积、赠送面积、是否精装...)
二、具体流程
- 发布
实例化代理类 ResizeObserver ,创建不同优惠(callback)的中介者,并让中介与房地产商 ResizeObserverController 维护关系,当有一个中介,房地产商一有新房源就要通知中介;如果没有认识的中介,就不需要通知(没得通知,估计要亏)(new ResizeObserver(callback))
- 订阅
客户根据优惠到对应的中介了解情况,中介需要登记客户信息以及客户需求(ResizeObservation),客户只需要等待通知。(observe)
- 通知
有新房源,房地产商就通知所有的中介,中介需要筛选所有客户信息,通知满足的客户需求的客户,这时候可能中介就会把房源信息(ResizeObserverEntry )稍微包装一下在告诉客户(都是套路)。(broadcastActive)
三、代码解析
- ResizeObserverController 类
// A list of substrings of CSS properties used to find transition events that
// might affect dimensions of observed elements.
const transitionKeys = ['top', 'right', 'bottom', 'left', 'width', 'height', 'size', 'weight'];
...
export default class ResizeObserverController {
...
/**
* Creates a new instance of ResizeObserverController.
*
* @private
*/
constructor() {
this.onTransitionEnd_ = this.onTransitionEnd_.bind(this); // resize回调函数
this.refresh = throttle(this.refresh.bind(this), REFRESH_DELAY); // 刷新节流函数
}
/**
* Adds observer to observers list.
* 添加 resizeObserverSPI 实例,如果还未启动监听,则默认调用 connect_ 开始监听
* @param {ResizeObserverSPI} observer - Observer to be added.
* @returns {void}
*/
addObserver(observer) {
// 如果已存在该 ResizeObserverSPI 实例 则跳过,否则,添加到observers_集合
if (!~this.observers_.indexOf(observer)) {
this.observers_.push(observer);
}
// Add listeners if they haven't been added yet.
if (!this.connected_) {
this.connect_();
}
}
/**
* Removes observer from observers list.
* 移除 resizeObserverSPI 实例,如果当前没有 resizeObserverSPI 实例,则默认调用disconnect_ 关闭监听
* @param {ResizeObserverSPI} observer - Observer to be removed.
* @returns {void}
*/
removeObserver(observer) {
const observers = this.observers_;
const index = observers.indexOf(observer);
// Remove observer if it's present in registry.
if (~index) {
observers.splice(index, 1);
}
// Remove listeners if controller has no connected observers.
if (!observers.length && this.connected_) {
this.disconnect_();
}
}
/**
* Invokes the update of observers. It will continue running updates insofar
* it detects changes.
* 刷新函数,通知 resizeObserverSPI
* @returns {void}
*/
refresh() {
const changesDetected = this.updateObservers_();
// Continue running updates if changes have been detected as there might
// be future ones caused by CSS transitions.
if (changesDetected) {
this.refresh();
}
}
/**
* Updates every observer from observers list and notifies them of queued
* entries.
* 调用关联的所有 resizeObserverSPI 实例,广播更新
* @private
* @returns {boolean} Returns "true" if any observer has detected changes in
* dimensions of it's elements.
*/
updateObservers_() {
// Collect observers that have active observations.
const activeObservers = this.observers_.filter(observer => {
return observer.gatherActive(), observer.hasActive();
});
// Deliver notifications in a separate cycle in order to avoid any
// collisions between observers, e.g. when multiple instances of
// ResizeObserver are tracking the same element and the callback of one
// of them changes content dimensions of the observed target. Sometimes
// this may result in notifications being blocked for the rest of observers.
activeObservers.forEach(observer => observer.broadcastActive());
return activeObservers.length > 0;
}
/**
* Initializes DOM listeners.
* 开始监听
* 重点:resize 监听机制(监听 window.resize document.transitionEnd 以及 观察 document 改变)
* @private
* @returns {void}
*/
connect_() {
// Do nothing if running in a non-browser environment or if listeners
// have been already added.
if (!isBrowser || this.connected_) {
return;
}
// Subscription to the "Transitionend" event is used as a workaround for
// delayed transitions. This way it's possible to capture at least the
// final state of an element.
document.addEventListener('transitionend', this.onTransitionEnd_);
window.addEventListener('resize', this.refresh);
if (mutationObserverSupported) {
this.mutationsObserver_ = new MutationObserver(this.refresh);
this.mutationsObserver_.observe(document, {
attributes: true,
childList: true,
characterData: true,
subtree: true
});
} else {
document.addEventListener('DOMSubtreeModified', this.refresh);
this.mutationEventsAdded_ = true;
}
this.connected_ = true;
}
/**
* Removes DOM listeners.
* 移除监听,等待
* @private
* @returns {void}
*/
disconnect_() {
// Do nothing if running in a non-browser environment or if listeners
// have been already removed.
if (!isBrowser || !this.connected_) {
return;
}
document.removeEventListener('transitionend', this.onTransitionEnd_);
window.removeEventListener('resize', this.refresh);
if (this.mutationsObserver_) {
this.mutationsObserver_.disconnect();
}
if (this.mutationEventsAdded_) {
document.removeEventListener('DOMSubtreeModified', this.refresh);
}
this.mutationsObserver_ = null;
this.mutationEventsAdded_ = false;
this.connected_ = false;
}
/**
* "Transitionend" event handler.
* transitionEnd 过渡回调,判断过渡属性是否是监听属性,如果是则通知 resizeObserverSPI 刷新
* @private
* @param {TransitionEvent} event
* @returns {void}
*/
onTransitionEnd_({propertyName = ''}) {
// Detect whether transition may affect dimensions of an element.
const isReflowProperty = transitionKeys.some(key => {
return !!~propertyName.indexOf(key);
});
if (isReflowProperty) {
this.refresh();
}
}
...
}
- ResizeObserverSPI 类
...
export default class ResizeObserverSPI {
...
/**
* Creates a new instance of ResizeObserver.
* 构造函数
* @param {ResizeObserverCallback} callback - Callback function that is invoked
* when one of the observed elements changes it's content dimensions.
* @param {ResizeObserverController} controller - Controller instance which
* is responsible for the updates of observer.
* @param {ResizeObserver} callbackCtx - Reference to the public
* ResizeObserver instance which will be passed to callback function.
*/
constructor(callback, controller, callbackCtx) {
if (typeof callback !== 'function') {
throw new TypeError('The callback provided as parameter 1 is not a function.');
}
this.callback_ = callback; // 回调函数
this.controller_ = controller; // 全局 ResizeObserverController 实例
this.callbackCtx_ = callbackCtx; // 关联 ResizeObserver 实例
}
/**
* Starts observing provided element.
* 监听元素,传入一个DOM节点
* @param {Element} target - Element to be observed. DOM节点
* @returns {void}
*/
observe(target) {
if (!arguments.length) {
throw new TypeError('1 argument required, but only 0 present.');
}
// Do nothing if current environment doesn't have the Element interface.
if (typeof Element === 'undefined' || !(Element instanceof Object)) {
return;
}
if (!(target instanceof getWindowOf(target).Element)) {
throw new TypeError('parameter 1 is not of type "Element".');
}
const observations = this.observations_; // DOM节点包装 ResizeObservation 实例集合
// Do nothing if element is already being observed. 如果已存在,则跳过
if (observations.has(target)) {
return;
}
// 否则,包装该DOM节点 创建ResizeObservation实例
observations.set(target, new ResizeObservation(target));
this.controller_.addObserver(this); // 登记 ResizeObserverSPI 实例
// Force the update of observations.
this.controller_.refresh(); // 默认刷新一次
}
/**
* Stops observing provided element.
* 取消DOM监听
* @param {Element} target - Element to stop observing.
* @returns {void}
*/
unobserve(target) {
if (!arguments.length) {
throw new TypeError('1 argument required, but only 0 present.');
}
// Do nothing if current environment doesn't have the Element interface.
if (typeof Element === 'undefined' || !(Element instanceof Object)) {
return;
}
if (!(target instanceof getWindowOf(target).Element)) {
throw new TypeError('parameter 1 is not of type "Element".');
}
const observations = this.observations_;
// Do nothing if element is not being observed.
// 不存在监听数组,跳过
if (!observations.has(target)) {
return;
}
// 否则,删除
observations.delete(target);
// 当前 resizeObserverSPI 实例监听的DOM节点为0,则controller_ 实例先注销 该resizeObserverSPI
if (!observations.size) {
this.controller_.removeObserver(this);
}
}
/**
* Stops observing all elements.
* 清空监听
* @returns {void}
*/
disconnect() {
this.clearActive(); // 清除更新节点数组
this.observations_.clear(); // 清除观察DOM节点
this.controller_.removeObserver(this); // 移除resizeObserverSPI实例
}
/**
* Collects observation instances the associated element of which has changed
* it's content rectangle.
* 获取满足resize事件的DOM节点(resizeObservation 实例)集合
* @returns {void}
*/
gatherActive() {
this.clearActive(); // 清除待更新节点数组
this.observations_.forEach(observation => {
if (observation.isActive()) { // 判断监听节点是否满足resize,如果是
this.activeObservations_.push(observation); // 则加入待更新节点数组
}
});
}
/**
* Invokes initial callback function with a list of ResizeObserverEntry
* instances collected from active resize observations.
* 遍历所有待更新节点,执行回调
* @returns {void}
*/
broadcastActive() {
// Do nothing if observer doesn't have active observations.
// 如果当前没有待更新节点 返回
if (!this.hasActive()) {
return;
}
const ctx = this.callbackCtx_; // resizeObserver 实例
// Create ResizeObserverEntry instance for every active observation.
// 封装回调事件参数
const entries = this.activeObservations_.map(observation => {
return new ResizeObserverEntry(
observation.target,
observation.broadcastRect()
);
});
this.callback_.call(ctx, entries, ctx); // 执行对应resizeObserver回调函数
this.clearActive(); // 清空所有待更新节点
}
/**
* Clears the collection of active observations.
* 清除待更新节点
* @returns {void}
*/
clearActive() {
this.activeObservations_.splice(0);
}
/**
* Tells whether observer has active observations.
* 判断是否有待更新节点
* @returns {boolean}
*/
hasActive() {
return this.activeObservations_.length > 0;
}
}
- ResizeObservation 类
export default class ResizeObservation {
/**
* Reference to the observed element.
* 监听节点DOM
* @type {Element}
*/
target;
/**
* Broadcasted width of content rectangle.
* 上一次宽度
* @type {number}
*/
broadcastWidth = 0;
/**
* Broadcasted height of content rectangle.
* 上一次高度
* @type {number}
*/
broadcastHeight = 0;
/**
* Reference to the last observed content rectangle.
* 上一次rect
* @private {DOMRectInit}
*/
contentRect_ = createRectInit(0, 0, 0, 0);
/**
* Creates an instance of ResizeObservation.
*
* @param {Element} target - Element to be observed.
*/
constructor(target) {
this.target = target;
}
/**
* Updates content rectangle and tells whether it's width or height properties
* have changed since the last broadcast.
* 判断是否满足resize
* @returns {boolean}
*/
isActive() {
const rect = getContentRect(this.target); // 获取rect
this.contentRect_ = rect;
// 比较宽、高
return (
rect.width !== this.broadcastWidth ||
rect.height !== this.broadcastHeight
);
}
/**
* Updates 'broadcastWidth' and 'broadcastHeight' properties with a data
* from the corresponding properties of the last observed content rectangle.
* 更新宽、高
* @returns {DOMRectInit} Last observed content rectangle.
*/
broadcastRect() {
const rect = this.contentRect_;
this.broadcastWidth = rect.width;
this.broadcastHeight = rect.height;
return rect;
}
}