Contribute to this guide

guide可观察对象

可观察对象 是具有可观察属性的对象。这意味着当此类属性的值发生变化时,可观察对象会触发事件,并且更改会反映在监听该事件的代码的其他部分中。

可观察对象是 CKEditor 5 框架 的常见构建块。它们在 UI 中特别受欢迎,View 类及其子类从可观察接口中获益最多:它是 绑定到可观察对象的模板 使用户界面变得动态且交互式。一些基本类,如 EditorCommand 也是可观察对象。

任何类都可以成为可观察对象;您需要做的就是将 Observable 混入其中

import { ObservableMixin, mix } from 'ckeditor5';

class AnyClass {
    // Any class definition.
    // ...
}

mix( AnyClass, ObservableMixin );

可观察对象在管理应用程序状态时非常有用,应用程序状态可能是动态的,而且通常是在应用程序组件之间集中且共享的。一个可观察对象还可以使用 属性绑定 将其状态(或其部分)传播到另一个可观察对象。

可观察对象也可以 装饰其方法,这使得可以使用事件监听器来控制它们的执行,从而使外部代码对它们的执行行为具有一定的控制权。

由于可观察对象只是事件 发射器 上的另一层,因此请查看 事件系统深入指南 以了解有关事件的高级用法以及一些其他示例。

# 使属性可观察

在将 Observable 混入您的类后,您可以定义可观察属性。为此,请使用 set() 方法

让我们创建一个名为 Button 的简单 UI 视图(组件),它包含几个属性,看看它们是什么样子的

class Button extends View {
    constructor() {
        super();

        // This property is not observable.
        // Not all properties must be observable, it's always up to you!
        this.type = 'button';

        const bind = this.bindTemplate;

        // this.label is observable but undefined.
        this.set( 'label' );

        // this.isOn is observable and false.
        this.set( 'isOn', false );

        // this.isEnabled is observable and true.
        this.set( 'isEnabled', true );

        // More observable's properties.
        // ...
    }
}

请注意,由于 Button 扩展了 View 类(它已经是可观察的),因此您不需要混入 ObservableMixin

set() 方法可以接受一个键值对对象来缩短代码。了解这一点后,使属性可观察就像下面这样简单

this.set( {
	label: undefined,
	isOn: false,
	isEnabled: true
} );

最后,让我们创建一个新的视图,看看它是如何与世界通信的。

每次 label 属性发生变化时,视图都会触发 change:label 事件,该事件包含有关其过去状态和新值的信息。change:isEnabledchange:isOn 事件将分别为 isEnabledisOn 的更改触发。

const view = new Button();

view.on( 'change:label', ( evt, propertyName, newValue, oldValue ) => {
    console.log(
        `#${ propertyName } has changed from "${ oldValue }" to "${ newValue }"`
    );
} )

view.label = 'Hello world!'; // -> #label has changed from "undefined" to "Hello world!"
view.label = 'Bold'; // -> #label has changed from "Hello world!" to "Bold"

view.type = 'submit'; // Changing a regular property fires no event.

视图触发的事件用于更新 DOM 并使组件变得动态。让我们给我们的视图一些模板,并将其绑定到我们创建的可观察属性。

您可以在专门的 指南 中了解有关编辑器 UI 和模板系统的更多信息。

class Button extends View {
    constructor() {
        super();

        // Previously defined properties.
        // ...

        // This template will have the following symbolic representation in DOM:
        //
        // 	<button class="[ck-disabled] ck-[on|off]" type="button">
        // 		{{ this.label }}
        // 	</button>
        //
        this.setTemplate( {
            tag: 'button',
            attributes: {
                class: [
                    // The 'ck-on' and 'ck-off' classes toggle according to the #isOn property.
                    bind.to( 'isOn', value => value ? 'ck-on' : 'ck-off' ),

                    // The 'ck-enabled' class appears when the #isEnabled property is false.
                    bind.if( 'isEnabled', 'ck-disabled', value => !value )
                ],
                type: this.type
            },
            children: [
                {
                    // The text of the button is bound to the #label property.
                    text: bind.to( 'label' )
                }
            ]
        } );
    }
}

由于 labelisOnisEnabled 是可观察的,因此任何更改都将立即反映在 DOM 中

const button = new Button();

// Render the button to create its #element.
button.render();

