Contribute to this guide

guide事件系统

发射器 是可以触发事件的对象。它们还提供了一种监听其他发射器事件的方法。

发射器在整个编辑器架构中被广泛使用。它们是 可观察对象引擎的视图观察器转换 等机制的基础。

任何类都可以成为事件发射器。您只需要将 Emitter 混入其中即可

import { EmitterMixin, mix } from 'ckeditor5';

class AnyClass {
    // Class's code.
    // ...
}

mix( AnyClass, EmitterMixin );

# 监听事件

向事件添加回调很简单。您可以直接在发射器对象上监听并使用匿名函数

emitter.on( 'eventName', ( eventInfo, ...args ) => { /* ... */ } );

但是,如果您想能够删除事件监听器,则需要一个函数对象

emitter.off( 'eventName', handler );

还有另一种添加事件监听器的方法——使用 listenTo()。这样,一个发射器就可以监听另一个发射器上的事件

foo.listenTo( bar, 'eventName', ( eventInfo, ...args ) => { /* ... */ } );

现在,您可以通过 stopListening() 轻松地将 foobar 中分离。

// Stop listening to a specific handler.
foo.stopListening( bar, 'eventName', handler );

// Stop listening to a specific event.
foo.stopListening( bar, 'eventName' );

// Stop listening to all events fired by a specific emitter.
foo.stopListening( bar );

// Stop listening to all events fired by all bound emitters.
foo.stopListening();

on()off() 方法是 listenTo( this, /* ... */ )stopListening( this, /* ... */ ) 的简写(发射器绑定到自身)。

# 监听器优先级

默认情况下,所有监听器都绑定在 normal 优先级上,但您可以在注册监听器时指定优先级

this.on( 'eventName', () => { /* ... */ }, { priority: 'high' } );
this.listenTo( emitter, 'eventName', () => { /* ... */ }, { priority: 'high' } );

有 5 个命名优先级

  • 最高
  • 正常
  • 最低

监听器按这些优先级的顺序触发(首先是 highest,然后是 high,等等)。对于在同一优先级上附加的多个监听器,它们按注册顺序触发。

注意:如果任何监听器 停止 事件,则不会调用任何其他监听器,包括那些在较低优先级上的监听器。

可以使用相对优先级 priorities.get( 'high' ) + 10,但这强烈建议不要这样做。

# 停止事件和返回值

传递给事件处理程序的第一个参数始终是 EventInfo 的实例。在那里,您可以检查事件的 name、事件的 source 发射器,并且您可以 stop() 事件以阻止进一步处理。

emitter.on( 'eventName', ( eventInfo, data ) => {
    console.log( 'foo' );
    eventInfo.stop();
} );

emitter.on( 'eventName', ( eventInfo, data ) => {
    console.log( 'bar' ); // This won't be called.
} );

emitter.fire( 'eventName' ); // Logs "foo" only.

监听器可以设置 return 值。在所有回调处理完后,此值将由 fire() 返回。

emitter.on( 'eventName', ( eventInfo, data ) => {
    eventInfo.return = 123;
} );

emitter.fire( 'eventName' ); // -> 123

# 监听命名空间事件

事件系统支持命名空间事件,以便您可以构建回调结构。您可以在事件名称中使用 : 来实现命名空间

this.fire( 'foo:bar:baz', data );

然后,监听器可以绑定到特定事件或整个命名空间

this.on( 'foo', () => { /* ... */ } );
this.on( 'foo:bar', () => { /* ... */ } );
this.on( 'foo:bar:baz', () => { /* ... */ } );

这样,您可以拥有更通用的事件,监听更广泛的事件(在本例中为 'foo'),或更详细的回调,监听指定的事件('foo:bar''foo:bar:baz')。

这种机制例如在转换中使用,在那里,由于名为 'insert:<elementName>' 的事件,您可以监听特定元素的插入(如 'insert:p')或所有元素的插入('insert')。

注意:在同一优先级上注册的监听器将按注册顺序触发(无论监听整个命名空间还是特定事件)。

# 触发事件

Emitter 混入您的类后,您可以通过以下方式触发事件

this.fire( 'eventName', argA, argB, /* ... */ );

所有传递的参数都将在添加到事件的所有监听器中可用。

注意:大多数基类(如 CommandPlugin)已经是 发射器 并触发它们自己的事件。

# 已停止事件

有时知道事件是否被任何监听器停止很有用。有一种替代方法可以仅为此目的触发事件

import { EventInfo } from 'ckeditor5';

// Prepare the event info...
const eventInfo = new EventInfo( this, 'eventName' );

// ...and fire the event.
this.fire( eventInfo, argA, argB, /* ... */ );

// Here you can check if the event was stopped.
if ( eventInfo.stop.called ) {
    // The event was stopped.
}

请注意,EventInfo 在第一个参数中期望源对象作为事件的来源。

# 事件返回值

如果任何处理程序设置了 eventInfo.return 字段,则此值将在所有回调处理完后由 fire() 返回。

emitter.on( 'eventName', ( eventInfo, ...args ) => {
    eventInfo.return = 123;
} );

const result = emitter.fire( 'eventName', argA, argB, /* ... */ );

console.log( result ); // -> 123

# 事件委托

Emitter 接口还提供 事件委托 机制,以便选择事件由另一个 Emitter 触发。

# 设置事件委托

将特定事件委托给另一个发射器

emitterA.delegate( 'foo' ).to( emitterB );
emitterA.delegate( 'foo', 'bar' ).to( emitterC );

您可以使用不同的名称委托事件

emitterA.delegate( 'foo' ).to( emitterB, 'bar' );
emitterA.delegate( 'foo' ).to( emitterB, name => `delegated:${ name }` );

