CKEditor 5 Observables 深入解析可观察属性、数据绑定与方法装饰【免费下载链接】ckeditor5Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.项目地址: https://gitcode.com/GitHub_Trending/ck/ckeditor5本文基于 Observables 深入指南结合 ObservableMixin 源码 与测试用例系统讲解 CKEditor 5 框架中最核心的状态管理机制可观察属性Observable Properties、属性绑定Property Bindings与方法装饰Method Decorating。读完本文你将掌握如何让任意类具备可观察能力、如何用set()/bind()/unbind()组织组件状态以及如何通过decorate()让外部代码以事件方式拦截、改写方法行为。Observables可观察对象是拥有可被观察属性的对象当某个属性值发生变化时Observable 会触发一个事件任何监听该事件的代码都能感知变化并做出响应。它们是 CKEditor 5 框架框架索引中最常见的构建块在 UI 层尤为普及——View类 及其子类正是通过模板绑定到可观察属性来实现用户界面的动态交互参见 UI 库指南。此外一些基础类如Editor或Command本身也是 Observable。一、让任何类变成 Observable任何类都可以变为 Observable只需把ObservableMixin混入mix其中import { ObservableMixin, mix } from ckeditor5; class AnyClass { // 任何类定义。 // ... } mix( AnyClass, ObservableMixin );从源码看ObservableMixin是一个高阶函数它接收一个基类base返回继承该基类并实现Observable接口的新类不传参数时返回默认的可观察类内部实现为ObservableMixin( EmitterMixin() )。这一点在 测试用例 中得到了验证ObservableMixin会混入EmitterMixin因此 Observable 天然是事件发射器拥有on/off/listenTo等方法并实现set、bind、unbind三个核心方法。在 TypeScript 中同样可以基于混入继承class MyClass extends ObservableMixin( BaseClass ) { // 该类派生自 BaseClass并实现了 Observable 接口。 }提示Observable 只是事件系统Emitter之上的一层封装若要了解更多高级事件用法可参阅 事件系统深入指南。Observable 对管理应用状态尤其有用应用状态往往是动态的、集中式的、并在组件间共享。一个 Observable 还可以通过属性绑定把自己的状态或其一部分传播给另一个 Observable也可以装饰自己的方法用事件监听器控制方法执行给外部代码提供干预行为的通道。二、让属性可观察set()方法混入ObservableMixin之后就可以用set()定义可观察属性。让我们创建一个简单的 UI 视图组件Button看几个属性长什么样class Button extends View { constructor() { super(); // 这个属性不是可观察的。 // 并非所有属性都必须可观察这完全由你决定 this.type button; const bind this.bindTemplate; // this.label 是可观察的但值为 undefined。 this.set( label ); // this.isOn 是可观察的值为 false。 this.set( isOn, false ); // this.isEnabled 是可观察的值为 true。 this.set( isEnabled, true ); // 更多可观察属性... // ... } }因为Button继承自View而View已经可观察其基类链为DomEmitterMixin( ObservableMixin() )所以无需再混入ObservableMixin。set()也可以接受一个键值对对象来简化代码this.set( { label: undefined, isOn: false, isEnabled: true } );底层实现set()做了什么从源码看set()的核心逻辑observablemixin.ts#L93-L159值得注意每个 Observable 内部用Symbol( observableProperties )标记的Map存储属性状态首次调用set()时会通过initObservable()初始化这些内部数据结构set()用Object.defineProperty()定义该属性写入时先触发set:name事件在值真正写入前若新旧值不同或该属性是首次定义再写入新值并触发change:name事件禁止覆盖如果尝试set()一个已存在的普通属性包括原型链上的方法会抛出observable-set-cannot-override错误测试用例验证set:name事件即使在值相同的情况下也会触发而change:name只在值真正改变时触发测试用例验证。监听属性变化每当label属性变化视图会触发change:label事件事件携带该属性的旧值与新值change:isEnabled、change:isOn事件则分别在isEnabled、isOn变化时触发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; // 修改普通属性不会触发任何事件事件参数结构( evt, propertyName, newValue, oldValue )在源码的 ObservableChangeEvent 类型 与 测试断言 中均有印证。将可观察属性绑定到模板视图触发的事件用于更新 DOM让组件保持动态。下面给视图添加模板把它绑定到已创建的可观察属性上class Button extends View { constructor() { super(); // 之前定义的属性。 // ... // 该模板在 DOM 中的符号化表示 // // button class[ck-disabled] ck-[on|off] typebutton // {{ this.label }} // /button // this.setTemplate( { tag: button, attributes: { class: [ // ck-on 与 ck-off 类根据 #isOn 属性切换。 bind.to( isOn, value value ? ck-on : ck-off ), // 当 #isEnabled 属性为 false 时出现 ck-enabled 类。 bind.if( isEnabled, ck-disabled, value !value ) ], type: this.type }, children: [ { // 按钮文本绑定到 #label 属性。 text: bind.to( label ) } ] } ); } }因为label、isOn、isEnabled都是可观察属性任何变化都会立即反映到 DOMconst button new Button(); // 渲染按钮创建其 #element。 button.render(); button.label Bold; // button classck-off typebuttonBold/button button.isOn true; // button classck-on typebuttonBold/button button.label B; // button classck-on typebuttonB/button button.isOff false; // button classck-off typebuttonB/button button.isEnabled false; // button classck-off ck-disabled typebuttonB/button关于 UI 与模板系统的完整介绍可参阅 UI 库指南中的 Templates 章节。三、属性绑定 Property Bindings一个 Observable 可以把它的状态或其一部分传播给另一个 Observable从而简化代码——例如避免编写大量change:property事件监听器。要进行属性绑定先确保两个对象类都混入了ObservableMixin然后使用bind()方法创建绑定。简单绑定把上一节的加粗按钮实例绑定到加粗命令上只需几行代码就能让按钮自动使用命令的某些属性。bold命令是编辑器中的一个真实命令由BoldEditing注册提供两个可观察属性value和isEnabled。通过editor.commands.get( bold )获取命令const button new Button(); const command editor.commands.get( bold );Button与Command都是 Observable因此可以绑定它们的属性。一个合格的按钮必须在命令被禁用时更新外观一行属性绑定即可完成button.bind( isEnabled ).to( command );绑定之后button.isEnabled立即等于command.isEnabled每当command.isEnabled变化button.isEnabled会立刻同步由于按钮模板的 class 已绑定到button.isEnabled按钮的 DOM 元素也会随之更新。注意command.isEnabled必须是通过set()方法定义的可观察属性绑定才是动态的。幸运的是isEnabled是编辑器中每个命令的标准可观察属性——从 Command 构造函数 可以看到命令在构造时通过this.set( value, undefined )与this.set( isEnabled, false )初始化并监听模型变化自动刷新。但请记住创建自己的可观察类时set()是定义可观察属性的唯一途径。从源码看bind()内部会做参数校验属性名必须是字符串否则抛observable-bind-wrong-properties、不能重复抛observable-bind-duplicate-properties、同一属性不能绑定两次抛observable-bind-rebind这些约束在 测试用例 中均有覆盖。绑定建立后源码通过listenTo( toObservable, change, ... )见 attachBindToListeners监听源对象的所有change事件但只对其中被绑定的属性做出响应从而把事件翻译成目标属性的更新。属性重命名深入剖析bind( /* ... */ ).to( /* ... */ )语法其实上面的例子等价于const button new Button(); const command editor.commands.get( bold ); button.bind( isEnabled ).to( command, isEnabled );你可能注意到了to( /* ... */ )接口它用来指定源属性的名称也就是在绑定中重命名属性。Button与Command恰好拥有同名的isEnabled所以上面的代码被缩短了。如果要把Button#isOn绑定到Command#value代码是这样的button.bind( isOn ).to( command, value );属性在绑定中被重命名后只要command.value变化button.isOn的值就会同步反映。处理属性值另一个常见场景是对被绑定的属性值做处理——比如按钮只在满足某些条件时才被禁用。把回调函数作为第三个参数传入即可实现自定义逻辑。下面的例子中仅当command.value等于heading1时isEnabled才会被设为trueconst 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.isEnabledbutton.isOn反映command.value。注意command.value属性也像前面章节一样被重命名了。绑定多个 Observable绑定可以包含多个 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.isEnabled与editingDocument.isFocused如函数所指定两者都为true按钮才会启用。注意源码中bindTo()对多可观察绑定做了约束——绑定多个 Observable 时必须提供回调函数否则抛observable-bind-to-no-callback多个目标属性同时绑定多个源时属性数量必须一致否则抛observable-bind-to-properties-length。测试用例 对这些边界情况做了系统覆盖。绑定到 Observable 数组也可以把同一属性绑定到一组 Observable 上。例如让按钮绑定多个命令只有每一个命令都启用时按钮才启用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 ); } );这种绑定很有用例如一个按钮打开的下拉框里包含其他命令的按钮当所有命令都禁用时这个按钮也应该被禁用。从源码看bindToMany()只是把[observable, attribute]对展开后转交给to()处理。解除绑定如果不再需要对象的属性被绑定可以使用unbind()。可以指定属性名选择性解绑const button new Button(); const command editor.commands.get( bold ); button.bind( isOn, isEnabled ).to( command, value, isEnabled ); // 更多绑定... // ... // 从现在起button#isEnabled 不再绑定到命令。 button.unbind( isEnabled );也可以不传参数解除所有绑定const button new Button(); const command editor.commands.get( bold ); button.bind( isOn, isEnabled ).to( command, value, isEnabled ); // 更多绑定... // ... // #isEnabled 与 #isOn 都恢复独立。 // 不过它们会保留绑定最后一次确定的值。 button.unbind();从实现细节看unbind()会同时清理内部boundPropertiesSymbol与boundObservablesSymbol两张索引表并在某个源对象不再有任何绑定时调用stopListening( toObservable, change )移除监听不传参数时则批量清理并停止所有监听。四、装饰对象方法 Decorating Object Methods装饰对象方法会把普通方法变成事件驱动的方法且不改变其原始行为。当方法被装饰后每次执行该方法时都会创建并触发一个与它同名的事件。通过监听该事件可以取消方法执行、修改参数、甚至改写方法的返回值。这提供了额外的灵活性——例如让第三方代码以事件方式与核心类交互。装饰使用decorate()方法。装饰一下前面章节创建的Button类的focus方法看看它带来什么class Button extends View { constructor() { // 设置模板与绑定。 // ... this.decorate( focus ); } /** * 让按钮获得焦点。 * * param {Boolean} force 为 true 时即使按钮已在 DOM 中聚焦也会再次聚焦。 * returns {Boolean} DOM 元素被聚焦时返回 true否则返回 false。 */ focus( force ) { console.log( Focusing button, force argument${ force } ); // 除非强制否则按钮只会在尚未聚焦时聚焦。 if ( force || document.activeElement ! this.element ) { this.element.focus(); return true; } return false; } }源码中decorate()的实现方式值得了解它会保存原始方法用this.on( methodName, ... )把原始执行注册为normal优先级的监听器然后用一个包装函数替换原方法——该包装函数通过this.fire( methodName, args )触发事件若方法尚未定义则抛出observablemixin-cannot-decorate-undefined错误。此外stopListening()被重写observablemixin.ts#L312-L328当移除所有监听器时被装饰的方法会恢复到原始状态。在真实项目中Command构造函数 就调用了this.decorate( execute )这正是命令的执行可以被监听器拦截的机制来源。取消方法执行因为focus()现在是事件驱动的它可以从外部被控制。例如可以在某些参数下阻止聚焦。注意监听器使用high优先级 来拦截默认动作const button new Button(); // 渲染按钮创建其 #element。 button.render(); // 控制按钮行为的逻辑。 button.on( focus, ( evt, [ isForced ] ) { // 禁止强制聚焦该按钮。 if ( isForced true ) { evt.stop(); } }, { priority: high } ); button.focus(); // - Focusing button, force argumentundefined button.focus( true ); // 无任何输出执行已被停止。原因原始方法以 normal 优先级注册为监听器而high优先级的监听器会先执行一旦调用evt.stop()后续监听器包括原始方法都不会执行。修改返回值可以在事件监听器中控制被装饰方法的返回值。返回值通过事件数据中的return属性传递const button new Button(); // 渲染按钮创建其 #element。 button.render(); // 控制按钮行为的逻辑。 button.on( focus, ( evt, [ isForced ] ) { // 如果焦点是被强制的就假装按钮没有被聚焦。 if ( isForced true ) { evt.return false; } } ); console.log( button.focus() ); // - true console.log( button.focus( true ) ); // - false动态修改参数与返回值一样方法收到的参数也可以在事件监听器中修改。同样注意使用high优先级监听器来拦截默认动作const button new Button(); // 渲染按钮创建其 #element。 button.render(); // 控制按钮行为的逻辑。 button.on( focus, ( evt, args ) { // 总是强制聚焦。 args[ 0 ] true; }, { priority: high } ); button.focus(); // - Focusing button, forcetrue button.focus( true ); // - Focusing button, forcetrue注意此处监听器收到的第二个参数是参数数组装饰方法事件以args数组作为事件参数类型定义见 DecoratedMethodEvent修改args[ 0 ]即修改方法收到的第一个参数。五、TypeScript 中的使用建议在 TypeScript 项目中通过set()定义的属性应在类中先用declare关键字声明以获得完整的类型推断见 Observable 接口文档class MyObservable extends ObservableMixin() { public declare myProp: number; constructor() { super(); this.set( myProp, 2 ); } }绑定链的to()方法也带有完善的类型重载ObservableSingleBindChain、ObservableDualBindChain等见 observablemixin.ts#L1267-L1443能够对绑定的属性名与回调参数做编译期检查。六、小结与实践建议回顾本文的核心要点set()定义可观察属性属性写入触发set:name与change:name事件监听change:label这类事件即可感知状态变化普通属性直接赋值不触发任何事件。bind()建立属性绑定支持简单绑定、属性重命名、值处理回调、多属性绑定、多 Observable 组合以及toMany()批量绑定unbind()可选择性或整体解除绑定。decorate()装饰方法把方法变成事件驱动允许外部监听器取消执行evt.stop()、改写返回值evt.return与参数修改args数组。实际应用CKEditor 5 的View、Editor、Command等核心类大量使用这些机制——View用可观察属性驱动模板渲染Command用set()维护value/isEnabled并decorate( execute )开放执行拦截。继续深入阅读Observables 深入指南本文对应文档ObservableMixin 源码ObservableMixin 测试用例事件系统深入指南UI 库架构指南【免费下载链接】ckeditor5Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.项目地址: https://gitcode.com/GitHub_Trending/ck/ckeditor5创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考