这是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除了用于向外暴露方法以外也没有其他用处了。