在上一章的最后,我们分析到了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两个方法不再赘述了。
至此,这个持续了四篇的源码解析正式完结。