- 前端
- UI组件
【免费下载链接】ckeditor4
The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.
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, basicstylesbug, 4.11.0, 1722:标识该测试属于 bug 修复类、对应修复版本 4.11.0、关联 issue #1722;bender-ui: collapsed:测试 UI 默认折叠,仅暴露测试操作按钮;bender-ckeditor-plugins:指定加载toolbar、wysiwygarea、basicstyles三个插件,保证测试页面拥有完整的所见即所得编辑区域与工具栏。
复现步骤(原文完整继承)
原文给出的操作流程非常简洁,只有两步:
- 记住页面显示的
Filters count数值。 - 点击
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:
- 全局独立过滤器:
new CKEDITOR.filter( 'br' )——不绑定任何编辑器,仅允许<br>元素,生命周期与页面一致; - 编辑器绑定过滤器(带规则):
new CKEDITOR.filter( editor, 'br' )——绑定editor并追加规则; - 编辑器绑定过滤器(无规则):
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; }测试断言清晰对应手动测试的预期:
- 创建编辑器后手工构造 3 个过滤器:2 个绑定编辑器(
new CKEDITOR.filter( editor, 'br' )、new CKEDITOR.filter( editor ))与 1 个独立过滤器(new CKEDITOR.filter( 'br' )); - 调用
editor.destroy(); - 断言一:
countFilters( editor ) === 0——与编辑器绑定的过滤器已全部销毁,一个不剩; - 断言二:全局计数 === 初始值 + 1——仅剩手工创建的独立过滤器,编辑器相关过滤器没有泄漏。
该文件还包含多个与销毁相关的回归用例,例如富文本下拉面板打开时销毁编辑器、销毁后getSnapshot()返回空字符串、未完成初始化即销毁不触发CKEDITOR.warn等,共同覆盖destroy的边界场景。
开发者实践建议
基于上述源码事实,在使用 CKEditor 4 时应注意以下几点:
- 绑定编辑器时务必传入编辑器实例:
new CKEDITOR.filter( editor, rules ),让destroy()能自动回收;不要用纯规则字符串创建本应属于编辑器的过滤器。 - 不要手工销毁属于编辑器的过滤器:
editor.destroy()已统一遍历注册表清理(core/editor.js),手工提前filter.destroy()反而可能在编辑器销毁流程中引发不一致。 - 独立过滤器需自行管理生命周期:不绑定编辑器的过滤器(如
new CKEDITOR.filter( 'br' ))不在自动清理范围内,用完应显式调用filter.destroy()。 - 长生命周期应用应避免反复重建编辑器:即使修复后已无泄漏,
CKEDITOR.replace与destroy的反复执行仍会带来额外开销;确需重建时,可参考 tests/core/editor/manual/filterdestroy.html 的写法,在重建前后核对CKEDITOR.filter.instances数量。 - 善用注册表做运行时诊断:
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.
相关推荐
彻底解决HLS.js内存泄漏:播放器销毁与资源释放全指南
彻底解决HLS.js内存泄漏:播放器销毁与资源释放全指南 你是否遇到过这样的情况:单页应用中使用HLS.js播放视频后,切换页面时内存占用居高不下?随着用户浏览
音视频前端Celo稳定币系统深度解析:如何实现去中心化稳定货币架构
Celo稳定币系统深度解析:如何实现去中心化稳定货币架构 在加密货币世界中, Celo稳定币系统 代表了去中心化金融(DeFi)创新的重要里程碑。作为一个专注于
CKEditor 5 测试环境全解析:从自动化测试到手动测试与内存泄漏检测
CKEditor 5 测试环境全解析:从自动化测试到手动测试与内存泄漏检测 本文面向 CKEditor 5 框架的贡献者与二次开发者,系统讲解 docs/fra
前端富文本UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考