书接上文,我们已经看完了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_DELAY、connected、大胆猜测,这个单例负责控制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类内部的实现。