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

本文详细探讨了ResizeObserverSPI的broadcastActive方法和ResizeObservation类的工作原理。ResizeObservation在构造时记录目标元素,通过isActive检查尺寸变化,并使用getContentRect获取元素几何信息。ResizeObserverEntry在广播活动时创建,包含只读属性,最终作为事件参数传递给回调函数。文章完整梳理了ResizeObserver从创建到执行回调的全过程。
摘要由CSDN通过智能技术生成

在上一章的最后,我们分析到了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();
}

对activeObservations_应用map函数,利用处于活动状态的ResizeObservation实例创建ResizeObserverEntry。我们得先搞清楚ResizeObservation类是如何工作的。

/**
 * Creates an instance of ResizeObservation.
 *
 * @param {Element} target - Element to be observed.
 */
constructor(target) {
    this.target = target;
}

首先,这个类在构建时,内部的target被赋值为外部传入的HTML元素。

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

当SPI的gatherActive方法被调用,每一个被遍历的ResizeObservation都会调用isActive方法返回元素的宽度、高度是否发生了变化。

/**
 * Updates content rectangle and tells whether it's width or height properties
 * have changed since the last broadcast.
 *
 * @returns {boolean}
 */
isActive() {
    const rect = getContentRect(this.target);

    this.contentRect_ = rect;

    return (
        rect.width !== this.broadcastWidth ||
        rect.height !== this.broadcastHeight
    );
}

可以看出,该方法调用了geometry.js中的getContentRect方法返回当前元素的x、y、宽度、高度。如果相较上一次发生了变化。这个方法就会返回true,在SPI类中,这个ResizeObservation实例也就会被记录为activeObservations_并被更新。

return new ResizeObserverEntry(
    observation.target,
    observation.broadcastRect()
);

当控制器对某个ResizeObserverSPI调用broadcastActive方法时,其内部会创建一个ResizeObserverEntry数组。创建ResizeObserverEntry时会使用ResizeObservation实例的broadcastRect方法。

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

由于isActive函数的执行时间是在控制器创建的节流函数以内,所以broadcastRect可以复用刚刚算好的contentRect_。而contentRect_在 isActive方法中是通过调用geometry.js中的getContentRect方法算出的。

/**
 * Calculates an appropriate content rectangle for provided html or svg element.
 *
 * @param {Element} target - Element content rectangle of which needs to be calculated.
 * @returns {DOMRectInit}
 */
export function getContentRect(target) {
    if (!isBrowser) {
        return emptyRect;
    }

    if (isSVGGraphicsElement(target)) {
        return getSVGContentRect(target);
    }

    return getHTMLElementContentRect(target);
}

在这个方法中,对于一般的HTML元素,它通过getHTMLElementContentRect方法返回其Rect。

/**
 * Calculates content rectangle of provided HTMLElement.
 *
 * @param {HTMLElement} target - Element for which to calculate the content rectangle.
 * @returns {DOMRectInit}
 */
function getHTMLElementContentRect(target) {
    // Client width & height properties can't be
    // used exclusively as they provide rounded values.
    const {clientWidth, clientHeight} = target;

    // By this condition we can catch all non-replaced inline, hidden and
    // detached elements. Though elements with width & height properties less
    // than 0.5 will be discarded as well.
    //
    // Without it we would need to implement separate methods for each of
    // those cases and it's not possible to perform a precise and performance
    // effective test for hidden elements. E.g. even jQuery's ':visible' filter
    // gives wrong results for elements with width & height less than 0.5.
    if (!clientWidth && !clientHeight) {
        return emptyRect;
    }

    const styles = getWindowOf(target).getComputedStyle(target);
    const paddings = getPaddings(styles);
    const horizPad = paddings.left + paddings.right;
    const vertPad = paddings.top + paddings.bottom;

    // Computed styles of width & height are being used because they are the
    // only dimensions available to JS that contain non-rounded values. It could
    // be possible to utilize the getBoundingClientRect if only it's data wasn't
    // affected by CSS transformations let alone paddings, borders and scroll bars.
    let width = toFloat(styles.width),
        height = toFloat(styles.height);

    // Width & height include paddings and borders when the 'border-box' box
    // model is applied (except for IE).
    if (styles.boxSizing === 'border-box') {
        // Following conditions are required to handle Internet Explorer which
        // doesn't include paddings and borders to computed CSS dimensions.
        //
        // We can say that if CSS dimensions + paddings are equal to the "client"
        // properties then it's either IE, and thus we don't need to subtract
        // anything, or an element merely doesn't have paddings/borders styles.
        if (Math.round(width + horizPad) !== clientWidth) {
            width -= getBordersSize(styles, 'left', 'right') + horizPad;
        }

        if (Math.round(height + vertPad) !== clientHeight) {
            height -= getBordersSize(styles, 'top', 'bottom') + vertPad;
        }
    }

    // Following steps can't be applied to the document's root element as its
    // client[Width/Height] properties represent viewport area of the window.
    // Besides, it's as well not necessary as the <html> itself neither has
    // rendered scroll bars nor it can be clipped.
    if (!isDocumentElement(target)) {
        // In some browsers (only in Firefox, actually) CSS width & height
        // include scroll bars size which can be removed at this step as scroll
        // bars are the only difference between rounded dimensions + paddings
        // and "client" properties, though that is not always true in Chrome.
        const vertScrollbar = Math.round(width + horizPad) - clientWidth;
        const horizScrollbar = Math.round(height + vertPad) - clientHeight;

        // Chrome has a rather weird rounding of "client" properties.
        // E.g. for an element with content width of 314.2px it sometimes gives
        // the client width of 315px and for the width of 314.7px it may give
        // 314px. And it doesn't happen all the time. So just ignore this delta
        // as a non-relevant.
        if (Math.abs(vertScrollbar) !== 1) {
            width -= vertScrollbar;
        }

        if (Math.abs(horizScrollbar) !== 1) {
            height -= horizScrollbar;
        }
    }

    return createRectInit(paddings.left, paddings.top, width, height);
}

