道招

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 );
},

为了降低学习成本,一般onceon的入参都是一样的,所以我们只需要将原来的第二个参数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 );

这里面的stopEventcancelEvent

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对事件处理逻辑有点小启发,有时间再写一下。

更新时间:
上一篇:《浏览器工作原理与实践》笔记之HTTP2下一篇:CKEditor系列(三)粘贴操作是怎么完成的

相关文章

CKEditor系列(三)粘贴操作是怎么完成的

在上一篇文章 CKEditor系列(二)事件系统是怎么实现的 中,我们了解了CKEditor中事件系统的运行流程,我们先简单回顾下: 用户注册回调函数时可以指定优先级,值越小的优先级越高,默 阅读更多…

CKEditor系列(一)CKEditor4项目怎么跑起来的

我们先看CKEditor的入口ckeditor.js,它里面有一部分是压缩版,压缩版部分对应的源码地址为src/core/ckeditor_base.js // src/core/ckedit 阅读更多…

用个数组来理解vue的diff算法(一)

原文地址: 道招网 的 用个数组来理解vue的diff算法(一) Vue使用的diff算法,我相信用vue的估计都听过,并且看到源码的也不在少数。 先对下面的代码做下说明: 由于这里用 阅读更多…

Vue2.6.10源码分析(一)vue项目怎么神奇的跑起来的

先看index.html的代码吧 &lt;!DOCTYPE html&gt; &lt;html lang=&quot;en&quot;&gt; &lt;head&gt; &lt;meta 阅读更多…

关注道招网公众帐号
道招开发者二群