button.label = 'Bold';     // <button class="ck-off" type="button">Bold</button>
button.isOn = true;        // <button class="ck-on" type="button">Bold</button>
button.label = 'B';        // <button class="ck-on" type="button">B</button>
button.isOff = false;      // <button class="ck-off" type="button">B</button>
button.isEnabled = false;  // <button class="ck-off ck-disabled" type="button">B</button>

# 属性绑定

一个可观察对象也可以将自己的状态(或其一部分)传播到另一个可观察对象,以简化代码,例如,避免使用多个 change:property 事件监听器。要开始绑定对象属性,请确保这两个对象(类)都混入了 Observable,然后使用 bind() 方法来创建绑定。

# 简单绑定

使用上一节中的粗体按钮实例,将其绑定到粗体命令。这将使按钮使用某些命令属性并仅用几行代码自动执行用户界面。

粗体命令是编辑器的一个实际命令(由 BoldEditing 注册),它提供两个可观察属性:valueisEnabled。要获取命令,请使用 editor.commands.get( 'bold' )

请注意,ButtonCommand 类都是 可观察的,这就是您可以绑定它们的属性的原因。

const button = new Button();
const command = editor.commands.get( 'bold' );

任何“像样的”按钮都必须在命令变为禁用时更新其外观。一个简单的属性绑定可以执行此操作,如下所示

button.bind( 'isEnabled' ).to( command );

之后

  • button.isEnabled **立即等于** command.isEnabled
  • 每当 command.isEnabled 发生变化时,button.isEnabled 将立即反映其值,
  • 因为按钮的模板将其类绑定到 button.isEnabled,所以按钮的 DOM 元素也将被更新。

请注意,command.isEnabled **必须** 使用 set() 方法定义,以便绑定变得动态。在本例中,我们很幸运,因为 isEnabled 是编辑器中每个命令的标准可观察属性。但请记住,当您创建自己的可观察类时,使用 set() 方法是定义可观察属性的唯一方法。

# 重命名属性

现在让我们深入研究一下 bind( /* ... */ ).to( /* ... */ ) 语法。事实上,最后一个示例对应于以下代码

const button = new Button();
const command = editor.commands.get( 'bold' );

button.bind( 'isEnabled' ).to( command, 'isEnabled' );

您可能已经注意到了 to( /* ... */ ) 接口,它有助于指定属性的名称(或者只是在绑定中“重命名”属性)。

ButtonCommand 类都共享相同的 isEnabled 属性,这使我们能够缩短代码。但是,如果我们决定将 Button#isOn 绑定到 Command#value,那么代码将如下所示

button.bind( 'isOn' ).to( command, 'value' );

属性在绑定中被“重命名”,从现在起,每当 command.value 发生变化时,button.isOn 的值都会反映它。

# 处理属性值

另一个用例是处理绑定属性值,例如,当按钮仅在满足特定条件时才禁用。将回调作为第三个参数传递允许实现自定义逻辑。

在下面的示例中,仅当 command.value 等于 'heading1' 时,isEnabled 属性才会被设置为 true

const command = editor.commands.get( 'heading' );
button.bind( 'isOn' ).to( command, 'value', value => value === 'heading1' );

# 绑定多个属性

可以一次绑定多个属性以简化代码

const button = new Button();
const command = editor.commands.get( 'bold' );

button.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

这与以下内容相同

button.bind( 'isOn' ).to( command, 'value' );
button.bind( 'isEnabled' ).to( command, 'isEnabled' );

在上述绑定中,button.isEnabled 的值将反映 command.isEnabled,而 button.isOn 的值将反映 command.value

请注意,命令的 value 属性也在绑定中被“重命名”,就像在 上一个示例 中一样。

# 使用多个可观察对象绑定

绑定可以包含多个可观察对象,将多个属性组合在自定义回调函数中。让我们创建一个按钮,该按钮仅在 command 启用且 编辑文档(也是一个 Observable)处于焦点时启用

const button = new Button();
const command = editor.commands.get( 'bold' );
const editingDocument = editor.editing.view.document;

button.bind( 'isEnabled' ).to( command, 'isEnabled', editingDocument, 'isFocused',
    ( isCommandEnabled, isDocumentFocused ) => isCommandEnabled && isDocumentFocused );

绑定使 button.isEnabled 的值依赖于 command.isEnablededitingDocument.isFocused,如函数中所指定:两者都必须为 true,按钮才会启用。