也可以委托所有事件

emitterA.delegate( '*' ).to( emitterB );

注意:无论源发射器上的任何处理程序是否已停止委托事件,委托事件都将从目标发射器触发。

# 停止委托

您可以通过调用 stopDelegating() 方法来停止委托。它可以在不同的级别使用

// Stop delegating all events.
emitterA.stopDelegating();

// Stop delegating a specific event to all emitters.
emitterA.stopDelegating( 'foo' );

// Stop delegating a specific event to a specific emitter.
emitterA.stopDelegating( 'foo', emitterB );

# 委托事件信息

委托事件提供 path,其中包含该事件在委托路径上遇到的发射器。

emitterA.delegate( 'foo' ).to( emitterB, 'bar' );
emitterB.delegate( 'bar' ).to( emitterC, 'baz' );

emitterA.on( 'foo', eventInfo => console.log( 'event', eventInfo.name, 'emitted by A; source:', eventInfo.source, 'path:', eventInfo.path ) );
emitterB.on( 'bar', eventInfo => console.log( 'event', eventInfo.name, 'emitted by B; source:', eventInfo.source, 'path:', eventInfo.path ) );
emitterC.on( 'baz', eventInfo => console.log( 'event', eventInfo.name, 'emitted by C; source:', eventInfo.source, 'path:', eventInfo.path ) );

emitterA.fire( 'foo' );

// Outputs:
//   event "foo" emitted by A; source: emitterA; path: [ emitterA ]
//   event "bar" emitted by B; source: emitterA; path: [ emitterA, emitterB ]
//   event "baz" emitted by C; source: emitterA; path: [ emitterA, emitterB, emitterC ]

# 视图事件冒泡

view.Document 不仅仅是一个 Observable 和一个 发射器,它还实现了特殊的 BubblingEmitter 接口(由 BubblingEmitterMixin 实现)。它为在虚拟 DOM 树上冒泡事件提供了一种机制。

这与您从 DOM 树事件冒泡中了解到的冒泡不同。您不会在视图文档树中特定元素实例上注册监听器。相反,您可以为特定上下文注册处理程序。上下文可以是元素的名称,或虚拟上下文之一('$capture''$text''$root''$document'),或者用于匹配所需节点的回调。

# 监听冒泡事件

在视图元素名称上下文中注册的监听器

this.listenTo( view.document, 'enter', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: 'blockquote' } );

this.listenTo( view.document, 'enter', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: 'li' } );

在虚拟上下文中注册的监听器

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: '$text', priority: 'high' } );

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: '$root' } );

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: '$capture' } );

在自定义回调函数上下文中注册的监听器

import { isWidget } from 'ckeditor5';

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: isWidget } );

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: isWidget, priority: 'high' } );

注意:如果未指定 context,则事件将绑定到 '$document' 上下文。

# 冒泡事件流

冒泡始终从虚拟 '$capture' 上下文开始。首先触发附加到此上下文的所有监听器(并按其优先级顺序)。

然后,真正的冒泡从选择位置(锚点或焦点,取决于哪个更深)开始。

如果在选择位置允许文本节点,则第一个上下文为'$text'。然后,事件会通过所有元素冒泡到'$root',最后到'$document'

在所有上下文中,可以在所需优先级下注册监听器。如果监听器阻止了一个事件,则该事件不会针对剩余的上下文触发。

# 示例

假设给定的内容和选择

<blockquote>
    <p>
        Foo[]bar
    </p>
</blockquote>

事件将针对以下上下文触发

  1. '$capture'
  2. '$text'
  3. 'p'
  4. 'blockquote'
  5. '$root'
  6. '$document'

假设给定的内容和选择(在小部件上)

<blockquote>
    <p>
        Foo
        [<img />]	// enhanced with toWidget()
        bar
    </p>
</blockquote>

事件将针对以下上下文触发

  1. '$capture'
  2. 'img'
  3. widget(假设使用了自定义匹配器)
  4. 'p'
  5. 'blockquote'
  6. '$root'
  7. '$document'

一个更复杂的示例

<blockquote>
    <figure class="table">	// enhanced with toWidget()
        <table>
            <tr>
                <td>
                    <p>
                        foo[]bar
                    </p>
                </td>
            </tr>
        </table>
    </figure>
</blockquote>

将触发的事件

  1. '$capture'
  2. '$text'
  3. 'p'
  4. 'td'
  5. 'tr'
  6. 'table'
  7. 'figure'
  8. widget(假设使用了自定义匹配器)
  9. 'blockquote'
  10. '$root'
  11. '$document'

# BubblingEventInfo

在某些事件中,第一个参数不是标准的EventInfo,而是BubblingEventInfo。这是一个扩展,它提供了当前的eventPhasecurrentTarget.

目前,以下事件可以使用此信息

因此,上述示例中的事件将扩展以下eventPhase数据

  1. '$capture' - 捕获
  2. '$text' - 在目标上
  3. 'p' - 冒泡
  4. 'td' - 冒泡
  5. 'tr' - 冒泡
  6. 'table' - 冒泡
  7. 'figure' - 冒泡
  8. widget - 冒泡
  9. 'blockquote' - 冒泡
  10. '$root' - 冒泡
  11. '$document' - 冒泡

对于选择小部件的示例

<blockquote>
    <p>
        Foo
        [<img />]	 // Enhanced with toWidget().
        bar
    </p>
</blockquote>

将触发的事件

  1. '$capture' - 捕获
  2. 'img' - 在目标上
  3. widget - 在目标上(假设使用了自定义匹配器)
  4. 'p' - 冒泡
  5. 'blockquote' - 冒泡
  6. '$root' - 冒泡
  7. '$document' - 冒泡