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

鲜于子琪
2023-12-01

书接上文,我们已经看完了ResizeObserver类,但该类的constructor中使用了两个外部导入的类,分别是ResizeObserverController和ResizeObserverSPI,先来看看ResizeObserverController的完整源码

import isBrowser from './utils/isBrowser.js';
import throttle from './utils/throttle.js';

// Minimum delay before invoking the update of observers.
const REFRESH_DELAY = 20;

// 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'];

// Check if MutationObserver is available.
const mutationObserverSupported = typeof MutationObserver !== 'undefined';

/**
 * Singleton controller class which handles updates of ResizeObserver instances.
 */
export default class ResizeObserverController {
    /**
     * Indicates whether DOM listeners have been added.
     *
     * @private {boolean}
     */
    connected_ = false;

    /**
     * Tells that controller has subscribed for Mutation Events.
     *
     * @private {boolean}
     */
    mutationEventsAdded_ = false;

    /**
     * Keeps reference to the instance of MutationObserver.
     *
     * @private {MutationObserver}
     */
    mutationsObserver_ = null;

    /**
     * A list of connected observers.
     *
     * @private {Array<ResizeObserverSPI>}
     */
    observers_ = [];

    /**
     * Holds reference to the controller's instance.
     *
     * @private {ResizeObserverController}
     */
    static instance_ = null;

    /**
     * Creates a new instance of ResizeObserverController.
     *
     * @private
     */
    constructor() {
        this.onTransitionEnd_ = this.onTransitionEnd_.bind(this);
        this.refresh = throttle(this.refresh.bind(this), REFRESH_DELAY);
    }

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

    /**
     * Removes observer from observers list.
     *
     * @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.
     *
     * @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.
     *
     * @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.
     *
     * @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.
     *
     * @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();
        }
    }

    /**
     * Returns instance of the ResizeObserverController.
     *
     * @returns {ResizeObserverController}
     */
    static getInstance() {
        if (!this.instance_) {
            this.instance_ = new ResizeObserverController();
        }

        return this.instance_;
    }
}

先从上方的变量声明部分看起

// Minimum delay before invoking the update of observers.
const REFRESH_DELAY = 20;

// 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'];

// Check if MutationObserver is available.
const mutationObserverSupported = typeof MutationObserver !== 'undefined';

REFRESH_DELAY observers的最小更新间隔
tansitionKeys resize-observer-polyfill默认会监听这几个属性(根据其注释的描述,似乎会监视名字中包含这些字符串的属性)
mutationObserverSupported 通过typeof返回当前环境是否支持MutationObserver
然后往下,我们能看到ReisizeObserverController类内部的变量声明部分

/**
 * Singleton controller class which handles updates of ResizeObserver instances.
 */
export default class ResizeObserverController {
    /**
     * Indicates whether DOM listeners have been added.
     *
     * @private {boolean}
     */
    connected_ = false;

    /**
     * Tells that controller has subscribed for Mutation Events.
     *
     * @private {boolean}
     */
    mutationEventsAdded_ = false;

    /**
     * Keeps reference to the instance of MutationObserver.
     *
     * @private {MutationObserver}
     */
    mutationsObserver_ = null;

    /**
     * A list of connected observers.
     *
     * @private {Array<ResizeObserverSPI>}
     */
    observers_ = [];

