CKEditor5——Uitls(DomEmitterMixin类理解)

今天开始研究在CK5的Utils包中的另一个比较重要的类,这个类就是DomEmitterMixin
,我们先看看这个类的代码:
const DomEmitterMixin = extend( {}, EmitterMixin, {
listenTo( emitter, event, callback, options = {} ){
},
stopListening(){
},
_getProxyEmitter() {
},
_getAllProxyEmitters(){
}
}
通过上面的代码,我们不难看出,这个类是继承字基础类EmitterMixin
,然后扩展并且重写了基类的两个方法,它们分别是:listenTo和stopListening
。思考一下,这在设计模式中是什么模式呢?是不是就是装饰器模式。如果猜测没错的话,这个装饰器增强的功能就是为dom节点绑定事件和删除事件。接下来,我们分析关键的方法listenTo
:
listenTo( emitter, event, callback, options = {} ) {
// Check if emitter is an instance of DOM Node. If so, use corresponding ProxyEmitter (or create one if not existing).
if ( isNode( emitter ) || isWindow( emitter ) ) {
const proxyOptions = {
capture: !!options.useCapture,
passive: !!options.usePassive
};
const proxyEmitter = this._getProxyEmitter( emitter, proxyOptions ) || new ProxyEmitter( emitter, proxyOptions );
this.listenTo( proxyEmitter, event, callback, options );
} else {
// Execute parent class method with Emitter (or ProxyEmitter) instance.
EmitterMixin.listenTo.call( this, emitter, event, callback, options );
}
}
注意到没,这里开始对emitter
进行判断,如果是Window
对象或者Node
对象,那么就采用增加的业务逻辑,否则调用基类的方法进行绑定,因此,我们之间的猜测是正确的。
这里增强的业务逻辑也比较简单,就是根据emitter
和配置对象proxyOptions
要么创建一个ProxyEmitter
,要么采用已经创建好的proxyEmitter
进行事件的绑定。
这里我们重点关注这行代码:
this.listenTo( proxyEmitter, event, callback, options );
我们知道这行代码会调用添加事件监听器的事件,因此,我们猜想ProxyEmitter
一定会重写添加事件监听器的方法,否则无法绑定dom事件,也就是说具体的装饰增强实际上是在ProxyEmitter
中实现的;所以我们需要看看ProxyEmitter
的具体实现:
class ProxyEmitter {
/**
* @param {Node} node DOM Node that fires events.
* @param {Object} [options] Additional options.
* @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
* listener before being dispatched to any EventTarget beneath it in the DOM tree.
* @param {Boolean} [options.usePassive=false] Indicates that the function specified by listener will never call preventDefault()
* and prevents blocking browser's main thread by this event handler.
*/
constructor( node, options ) {
// Set emitter ID to match DOM Node "expando" property.
_setEmitterId( this, getProxyEmitterId( node, options ) );
// Remember the DOM Node this ProxyEmitter is bound to.
this._domNode = node;
// And given options.
this._options = options;
}
}
extend( ProxyEmitter.prototype, EmitterMixin, {
/**
* Collection of native DOM listeners.
*
* @private
* @member {Object} module:utils/dom/emittermixin~ProxyEmitter#_domListeners
*/
/**
* Registers a callback function to be executed when an event is fired.
*
* It attaches a native DOM listener to the DOM Node. When fired,
* a corresponding Emitter event will also fire with DOM Event object as an argument.
*
* **Note**: This is automatically called by the
* {@link module:utils/emittermixin~EmitterMixin#listenTo `EmitterMixin#listenTo()`}.
*
* @method module:utils/dom/emittermixin~ProxyEmitter#attach
* @param {String} event The name of the event.
*/
attach( event ) {
// If the DOM Listener for given event already exist it is pointless
// to attach another one.
if ( this._domListeners && this._domListeners[ event ] ) {
return;
}
const domListener = this._createDomListener( event );
// Attach the native DOM listener to DOM Node.
this._domNode.addEventListener( event, domListener, this._options );
if ( !this._domListeners ) {
this._domListeners = {};
}
// Store the native DOM listener in this ProxyEmitter. It will be helpful
// when stopping listening to the event.
this._domListeners[ event ] = domListener;
},
/**
* Stops executing the callback on the given event.
*
* **Note**: This is automatically called by the
* {@link module:utils/emittermixin~EmitterMixin#stopListening `EmitterMixin#stopListening()`}.
*
* @method module:utils/dom/emittermixin~ProxyEmitter#detach
* @param {String} event The name of the event.
*/
detach( event ) {
let events;
// Remove native DOM listeners which are orphans. If no callbacks
// are awaiting given event, detach native DOM listener from DOM Node.
// See: {@link attach}.
if ( this._domListeners[ event ] && ( !( events = this._events[ event ] ) || !events.callbacks.length ) ) {
this._domListeners[ event ].removeListener();
}
},
/**
* Adds callback to emitter for given event.
*
* @protected
* @method module:utils/dom/emittermixin~ProxyEmitter#_addEventListener
* @param {String} event The name of the event.
* @param {Function} callback The function to be called on event.
* @param {Object} [options={}] Additional options.
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
* the priority value the sooner the callback will be fired. Events having the same priority are called in the
* order they were added.
*/
_addEventListener( event, callback, options ) {
this.attach( event );
EmitterMixin._addEventListener.call( this, event, callback, options );
},
/**
* Removes callback from emitter for given event.
*
* @protected
* @method module:utils/dom/emittermixin~ProxyEmitter#_removeEventListener
* @param {String} event The name of the event.
* @param {Function} callback The function to stop being called.
*/
_removeEventListener( event, callback ) {
EmitterMixin._removeEventListener.call( this, event, callback );
this.detach( event );
},
/**
* Creates a native DOM listener callback. When the native DOM event
* is fired it will fire corresponding event on this ProxyEmitter.
* Note: A native DOM Event is passed as an argument.
*
* @private
* @method module:utils/dom/emittermixin~ProxyEmitter#_createDomListener
* @param {String} event The name of the event.
* @returns {Function} The DOM listener callback.
*/
_createDomListener( event ) {
const domListener = domEvt => {
this.fire( event, domEvt );
};
// Supply the DOM listener callback with a function that will help
// detach it from the DOM Node, when it is no longer necessary.
// See: {@link detach}.
domListener.removeListener = () => {
this._domNode.removeEventListener( event, domListener, this._options );
delete this._domListeners[ event ];
};
return domListener;
}
} );
我们首先看构造函数,首先设置一个id,然后将dom节点和配置参数存储起来。关键的方法是看扩展的这个类的原型,我们看到了一个关键的方法就是_addEventListener
首先调用attach()
这个关键的attach就是增强的关键方法,为dom节点添加事件:
首先判断事件是否存在,如果存在就不需要添加了。
其次创建一个domListener
回调函数,然后在dom节点上绑定对应的事件,并且在回调函数上添加了一个移除监听器的方法,用于销毁创建的监听器。
将新建的回调函数绑定到this._domListeners[event]
有了以上的三个步骤,就完成了给dom节点添加事件的功能,因此增加后的节点可以认为是一个domEmitter。
同时需要注意的是,除了调用attach()方法之外,其实还调用了基类的方法来绑定事件。
以上基本分析了绑定事件的全部过程,至于如果为节点创建id啥的,这部分对主流程影响不大,有兴趣的可以自行分析,移出事件的过程也是类似的道理。这个类的增强将装饰器设计模式的使用发挥到了淋漓尽致的地步,需要仔细体会才能看到代码的精妙之处。
欢迎讨论。