**这就是最底层的实现。**通过该方法最终会算出一个准确的Rect,包含了x左边距,y上边距,宽度,高度。

return createRectInit(paddings.left, paddings.top, width, height);

//...

/**
 * Creates DOMRectInit object based on the provided dimensions and the x/y coordinates.
 * Spec: https://drafts.fxtf.org/geometry/#dictdef-domrectinit
 *
 * @param {number} x - X coordinate.
 * @param {number} y - Y coordinate.
 * @param {number} width - Rectangle's width.
 * @param {number} height - Rectangle's height.
 * @returns {DOMRectInit}
 */
export function createRectInit(x, y, width, height) {
    return {x, y, width, height};
}

当SPI在执行broadcastActive创建entries时,用到了ResizeObserverEntry这个类。

/**
 * 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();
}
import {createReadOnlyRect} from './utils/geometry.js';
import defineConfigurable from './utils/defineConfigurable.js';

export default class ResizeObserverEntry {
    /**
     * Element size of which has changed.
     * Spec: https://wicg.github.io/ResizeObserver/#dom-resizeobserverentry-target
     *
     * @readonly
     * @type {Element}
     */
    target;

    /**
     * Element's content rectangle.
     * Spec: https://wicg.github.io/ResizeObserver/#dom-resizeobserverentry-contentrect
     *
     * @readonly
     * @type {DOMRectReadOnly}
     */
    contentRect;

    /**
     * Creates an instance of ResizeObserverEntry.
     *
     * @param {Element} target - Element that is being observed.
     * @param {DOMRectInit} rectInit - Data of the element's content rectangle.
     */
    constructor(target, rectInit) {
        const contentRect = createReadOnlyRect(rectInit);

        // According to the specification following properties are not writable
        // and are also not enumerable in the native implementation.
        //
        // Property accessors are not being used as they'd require to define a
        // private WeakMap storage which may cause memory leaks in browsers that
        // don't support this type of collections.
        defineConfigurable(this, {target, contentRect});
    }
}

在这个方法中,只有一个孤零零的构造器,没有其他东西了。而在这个构造器中作者用createReadOnlyRect方法和defineConfigurable方法创建了一个只读的属性。

export function createReadOnlyRect({x, y, width, height}) {
    // If DOMRectReadOnly is available use it as a prototype for the rectangle.
    const Constr = typeof DOMRectReadOnly !== 'undefined' ? DOMRectReadOnly : Object;
    const rect = Object.create(Constr.prototype);

    // Rectangle's properties are not writable and non-enumerable.
    defineConfigurable(rect, {
        x, y, width, height,
        top: y,
        right: x + width,
        bottom: height + y,
        left: x
    });

    return rect;
}

这两者都使用到了defineConfigurable

/**
 * Defines non-writable/enumerable properties of the provided target object.
 *
 * @param {Object} target - Object for which to define properties.
 * @param {Object} props - Properties to be defined.
 * @returns {Object} Target object.
 */
export default (target, props) => {
    for (const key of Object.keys(props)) {
        Object.defineProperty(target, key, {
            value: props[key],
            enumerable: false,
            writable: false,
            configurable: true
        });
    }

    return target;
};

利用原声的defineProperty方法创建一个不可修改的事件参数。整个ResizeObserverEntry实例就是一个不可修改的事件参数。在ResizeObserverSPI的broadcastActive方法中,最后entries(ResizeObserverEntry数组)会被整个作为事件参数传入我们写的回调函数。用到了Call方法

const entries = this.activeObservations_.map(observation => {
    return new ResizeObserverEntry(
        observation.target,
        observation.broadcastRect()
    );
});

this.callback_.call(ctx, entries, ctx);
const ro = new ResizeObserver((entries, observer) => {
    for (const entry of entries) {
        const {left, top, width, height} = entry.contentRect;
        console.log(entry.target);
    }
});

这里的entries参数实际上就是这个东西

const entries = this.activeObservations_.map(observation => {
    return new ResizeObserverEntry(
        observation.target,
        observation.broadcastRect()
    );
});

而observer,则指向ResizeObserver实例

class ResizeObserver {
    /**
     * Creates a new instance of ResizeObserver.
     *
     * @param {ResizeObserverCallback} callback - Callback that is invoked when
     *      dimensions of the observed elements change.
     */
    constructor(callback) {
        if (!(this instanceof ResizeObserver)) {
            throw new TypeError('Cannot call a class as a function.');
        }
        if (!arguments.length) {
            throw new TypeError('1 argument required, but only 0 present.');
        }

        const controller = ResizeObserverController.getInstance();
        const observer = new ResizeObserverSPI(callback, controller, this);
// ...

至此,我们完整梳理了一遍从ResizeObserver创建到ResizeObserver执行回调函数的全过程。ResizeObserver中的unobserve, disconnect两个方法不再赘述了。
至此,这个持续了四篇的源码解析正式完结。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值