CKEditor系列(二)事件系统是怎么实现的
CKEditor系列(二)事件系统是怎么实现的
CKEditor的事件系统的源代码在core/event.js里面 我们看看整个事件系统的实现过程
事件监听on
CKEDITOR.event.prototype = ( function() {
// Returns the private events object for a given object.
var getPrivate = function( obj ) {
var _ = ( obj.getPrivate && obj.getPrivate() ) || obj._ || ( obj._ = {} );
return _.events || ( _.events = {} );
};
var eventEntry = function( eventName ) {
this.name = eventName;
this.listeners = [];
};
eventEntry.prototype = {
// Get the listener index for a specified function.
// Returns -1 if not found.
getListenerIndex: function( listenerFunction ) {
for ( var i = 0, listeners = this.listeners; i < listeners.length; i++ ) {
if ( listeners[ i ].fn == listenerFunction )
return i;
}
return -1;
}
};
// Retrieve the event entry on the event host (create it if needed).
function getEntry( name ) {
// Get the event entry (create it if needed).
var events = getPrivate( this );
return events[ name ] || ( events[ name ] = new eventEntry( name ) );
}
return {
/**
* @param {String} eventName The event name to which listen.
* @param {Function} listenerFunction The function listening to the
* event. A single {@link CKEDITOR.eventInfo} object instanced
* is passed to this function containing all the event data.
* @param {Object} [scopeObj] The object used to scope the listener
* call (the `this` object). If omitted, the current object is used.
* @param {Object} [listenerData] Data to be sent as the
* {@link CKEDITOR.eventInfo#listenerData} when calling the
* listener.
* @param {Number} [priority=10] The listener priority. Lower priority
* listeners are called first. Listeners with the same priority
* value are called in registration order.
* @returns {Object} An object containing the `removeListener`
* function, which can be used to remove the listener at any time.
*/
on: function( eventName, listenerFunction, scopeObj, listenerData, priority ) {
var me = this;
// Create the function to be fired for this listener.
function listenerFirer( editor, publisherData, stopFn, cancelFn ) {
var ev = {
name: eventName,
sender: this,
editor: editor,
data: publisherData,
listenerData: listenerData,
stop: stopFn,
cancel: cancelFn,
removeListener: removeListener
};
var ret = listenerFunction.call( scopeObj, ev );
return ret === false ? EVENT_CANCELED : ev.data;
}
function removeListener() {
me.removeListener( eventName, listenerFunction );
}
var event = getEntry.call( this, eventName );
if ( event.getListenerIndex( listenerFunction ) < 0 ) {
// Get the listeners.
var listeners = event.listeners;
// Fill the scope.
if ( !scopeObj )
scopeObj = this;
// Default the priority, if needed.
if ( isNaN( priority ) )
priority = 10;
listenerFirer.fn = listenerFunction;
listenerFirer.priority = priority;
// Search for the right position for this new listener, based on its
// priority.
for ( var i = listeners.length - 1; i >= 0; i-- ) {
// Find the item which should be before the new one.
if ( listeners[ i ].priority <= priority ) {
// Insert the listener in the array.
listeners.splice( i + 1, 0, listenerFirer );
return { removeListener: removeListener };
}
}
// If no position has been found (or zero length), put it in
// the front of list.
listeners.unshift( listenerFirer );
}
return { removeListener: removeListener };
},
}
})()
我们平时监听事件一般这样
editor.on('test', function test(evt) {
// xxx
evt.stop();
})
根据上面on
的实现,我们可以看到会进行一下步骤:
- 定义新的
listenerFirer
,而不是直接使用我们传递进来的listenerFunction
,只有这样我们才方便设计一套功能更加强大的事件系统 - 首先会寻找此事件名的事件信息
var event = getEntry.call( this, eventName );
并看看当前注册的事件回调是否已经存在event.getListenerIndex( listenerFunction ) < 0
,如果不存在的话才会注册 。 - 设置默认的上下文
scopeObj
和优先级priority
。
// Fill the scope.
if ( !scopeObj )
scopeObj = this;
// Default the priority, if needed.
if ( isNaN( priority ) )
priority = 10;
- 将我们传递进来的
listenerFunction
作为listenerFirer
的属性listenerFirer.fn = listenerFunction;
- 将注册的回调按照优先级要求调整对应其在
listeners
数组的的顺序,从后往前遍历,如果遇到数组里面有priority
比自己小的,就将新注册的回调插在它的后面;如果没有的话,直接将它插在数组的最前面。这样保证了listeners
是按照priority
从小到大的顺序了。
// Search for the right position for this new listener, based on its
// priority.
for ( var i = listeners.length - 1; i >= 0; i-- ) {
// Find the item which should be before the new one.
if ( listeners[ i ].priority <= priority ) {
// Insert the listener in the array.
listeners.splice( i + 1, 0, listenerFirer );
return { removeListener: removeListener };
}
}
listeners.unshift( listenerFirer );
事件移除 removeListener
在上面on
方法中我们看到在结尾会始终返回{ removeListener: removeListener };
,移除的方法一并也返回了
removeListener: function( eventName, listenerFunction ) {
// Get the event entry.
var event = getPrivate( this )[ eventName ];
if ( event ) {
var index = event.getListenerIndex( listenerFunction );
if ( index >= 0 )
event.listeners.splice( index, 1 );
}
},
同样是找到event
,将其在event.listeners
中移除即可。
只监听一次事件once
once: function() {
var args = Array.prototype.slice.call( arguments ),
fn = args[ 1 ];
args[ 1 ] = function( evt ) {
evt.removeListener();
return fn.apply( this, arguments );
};
return this.on.apply( this, args );
},
为了降低学习成本,一般once
和on
的入参都是一样的,所以我们只需要将原来的第二个参数listenerFunction
改造下即可了——在执行回调前移除掉该监听事件。
事件捕获capture
capture: function() {
CKEDITOR.event.useCapture = 1;
var retval = this.on.apply( this, arguments );
CKEDITOR.event.useCapture = 0;
return retval;
},
也是用跟on
一样的入参,只是在on
之前将useCapture
置为1,监听后重置回0即可。
捕获场景用的较少
事件触发fire
fire: ( function() {
// Create the function that marks the event as stopped.
var stopped = 0;
var stopEvent = function() {
stopped = 1;
};
// Create the function that marks the event as canceled.
var canceled = 0;
var cancelEvent = function() {
canceled = 1;
};
return function( eventName, data, editor ) {
// Get the event entry.
var event = getPrivate( this )[ eventName ];
// Save the previous stopped and cancelled states. We may
// be nesting fire() calls.
var previousStopped = stopped,
previousCancelled = canceled;
// Reset the stopped and canceled flags.
stopped = canceled = 0;
if ( event ) {
var listeners = event.listeners;
if ( listeners.length ) {
// As some listeners may remove themselves from the
// event, the original array length is dinamic. So,
// let's make a copy of all listeners, so we are
// sure we'll call all of them.
listeners = listeners.slice( 0 );
var retData;
// Loop through all listeners.
for ( var i = 0; i < listeners.length; i++ ) {
// Call the listener, passing the event data.
if ( event.errorProof ) {
try {
retData = listeners[ i ].call( this, editor, data, stopEvent, cancelEvent );
} catch ( er ) {}
} else {
retData = listeners[ i ].call( this, editor, data, stopEvent, cancelEvent );
}
if ( retData === EVENT_CANCELED )
canceled = 1;
else if ( typeof retData != 'undefined' )
data = retData;
// No further calls is stopped or canceled.
if ( stopped || canceled )
break;
}
}
}
var ret = canceled ? false : ( typeof data == 'undefined' ? true : data );
// Restore the previous stopped and canceled states.
stopped = previousStopped;
canceled = previousCancelled;
return ret;
};
} )()
我们可以通过返回值知道该事件是否被取消了,还是说被某个监听回调处理了。
上面有几个地方需要注意下:
避免回调漏执行
listeners = listeners.slice( 0 );
注释里面也说的很清楚:因为有的事件可能会将自己从listeners
中移除,我们为了确保能遍历到所有的listeners
的,特进行一次slice
操作。
事件取消
retData = listeners[ i ].call( this, editor, data, stopEvent, cancelEvent );
这里面的stopEvent
和cancelEvent
。
var stopped = 0;
var stopEvent = function() {
stopped = 1;
};
var canceled = 0;
var cancelEvent = function() {
canceled = 1;
};
都是用来更新闭包里面标志位的值。
前面on
的时候作为evt
对象的方法。
function listenerFirer( editor, publisherData, stopFn, cancelFn ) {
var ev = {
name: eventName,
sender: this,
editor: editor,
data: publisherData,
listenerData: listenerData,
stop: stopFn,
cancel: cancelFn,
removeListener: removeListener
};
var ret = listenerFunction.call( scopeObj, ev );
return ret === false ? EVENT_CANCELED : ev.data;
}
我们执行evt.stop()
或者evt.cancel()
会改变标志位。
在fire
的时候循环时会据此进行break
。
for ( var i = 0; i < listeners.length; i++ ) {
...
// No further calls is stopped or canceled.
if ( stopped || canceled )
break;
}
只触发一次事件fireOnce
fireOnce: function( eventName, data, editor ) {
var ret = this.fire( eventName, data, editor );
delete getPrivate( this )[ eventName ];
return ret;
},
先执行fire
返回将该event直接删掉,这样对应的listenrs也都没了,event被删了,下次再fire
时,var event = getPrivate( this )[ eventName ];
就找不到了,也就没回调执行了。
总结
通过对CKEditor源码的学习,我们知道了怎么扩展我们自己的事件系统,我们平时都是简单的事件系统,on的时候就往数组里面的push
,fire的时候就执行循环遍历
。在大多数场景下也是够用的,但是当我们需要一些额外的类似事件之前的优先级的要求时,就不够用了。
最近在编写CKEditor的粘贴相关的业务插件时CKEditor对事件处理逻辑有点小启发,有时间再写一下。
- 分类:
- Web前端
相关文章
Vue2.6.10源码分析(一)vue项目怎么神奇的跑起来的
先看index.html的代码吧 <!DOCTYPE html> <html lang="en"> <head> <meta 阅读更多…
CKEditor富文本编辑器职责分离
背景 CKEditor富文本 编辑器 (生产版本1.1.1及以前)里面包含富文本基础插件及当前最新的邮件特定的业务插件(签名、快捷回复、邀评、默认样式、选择颜色、插入图片、粘贴模式) O端 阅读更多…
用个数组来理解vue的diff算法(一)
原文地址: 道招网 的 用个数组来理解vue的diff算法(一) Vue使用的diff算法,我相信用vue的估计都听过,并且看到源码的也不在少数。 先对下面的代码做下说明: 由于这里用 阅读更多…
CKEditor系列(六)改造原编辑器默认样式dom结构效果对比
熟悉的朋友应该知道之前是用的wangEditor,近期才迁移到CKEditor,很早的时候项目就支持一个叫“默认样式”的功能,需求就是部分BU希望能够统一邮件对外发送的样式,比如统一使用“宋体,黑色 阅读更多…
富文本编辑器CKEditor4迁移方案
之前写过 《富文本编辑器wangEditor迁移CKEditor前后效果对比》 ,结合大家的反馈后进行了调整。 增加了具体案例的展示CKEditor插件和事件系统,重新整理成迁移方案。 一、背景 阅读更多…
富文本编辑器wangEditor迁移CKEditor前后效果对比
一、背景 富文本编辑器wangEditor的工具栏如图所示 富文本编辑器CKEditor4工具栏如图所示 二、wangEditor编辑器存在问题 1. 字号和字体设置 阅读更多…