尝试解读resize-observer-polyfill源码(3)

廖鸿达
2023-12-01

这是resize-observer-polyfill源码解读的第三章,在上一章的末尾,由于ResizeObserverController类内部没有调用自身内部的函数,并且controller作为单例参数被传入到了ResizeObserverSPI内。所以对于controller的操作,也都存在与SPI中。
先看看完整代码

import {Map} from './shims/es6-collections.js';
import ResizeObservation from './ResizeObservation.js';
import ResizeObserverEntry from './ResizeObserverEntry.js';
import getWindowOf from './utils/getWindowOf.js';

export default class ResizeObserverSPI {
    /**
     * Collection of resize observations that have detected changes in dimensions
     * of elements.
     *
     * @private {Array<ResizeObservation>}
     */
    activeObservations_ = [];

    /**
     * Reference to the callback function.
     *
     * @private {ResizeObserverCallback}
     */
    callback_;

    /**
     * Public ResizeObserver instance which will be passed to the callback
     * function and used as a value of it's "this" binding.
     *
     * @private {ResizeObserver}
     */
    callbackCtx_;

    /**
     * Reference to the associated ResizeObserverController.
     *
     * @private {ResizeObserverController}
     */
    controller_;

    /**
     * Registry of the ResizeObservation instances.
     *
     * @private {Map<Element, ResizeObservation>}
     */
    observations_ = new Map();

    /**
     * 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;
        this.callbackCtx_ = callbackCtx;
    }

    /**
     * Starts observing provided element.
     *
     * @param {Element} target - Element to be observed.
     * @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_;

        // Do nothing if element is already being observed.
        if (observations.has(target)) {
            return;
        }

        observations.set(target, new ResizeObservation(target));

        this.controller_.addObserver(this);

        // Force the update of observations.
        this.controller_.refresh();
    }

    /**
     * Stops observing provided element.
     *
     * @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);

        if (!observations.size) {
            this.controller_.removeObserver(this);
        }
    }

    /**
     * Stops observing all elements.
     *
     * @returns {void}
     */
    disconnect() {
        this.clearActive();
        this.observations_.clear();
        this.controller_.removeObserver(this);
    }

    /**
     * Collects observation instances the associated element of which has changed
     * it's content rectangle.
     *
     * @returns {void}
     */
    gatherActive() {
        this.clearActive();

        this.observations_.forEach(observation => {
            if (observation.isActive()) {
                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_;

        // 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);
        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;
    }
}

略过变量声明部分。直接看这个类的constructor

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;
    this.callbackCtx_ = callbackCtx;
}

先是熟悉的参数类型检测。如果callback不是函数将抛出类型错误。
随后是将参数一一赋值为类内的属性。
再往下,看到了三个有点眼熟的方法

/**
 * Starts observing provided element.
 *
 * @param {Element} target - Element to be observed.
 * @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_;

    // Do nothing if element is already being observed.
    if (observations.has(target)) {
        return;
    }

    observations.set(target, new ResizeObservation(target));

    this.controller_.addObserver(this);

    // Force the update of observations.
    this.controller_.refresh();
}

/**
 * Stops observing provided element.
 *
 * @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);

    if (!observations.size) {
        this.controller_.removeObserver(this);
    }
}

/**
 * Stops observing all elements.
 *
 * @returns {void}
 */
disconnect() {
    this.clearActive();
    this.observations_.clear();
    this.controller_.removeObserver(this);
}
// Expose public methods of ResizeObserver.
[
    'observe',
    'unobserve',
    'disconnect'
].forEach(method => {
    ResizeObserver.prototype[method] = function () {
        return observers.get(this)[method](...arguments);
    };
});

由于ResizeObserver类中的observers的每一个元素都以ResizeObserver为键,以对应的ResizeObserverSPI为值。所以以上的三个函数就是在ResizeObserver类中向外暴露的三个函数。

ro.observe(document.getElementById('container'));
ro.unobserve(document.getElementById('container'));
ro.disconnect();

先来看看observe方法

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".');
	}

首先是检查参数类型,然后是检查环境,如果环境中不存在Element接口,那就意味着当前的环境不是浏览器,也就没有监听HTML元素的必要了。随后,调用getWindowOf检查获取当前环境的全局对象,一般在浏览器中是document或window,如果传入的参数不是继承于全局对象。意味着传入的参数肯定不是HTML元素,于是抛出类型错误。