# 使用可观察对象数组绑定

可以将同一个属性绑定到可观察对象的数组。让我们将我们的按钮绑定到多个命令,以便每个命令都必须启用,按钮才会启用

const button = new Button();
const commands =  [ commandA, commandB, commandC ];

button.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( isAEnabled, isBEnabled, isCEnabled ) => {
    return isAEnabled && isBEnabled && isCEnabled;
} );

可以使用扩展运算符 (...) 和 Array.every() 方法来简化绑定

const commands =  [ commandA, commandB, commandC ];

button.bind( 'isEnabled' ).toMany( commands, 'isEnabled', ( ...areEnabled ) => {
    return areEnabled.every( isCommandEnabled => isCommandEnabled );
} );

这种绑定在以下情况下可能很有用,例如,当一个按钮打开一个包含其他命令按钮的下拉菜单时,如果所有命令都未启用,则该按钮应被禁用。

# 释放绑定

如果您不再希望对象的属性被绑定,可以使用 unbind() 方法。

您可以指定属性的名称来有选择地取消绑定它们

const button = new Button();
const command = editor.commands.get( 'bold' );

button.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

// More bindings.
// ...

// From now on, button#isEnabled is no longer bound to the command.
button.unbind( 'isEnabled' );

或者,您可以通过不带参数地调用该方法来取消所有绑定

const button = new Button();
const command = editor.commands.get( 'bold' );

button.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

// More bindings.
// ...

// Both #isEnabled and #isOn properties are independent back again.
// They will retain the last values determined by the bindings, though.
button.unbind();

# 装饰对象方法

装饰对象方法将它们转换为事件驱动的,而不改变其原始行为。

当一个方法被装饰时,会创建一个同名事件,并在每次执行该方法时触发。通过监听事件,可以取消执行,更改方法的参数或返回值。这提供了额外的灵活性,例如,让第三方代码以某种方式与装饰其方法的核心类进行交互。

使用 decorate() 方法可以进行装饰。装饰你之前在 上一节 中创建的 Button 类中的 focus 方法,看看它能提供什么。

class Button extends View {
    constructor() {
        // Setting the template and bindings.
        // ...

        this.decorate( 'focus' );
    }

    /**
    * Focuses the button.
    *
    * @param {Boolean} force When `true`, the button will be focused again, even if already
    * focused in DOM.
    * @returns {Boolean} `true` when the DOM element was focused in DOM, `false` otherwise.
    */
    focus( force ) {
        console.log( `Focusing button, force argument="${ force }"` );

        // Unless forced, the button will only focus when not already focused.
        if ( force || document.activeElement != this.element ) {
            this.element.focus();

            return true;
        }

        return false;
    }
}

# 取消执行

由于 focus() 方法现在是事件驱动的,因此可以从外部控制它。例如,可以停止对某些参数的聚焦。请注意,用于拦截默认操作的 high 监听器 优先级

const button = new Button();

// Render the button to create its #element.
button.render();

// The logic controlling the behavior of the button.
button.on( 'focus', ( evt, [ isForced ] ) => {
    // Disallow forcing the focus of this button.
    if ( isForced === true ) {
        evt.stop();
    }
}, { priority: 'high' } );

button.focus(); // -> 'Focusing button, force argument="undefined"'
button.focus( true ); // Nothing is logged, the execution has been stopped.

# 更改返回值

可以使用事件监听器来控制装饰方法的返回值。返回值在事件数据中作为 return 属性传递。

const button = new Button();

// Render the button to create its #element.
button.render();

// The logic controlling the behavior of the button.
button.on( 'focus', ( evt, [ isForced ] ) => {
    // Pretend the button wasn't focused if the focus was forced.
    if ( isForced === true ) {
        evt.return = false;
    }
} );

console.log( button.focus() ); // -> true
console.log( button.focus( true ) ); // -> false

# 动态更改参数

就像返回值一样,传递给方法的参数可以在事件监听器中更改。请注意,用于拦截默认操作的 high 监听器 优先级

const button = new Button();

// Render the button to create its #element.
button.render();

// The logic controlling the behavior of the button.
button.on( 'focus', ( evt, args ) => {
    // Always force the focus.
    args[ 0 ] = true;
}, { priority: 'high' } );

button.focus(); // -> 'Focusing button, force="true"'
button.focus( true ); // -> 'Focusing button, force="true"'