resize-observer-profill

·  阅读 133

resize-observer-profill【github】

一、思维导图

image.png 本工具主要使用的就是比较熟悉的 发布-订阅设计模式。

1.图中 ResizeObserver 就是对外提供的一个工具类,可以理解为 ResizeObserverSPI 的代理类,使用 Map 对象维护这层代理关系,通过使用ResizeObserver 工具类暴露的实例方法来使用 ResizeObserverSPI 内部方法。

2.ResizeObserverSPI 可以当成是中介,每个中介可能给的优惠不同(callback)。每个中介又维护各类房屋需求的客户信息(ResizeObservation 类)(observations_:[]维护客户关系)。

  1. ResizeObserverController 可以当成房地产商(先只考虑一个房地产商),提供各种房型信息,需要维护中介的关系(observers_:[] 维护中介关系)。

  2. ResizeObservation 类 可以当做是订阅客户的需求信息(期望房型、面积、福利...)

  3. ResizeObserverEntry 类 可以当作是房屋的包装信息(比如套内面积、赠送面积、是否精装...)

二、具体流程

  1. 发布

实例化代理类 ResizeObserver ,创建不同优惠(callback)的中介者,并让中介与房地产商 ResizeObserverController 维护关系,当有一个中介,房地产商一有新房源就要通知中介;如果没有认识的中介,就不需要通知(没得通知,估计要亏)(new ResizeObserver(callback))

  1. 订阅

客户根据优惠到对应的中介了解情况,中介需要登记客户信息以及客户需求(ResizeObservation),客户只需要等待通知。(observe)

  1. 通知

有新房源,房地产商就通知所有的中介,中介需要筛选所有客户信息,通知满足的客户需求的客户,这时候可能中介就会把房源信息(ResizeObserverEntry )稍微包装一下在告诉客户(都是套路)。(broadcastActive)

 

三、代码解析

  1. 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();
        }
    }
    ...
}
复制代码
  1. 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;
    }
}
复制代码
  1. 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;
    }
}
复制代码
分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改