	const observations = this.observations_;

    // Do nothing if element is already being observed.
    if (observations.has(target)) {
        return;
    }

    observations.set(target, new ResizeObservation(target));

    this.controller_.addObserver(this);

    // Force the update of observations.
    this.controller_.refresh();
}

接下来就是检查observations(Map)是否已经存在我们要监听的HTML元素,如果不存在,则以这个HTML元素为键、新建ResizeObservation对象为值插入到Map中。
调用controller(单例引用)并把这个SPI实例添加到controller(单例)的observers_属性中,并调用一次刷新方法。

/**
 * Adds observer to observers list.
 *
 * @param {ResizeObserverSPI} observer - Observer to be added.
 * @returns {void}
 */
addObserver(observer) {
    if (!~this.observers_.indexOf(observer)) {
        this.observers_.push(observer);
    }

    // Add listeners if they haven't been added yet.
    if (!this.connected_) {
        this.connect_();
    }
}

到此为止,SPI首次调用了控制器中的refresh方法,所以让我们回到ResizeObserverController中,找到refresh方法。

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();
    }
}

需要注意的是,当ResizeObserverController构建时,refresh方法会被一个节流函数包裹,所以不需要担心递归造成的性能问题。我们通过updateObservers_得知了是否发生变化。但它是如何实现的呢?

/**
 * Updates every observer from observers list and notifies them of queued
 * entries.
 *
 * @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;
}

通过分析ResizeObserverSPI的observe方法可知,这里observers_是由ResizeObserverSPI实例组成的数组,对其应用filter返回处于活动状态(hasActive返回true)的observer。

gatherActive() {
    this.clearActive();

    this.observations_.forEach(observation => {
        if (observation.isActive()) {
            this.activeObservations_.push(observation);
        }
    });
}

//...

clearActive() {
    this.activeObservations_.splice(0);
}

//...

hasActive() {
    return this.activeObservations_.length > 0;
}

hasActive执行前需要先调用gatherActive,遍历observations_(由dom元素作为键,ResizeObservation实例作为值),通过执行每一个ResizeObservation实例的isActive方法来得知其是否处于活动状态,并添加到activeObservations_列表。

当通过过滤器滤出处于活动状态的所有监听器之后,控制器会通过forEach遍历其中的每一个ResizeObserverSPI并调用它们的broadcastActive方法。

/**
 * 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_;

    // 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);
    this.clearActive();
}

为什么在ResizeObserverSPI中要先调用gatherActive设置activateObservations_而不是直接返回结果?
答案很明显了,是为了能在这里直接复用之前的结果。

看到这里,可算是基本理清这东西的结构了。当ResizeObserver被调用时,会创建一个控制器单例,并生成一个ResizeObserverSPI加入observers,即一个控制器对应多个SPI,控制器中的observers_也是一个ResizeObserverSPI数组。当ResizeObserver实例的observe函数被调用时其内部的Observations会添加对应的ResizeObservation,也就是说,每一个SPI可以有一个回调函数,但可以监听多个HTML元素(创建多个ResizeObservation)并作出响应。

一直疑惑为什么作者要在ResizeObserver里保存一个由ResizeObserverSPI组成的Map,然后又在Controller单例里写了另一个由ResizeObserverSPI组成的数组。ResizeObserver中的那个observers除了用于向外暴露方法以外也没有其他用处了。

 类似资料: