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前端
相关文章
CKEditor系列(四)支持动态多语言i18n
多语言文件结构 先看下CKEditor4的多语言文件长什么样子 //src/lang/zh-cn.js CKEDITOR.lang[ 'zh-cn' ] = { 阅读更多…
富文本编辑器wangEditor迁移CKEditor前后效果对比
一、背景 富文本编辑器wangEditor的工具栏如图所示 富文本编辑器CKEditor4工具栏如图所示 二、wangEditor编辑器存在问题 1. 字号和字体设置 阅读更多…
CKEditor系列(七)编辑器工具栏根据宽度自动折叠
刚才看了看上一篇写CKEditor的文章是在今年的一月份,现在轮到我们的设计师对编辑器下手了。我们回顾下现在的编辑器长什么样子。 需求 我们客户端默认窗口尺寸下,会出现排,并且第二排 阅读更多…
CKEditor富文本编辑器职责分离
背景 CKEditor富文本 编辑器 (生产版本1.1.1及以前)里面包含富文本基础插件及当前最新的邮件特定的业务插件(签名、快捷回复、邀评、默认样式、选择颜色、插入图片、粘贴模式) O端 阅读更多…
CKEditor系列(三)粘贴操作是怎么完成的
在上一篇文章 CKEditor系列(二)事件系统是怎么实现的 中,我们了解了CKEditor中事件系统的运行流程,我们先简单回顾下: 用户注册回调函数时可以指定优先级,值越小的优先级越高,默 阅读更多…
记录CKEditor4删除文本引起文本分割而升级版本的经历
背景 前段时间对接了一个electron壳提供功能————拼写检查,也就是在输入的英文有问题的时候,给予红色波浪线提示,邮件的时候能出现候选词,选择候选词后进行替换。 在功能上线当天上午产品 阅读更多…