☰
CKEditor 4 编辑器销毁时过滤器(Filter)未释放导致内存泄漏:`filterdestroy` 手动测试全解析
2026/10/15 3:45:26 网站建设 项目流程
  • 前端
  • UI组件

【免费下载链接】ckeditor4

The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.

项目地址:https://gitcode.com/gh_mirrors/ck/ckeditor4
点击查看免费下载

CKEditor 4 的 Advanced Content Filter(ACF,高级内容过滤器)是编辑器的核心内容治理机制,而其全局实例注册表CKEDITOR.filter.instances在早期版本中会在编辑器销毁后残留已失效的 Filter 实例,造成内存泄漏。本文以仓库中的手动测试用例tests/core/editor/manual/filterdestroy.md为骨架,结合core/filter.js与core/editor.js的源码实现,完整讲解该问题的复现步骤、根因、修复机制(ticket #1722,随 4.11.0 发布)以及日常开发中的防泄漏最佳实践。读完本文,你将掌握如何复现该缺陷、如何从源码层面追踪 Filter 实例生命周期,以及如何规范地编写与之配套的自动化回归测试。

背景:CKEDITOR.filter.instances全局注册表与内存泄漏隐患

CKEditor 4 的过滤器(CKEDITOR.filter,自 4.1.0 起引入)负责依据允许内容规则(Allowed Content Rules)对进入编辑器的 HTML 进行校验、转换与清洗。每一个 Filter 实例在创建时都会登记到一个全局静态注册表中,源码位于 core/filter.js:

CKEDITOR.filter.instances = {};

构造函数创建实例后立即执行注册(见 core/filter.js):

// Register filter instance. CKEDITOR.filter.instances[ this.id ] = this;

每个 Filter 拥有唯一自增id(this.id = CKEDITOR.tools.getNextNumber()),可通过CKEDITOR.filter.instances[ filter.id ]反查实例(core/filter.js)。

问题的根源在于:在 4.11.0 之前,注册表只进不出。当编辑器实例被销毁时,与其绑定的 Filter 实例仍然残留在CKEDITOR.filter.instances中,实例之间的引用链(编辑器 → 过滤器 → 规则对象)无法被垃圾回收,长生命周期页面(尤其是反复创建/销毁编辑器的单页应用)会累积大量无效 Filter,最终导致内存泄漏。这一缺陷在仓库 CHANGES.md 中被明确记录:

[#1722]: CKEDITOR.filter.instances is causing memory leaks.

随 CKEditor 4.11(CHANGES.md)一并修复。

手动测试用例解读:filterdestroy

仓库针对该缺陷提供了配套的手动测试,由一对文件组成:测试步骤说明 tests/core/editor/manual/filterdestroy.md 与测试页面 tests/core/editor/manual/filterdestroy.html。

测试元数据(Bender 标签)

手动测试文件头部声明了 Bender 测试运行器所需的三组标签:

@bender-tags: bug, 4.11.0, 1722 @bender-ui: collapsed @bender-ckeditor-plugins: toolbar, wysiwygarea, basicstyles
  • bug, 4.11.0, 1722:标识该测试属于 bug 修复类、对应修复版本 4.11.0、关联 issue #1722;
  • bender-ui: collapsed:测试 UI 默认折叠,仅暴露测试操作按钮;
  • bender-ckeditor-plugins:指定加载toolbar、wysiwygarea、basicstyles三个插件,保证测试页面拥有完整的所见即所得编辑区域与工具栏。

复现步骤(原文完整继承)

原文给出的操作流程非常简洁,只有两步:

  1. 记住页面显示的Filters count数值。
  2. 点击Rebuild editor按钮,观察数值变化。

预期结果(Expected):每次点击Rebuild editor按钮后,Filters count都不应改变。

非预期结果(Unexpected):Filters count发生了变化——这正是 4.11.0 之前因 Filter 实例残留而出现的泄漏现象。

测试页面背后的实现逻辑

仅凭 .md 文件无法看出该页面如何运作,需对照其配套 HTML tests/core/editor/manual/filterdestroy.html 才能完整理解:

<button id="rebuild" onclick="rebuildEditor()">Rebuild editor</button> <span id="counter"></span> <textarea id="editor"> </textarea> <script> var counter = document.getElementById( 'counter' ); // Global filter. new CKEDITOR.filter( 'br' ); rebuildEditor(); function rebuildEditor() { var currentEditor = CKEDITOR.instances.editor; if( currentEditor ) { currentEditor.destroy(); } var editor = CKEDITOR.replace( 'editor' ); new CKEDITOR.filter( editor, 'br' ); new CKEDITOR.filter( editor ); counter.innerText = 'Filters count: ' + bender.tools.objToArray( CKEDITOR.filter.instances ).length; } </script>

该页面刻意构造了三种不同生命周期的 Filter:

  1. 全局独立过滤器:new CKEDITOR.filter( 'br' )——不绑定任何编辑器,仅允许<br>元素,生命周期与页面一致;
  2. 编辑器绑定过滤器(带规则):new CKEDITOR.filter( editor, 'br' )——绑定editor并追加规则;
  3. 编辑器绑定过滤器(无规则):new CKEDITOR.filter( editor )——只绑定编辑器,规则从编辑器配置推导。

每次点击按钮都会先销毁当前编辑器(currentEditor.destroy()),再通过CKEDITOR.replace( 'editor' )重建,随后重新创建两个编辑器绑定过滤器。页面底部通过bender.tools.objToArray( CKEDITOR.filter.instances ).length实时统计全局注册表中的 Filter 数量并展示到counter。

测试判定的关键逻辑:在正确修复后,每轮重建结束时,注册表中只会剩下 1 个全局独立过滤器,因此Filters count恒为 1;若发生泄漏,每点击一次按钮,绑定到旧编辑器的过滤器就会残留,数值单调递增——这就是"数值变化"这一非预期现象的含义。

根因与修复:从源码看 Filter 生命周期管理

过滤器如何与编辑器绑定

Filter 构造函数的第一个参数既可以是规则字符串(独立过滤器),也可以是编辑器实例。核心逻辑位于 core/filter.js:

var editor = this.editor = editorOrRules instanceof CKEDITOR.editor ? editorOrRules : null;

当传入的是编辑器实例时,this.editor指向该编辑器,从而形成"编辑器 ↔ 过滤器"的绑定关系。构造文档也明确建议(core/filter.js):

如果过滤器仅供单个编辑器使用,应将编辑器实例作为第一个参数传入;这样在调用editor.destroy()时过滤器会被一并移除,以防止内存泄漏。

// In both cases the filter will be removed during the CKEDITOR.editor#method-destroy function execution. var filter1 = new CKEDITOR.filter( editor ); var filter2 = new CKEDITOR.filter( editor, 'b' );

顺带说明:第二个参数rules允许在绑定编辑器的同时直接传入规则字符串,该参数自4.11.0起可用(core/filter.js)——这与 #1722 的修复版本一致。

每个编辑器在初始化时,initComponents会同步创建其主过滤器并同时赋值给editor.filter与editor.activeFilter(core/editor.js):

editor.dataProcessor = new CKEDITOR.htmlDataProcessor( editor ); // Set activeFilter directly to avoid firing event. editor.filter = editor.activeFilter = new CKEDITOR.filter( editor );

此外,editor.setActiveFilter( filter )允许运行期切换活动过滤器(例如根据上下文临时放宽/收紧内容规则),并通过activeFilterChange事件联动 Enter 模式的自动调整(core/editor.js)。切换产生的临时过滤器同样与编辑器绑定,销毁时也必须一并清理。

editor.destroy()中的关键清理步骤

修复的核心落在editor.destroy()方法上(core/editor.js)。除常规的资源释放(触发beforeDestroy、同步编辑内容、卸载 editable、删除editor.filter、设置status = 'destroyed'、触发destroy、移除监听器、从CKEDITOR.instances注销)之外,新增了专门针对过滤器泄漏的清理段:

// Destroy filters attached to the editor (#1722). CKEDITOR.tools.array.forEach( CKEDITOR.tools.object.keys( filters ), function( id ) { var filter = filters[ id ]; if ( self === filter.editor ) { filter.destroy(); } } ); delete this.activeFilter;

其工作方式是:遍历全局注册表CKEDITOR.filter.instances,凡是filter.editor === 当前编辑器的过滤器,一律调用其destroy()方法;最后显式删除activeFilter引用。由于主过滤器、通过setActiveFilter切换的过滤器以及手工new CKEDITOR.filter( editor )创建的过滤器都满足filter.editor === editor这一条件,因此一次销毁即可全部回收,不会遗漏任何分支。

filter.destroy()的收尾动作

注册表中实例的最终移除由CKEDITOR.filter原型上的destroy()完成(core/filter.js):

destroy: function() { delete CKEDITOR.filter.instances[ this.id ]; // Deleting reference to filter instance should be enough, // but since these are big objects it's safe to clean them up too. delete this._; delete this.allowedContent; delete this.disallowedContent; }

该方法(自 4.4.5 起提供)不仅把实例从全局注册表删除,还主动清空内部三个体积较大的数据结构:_(优化后的允许/禁止规则缓存、元素转换表、cachedTests/cachedChecks缓存)、allowedContent(标准化允许规则数组)与disallowedContent(标准化禁止规则数组),彻底斩断引用,使对象可被垃圾回收。

修复前后的行为对比

场景4.11.0 之前4.11.0 及之后
new CKEDITOR.filter( editor )后销毁编辑器过滤器残留于CKEDITOR.filter.instances,计数递增随editor.destroy()一并destroy(),计数不变
反复重建编辑器(SPA 场景)注册表无限膨胀,内存泄漏每轮只保留独立过滤器,注册表稳定
独立过滤器(不绑定编辑器)不受影响,页面生命周期内保留不受影响,仍由开发者自行管理

自动化回归测试:destroy.js中的test destroy attached filters

仓库不仅提供了手动测试,还在 tests/core/editor/destroy.js 中加入了对应的自动化回归测试,防止缺陷复发:

// (#1722) 'test destroy attached filters': function() { var filters = countFilters(); bender.editorBot.create( { name: 'editor_filter_destroy' }, function( bot ) { var editor = bot.editor; new CKEDITOR.filter( editor, 'br' ); new CKEDITOR.filter( editor ); new CKEDITOR.filter( 'br' ); editor.destroy(); assert.areEqual( 0, countFilters( editor ) ); assert.areEqual( filters + 1, countFilters() ); } ); }

辅助函数countFilters借助 Bender 测试框架的工具统计注册表数量(tests/core/editor/destroy.js):

function countFilters( editor ) { var filters = bender.tools.objToArray( CKEDITOR.filter.instances ); return editor ? CKEDITOR.tools.array.filter( filters, function( filter ) { return filter.editor === editor; } ).length : filters.length; }

测试断言清晰对应手动测试的预期:

  1. 创建编辑器后手工构造 3 个过滤器:2 个绑定编辑器(new CKEDITOR.filter( editor, 'br' )、new CKEDITOR.filter( editor ))与 1 个独立过滤器(new CKEDITOR.filter( 'br' ));
  2. 调用editor.destroy();
  3. 断言一:countFilters( editor ) === 0——与编辑器绑定的过滤器已全部销毁,一个不剩;
  4. 断言二:全局计数 === 初始值 + 1——仅剩手工创建的独立过滤器,编辑器相关过滤器没有泄漏。

该文件还包含多个与销毁相关的回归用例,例如富文本下拉面板打开时销毁编辑器、销毁后getSnapshot()返回空字符串、未完成初始化即销毁不触发CKEDITOR.warn等,共同覆盖destroy的边界场景。

开发者实践建议

基于上述源码事实,在使用 CKEditor 4 时应注意以下几点:

  1. 绑定编辑器时务必传入编辑器实例:new CKEDITOR.filter( editor, rules ),让destroy()能自动回收;不要用纯规则字符串创建本应属于编辑器的过滤器。
  2. 不要手工销毁属于编辑器的过滤器:editor.destroy()已统一遍历注册表清理(core/editor.js),手工提前filter.destroy()反而可能在编辑器销毁流程中引发不一致。
  3. 独立过滤器需自行管理生命周期:不绑定编辑器的过滤器(如new CKEDITOR.filter( 'br' ))不在自动清理范围内,用完应显式调用filter.destroy()。
  4. 长生命周期应用应避免反复重建编辑器:即使修复后已无泄漏,CKEDITOR.replace与destroy的反复执行仍会带来额外开销;确需重建时,可参考 tests/core/editor/manual/filterdestroy.html 的写法,在重建前后核对CKEDITOR.filter.instances数量。
  5. 善用注册表做运行时诊断:CKEDITOR.filter.instances是公开的只读诊断入口(core/filter.js),可通过filter.editor判断每个过滤器归属的编辑器,快速定位是哪个环节产生了游离实例。

总结

filterdestroy手动测试虽然只有两行操作步骤,却精准锚定了一个真实的资源管理缺陷:CKEDITOR.filter.instances只进不出导致的内存泄漏(#1722)。其修复由三层机制共同保障——构造函数按需建立编辑器绑定(core/filter.js)、editor.destroy()遍历注册表统一回收(core/editor.js)、filter.destroy()彻底清空内部对象(core/filter.js)——并配以 tests/core/editor/destroy.js 中的自动化断言守住回归底线。理解这条从手动复现、根因定位到自动回归的完整链路,有助于在集成 CKEditor 4 时写出无泄漏、可维护的内容治理代码。

  • 前端
  • UI组件

【免费下载链接】ckeditor4

The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.

项目地址:https://gitcode.com/gh_mirrors/ck/ckeditor4
点击查看免费下载

相关推荐

上一篇:NumPy 1.14.6 维护版本变更日志解读:四项关键修复与发布细节
下一篇:TiXL 图像模糊效果算子完全指南:Lib.image.fx.blur 家族(Blur、FastBlur、DirectionalBlur、Bloom、Sharpen)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询