- 前端
- UI组件
- 富文本
【免费下载链接】cherry-markdown
✨ A Markdown Editor
导读
Cherry Markdown 在 0.0.x 开发周期中通过一个 changelog 变更(.changeset/cherry-instance-determinism.md)修复了引擎渲染的实例级确定性问题:此前,同一份 Markdown 交给不同Engine实例渲染,输出的 HTML(尤其是段落节点的data-sign属性)可能不一致。本文以该变更为切入点,深入剖析其根因、修复方案(ParagraphBase.resetCacheCounter())、底层缓存机制,以及仓库中对应的回归测试(EngineDeterminism.spec.ts)与验证方法,帮助你理解 Cherry Markdown 预览区局部更新、编辑/预览同步滚动等核心机制背后的确定性设计。
变更背景:引擎输出为何会依赖实例
Cherry Markdown 的渲染流程围绕Engine展开:Engine.makeHtml(md)依次执行大文本缓存、$beforeMakeHtml、段落级 hook 渲染($dealParagraph)、$afterMakeHtml等步骤,最终产出 HTML 字符串(Engine.js)。其中段落级语法(标题、列表、代码块、公式块等)会通过ParagraphBase维护两类关键信息:
- 签名(sign):基于段落内容哈希生成,写入 HTML 的
data-sign属性,用于预览区域的局部更新; - 行号(lines):写入
data-lines,用于编辑区与预览区同步滚动。
问题在于:段落缓存键的前缀~~C{n}来自一个模块级共享计数器cacheCounter(ParagraphBase.js)。不同实例在构造时按创建顺序依次递增编号,于是同一个文档在不同实例中可能被标记为~~C2、~~C16等不同编号。
单独的占位符编号不同本身不致命(渲染完成后占位符会被还原为真实内容),但存在一条关键链路会把它泄漏到最终输出中:
- 某段落的 Markdown 中尚未完成替换就残留了另一个 hook 的
~~C{n}占位符(例如“未闭合的块级公式区域之后紧跟行内公式”这种形态); - 该段落随后以含占位符的字符串参与
$engine.hash()计算签名; - 由于不同实例占位符编号不同,哈希结果不同,最终输出的
data-sign也随之漂移。
也就是说,同一份文档、不同实例、输出字节不同。对于需要确定性输出的场景(SSR、快照测试、文档对比、多实例并存渲染),这是一个必须消除的不稳定因素。
根因定位:模块级计数器的跨实例污染
修复的核心落在 ParagraphBase.js 新增的静态方法:
static resetCacheCounter() { cacheCounter = 0; }每次Engine实例构造时调用该方法,把共享的段落缓存计数器归零(Engine.js):
// 实例级确定性:内建段落 hook 每次从 ~~C0 开始编号,跨实例占位符一致 ParagraphBase.resetCacheCounter(); this.hookCenter = new HookCenter(hooksConfig, markdownParams, cherry);这样每个新实例的内建段落 hook 都从~~C0重新编号,占位符编号不再依赖实例创建顺序,最终data-sign也就与实例无关。
从源码结构看,cacheCounter是ParagraphBase模块顶层的一个普通let变量,由所有继承ParagraphBase的段落级 hook 共享。每个需要缓存的 hook 在构造时读取当前编号并自增(ParagraphBase.js):
constructor({ needCache, defaultCache = {} } = { needCache: false }) { super({}); this.needCache = !!needCache; this.sign = ''; if (needCache) { this.cache = new LRUCache(2000); this.cacheKey = `~~C${cacheCounter}`; cacheCounter += 1; } ... }因此修复的语义非常明确:把“跨实例共享”的编号状态,重置为“每个实例独立、从零开始”,保证同一套内建 hook 在不同实例中获得一致的~~C{n}占位符编号。
关键机制一:段落缓存键与>makeHtml(str, sentenceMakeFunc) { ... return str.replace(this.RULE.reg, (match, preLines, content) => { ... const processor = (p) => { if (p.trim() === '') { return ''; } const { sign, html } = this.cacheAndGetData(p, sentenceMakeFunc, 3000, -100); let domName = 'p'; const isContainBlockTest = new RegExp(`<(${blockNames})[^>]*>`, 'i'); if (isContainBlockTest.test(html)) { domName = 'div'; } const lines = this.getLineCount(p, p); return `<${domName}>赞
- 前端
- UI组件
- 富文本
【免费下载链接】cherry-markdown
✨ A Markdown Editor
相关推荐
Cangjie-SIG/RGF_CJ深度解析:跨技术渲染一致性实现原理
Cangjie SIG/RGF_CJ深度解析:跨技术渲染一致性实现原理 引言:Windows渲染技术的碎片化困境 在Windows平台开发图形界面应用时,开发者
图形学桌面应用oh-my-openagent 文档漂移审计实战:以 F3 修复工作单为例的文档-代码一致性治理
oh my openagent 文档漂移审计实战:以 F3 修复工作单为例的文档 代码一致性治理 导读 :本文以 oh my openagent 仓库中一次真实
人工智能AI Agent代码智能体多智能体MCP ClientsAgent 编排AI-Media2Doc终极指南:如何快速将音视频转化为小红书/公众号/笔记/思维导图
AI Media2Doc终极指南:如何快速将音视频转化为小红书/公众号/笔记/思维导图 AI Media2Doc是一款功能强大的开源工具,能够一键将音视频内容智
人工智能AI 应用语音后端前端