    /**
     * Holds reference to the controller's instance.
     *
     * @private {ResizeObserverController}
     */
    static instance_ = null;

第一行就点明了这个类在实际使用中是一个单例,再结合上面看到的REFRESH_DELAYconnected、大胆猜测,这个单例负责控制Observer的事件监听与状态更新。
接下来是constructor

/**
 * Creates a new instance of ResizeObserverController.
 *
 * @private
 */
constructor() {
    this.onTransitionEnd_ = this.onTransitionEnd_.bind(this);
    this.refresh = throttle(this.refresh.bind(this), REFRESH_DELAY);
}

可以看到,在constructor内,this.onTransitionEnd_的this被指向了constructor内的this,而this.refresh 被throttle函数包裹。成为了一个节流函数。以下是throttle的实现。

import requestAnimationFrame from '../shims/requestAnimationFrame.js';

// Defines minimum timeout before adding a trailing call.
const trailingTimeout = 2;

/**
 * Creates a wrapper function which ensures that provided callback will be
 * invoked only once during the specified delay period.
 *
 * @param {Function} callback - Function to be invoked after the delay period.
 * @param {number} delay - Delay after which to invoke callback.
 * @returns {Function}
 */
export default function (callback, delay) {
    let leadingCall = false,
        trailingCall = false,
        lastCallTime = 0;

    /**
     * Invokes the original callback function and schedules new invocation if
     * the "proxy" was called during current request.
     *
     * @returns {void}
     */
    function resolvePending() {
        if (leadingCall) {
            leadingCall = false;

            callback();
        }

        if (trailingCall) {
            proxy();
        }
    }

    /**
     * Callback invoked after the specified delay. It will further postpone
     * invocation of the original function delegating it to the
     * requestAnimationFrame.
     *
     * @returns {void}
     */
    function timeoutCallback() {
        requestAnimationFrame(resolvePending);
    }

    /**
     * Schedules invocation of the original function.
     *
     * @returns {void}
     */
    function proxy() {
        const timeStamp = Date.now();

        if (leadingCall) {
            // Reject immediately following calls.
            if (timeStamp - lastCallTime < trailingTimeout) {
                return;
            }

            // Schedule new call to be in invoked when the pending one is resolved.
            // This is important for "transitions" which never actually start
            // immediately so there is a chance that we might miss one if change
            // happens amids the pending invocation.
            trailingCall = true;
        } else {
            leadingCall = true;
            trailingCall = false;

            setTimeout(timeoutCallback, delay);
        }

        lastCallTime = timeStamp;
    }

    return proxy;
}

可以看出这个throttle函数接受两个参数,分别是回调函数和延迟时间,最后返回proxy函数。所以我们可以从proxy函数开始分析,首先,proxy函数中声明了timeStamp私有变量用于保存当前的时间戳,随后对leadingCall进行判断,调用这个函数时,leadingCall的值为false所以会进入else分支,并在该分支中设置leadingCall为true,以及一个setTimeout延时,在setTimeout回调函数中会调用requestAnimationFrame并传入resolePending作为回调函数。

function resolvePending() {
    if (leadingCall) {
        leadingCall = false;

        callback();
    }

    if (trailingCall) {
        proxy();
    }
}

在resolvePending函数中我们看到它通过判断trailingCall决定是否调用proxy函数。在proxy函数的leadingCall判断的第一个分支中,可以看到作者对trailingCall的解释

// Schedule new call to be in invoked when the pending one is resolved.
// This is important for "transitions" which never actually start
// immediately so there is a chance that we might miss one if change
// happens amids the pending invocation.
trailingCall = true;

本人愚钝,没看懂为什么要这样设计。只是根据代码原理可知,throttle函数将proxy函数返回到了ResizeObserverController中,当Controller中的refresh函数被调用时,proxy会被执行,如果proxy被频繁执行(间隔时间小于20毫秒)就会进入第一个分支,trailingCall会被设置为true,则回调函数执行完成会立即再次执行proxy函数。如果用户在在对浏览器窗口大小进行缩放的时候,最后一次resize事件被节流函数拒绝,则最后一次回调函数不会被执行,数据或者界面可能会出现显示异常。设计这个trailingCall的目的可能是为了防止最后一次更新由于节流函数而被漏掉。
我们已经了解了这个节流函数的原理。回到ResizeObserverController。

/**
 * Returns instance of the ResizeObserverController.
 *
 * @returns {ResizeObserverController}
 */
static getInstance() {
    if (!this.instance_) {
        this.instance_ = new ResizeObserverController();
    }

    return this.instance_;
}

作者是这样使用ResizeObserverController实现单例的:

// ResizeObserver.js
const controller = ResizeObserverController.getInstance();
// ResizeObserverController.js
/**
* Returns instance of the ResizeObserverController.
 *
 * @returns {ResizeObserverController}
 */
static getInstance() {
    if (!this.instance_) {
        this.instance_ = new ResizeObserverController();
    }

    return this.instance_;
}

作者通过getInstance函数实现了单例。
至此,对于ResizeObserverController的源码分析暂时无法推进了,因为getInstance函数只是单纯的返回ResizeObserverController实例,并没有调用任何内部方法。在ResizeObserver中,回调函数和控制器单例的引用被作为建立ResizeObserverSPI的参数。所以,下一章,将会分析ResizeObserverSPI类内部的实现。

 类似资料: