[{"data":1,"prerenderedAt":592},["ShallowReactive",2],{"markdown-doc:\u002Fguide\u002Fcomparison":3},{"id":4,"title":5,"body":6,"description":582,"extension":72,"keywords":583,"locale":584,"meta":585,"navigation":104,"order":586,"path":587,"section":588,"seo":589,"stem":590,"__hash__":591},"docs\u002Fguide\u002Fcomparison.zh-CN.md","与 Incremark 比较",{"type":7,"value":8,"toc":561},"minimark",[9,17,24,28,31,34,41,44,48,52,55,58,61,64,67,121,128,135,138,141,144,194,205,208,215,249,263,280,283,342,345,349,352,356,359,362,366,373,376,386,389,393,396,399,402,405,408,417,420,423,426,434,437,440,443,450,453,456,474,477,480,520,538,557],[10,11,12,16],"p",{},[13,14,15],"strong",{},"Incremark 是我们开发的第一代流式 Markdown 工具。"," QuireKit Markdown 延续了同一个目标：让 AI 一边生成文字，页面一边显示排版好的内容。",[10,18,19,20,23],{},"这次重新设计，主要解决一个容易被低估的问题：",[13,21,22],{},"后面收到的内容，仍然可能改变前面那句话应该怎样显示。"," 新引擎在减少重复解析和渲染的同时，让这些后到的定义能够正确更新前文。",[25,26,27],"h2",{"id":27},"看一棵树怎样更新",[10,29,30],{},"“中间插入”会连续修改同一份原文三次：先在第二段加入加粗，再在第一段加入行内代码，\n最后在中间插入两项列表。每次上一轮的结果都成为下一轮的起点。",[10,32,33],{},"点击播放，跟随移动片段进入原文落点，再找到橙色的受影响分支、换入蓝色的新节点。\n绿色节点保留原树对象。可以暂停、单步、回退，或选择某次插入与某个阶段。\n视图固定，不支持拖动或缩放。",[10,35,36,37,40],{},"第三次插在段落边界：第三段原文虽然没改，解析器仍重建了它的语法分支。\n这也说明更新范围要看实际语法与上下文，不能只按改了几个字判断。\n“末尾追加”和“后置引用”保留对照；后置定义到达后，前面变成链接的是 ",[13,38,39],{},"bar","。\n树和阅读结果来自真实引擎；移动轨迹与慢放是教学编排，不是底层执行日志。",[10,42,43],{},"这段树变化也由我们的流程图库绘制与播放：节点、连线、片段移动及树的替换，都使用同一个公共播放器。",[45,46],"docs-demo",{"name":47},"algorithms\u002Fplayback",[49,50,51],"h3",{"id":51},"对照旧版的流程",[10,53,54],{},"切换“旧 Incremark”和“当前算法”，沿着箭头看一份新片段到达后会经历哪些步骤。图使用我们现有的流程图编辑器渲染，支持缩放和查看大图。",[45,56],{"name":57},"algorithms\u002Fflow",[25,59,60],{"id":60},"为什么要重新设计",[10,62,63],{},"旧 Incremark 把正在生成的回答分成两部分：已经结束的段落，和还在继续写的部分。新文字到来时，尽量保留前面的解析结果，只重新解析末尾。这个思路能减少重复计算。",[10,65,66],{},"但 Markdown 不总是写完一段就能确定这一段的意思。例如，AI 可以先写正文，最后才给出链接地址：",[68,69,74],"pre",{"className":70,"code":71,"language":72,"meta":73,"style":73},"language-md shiki shiki-themes github-light github-light github-dark","更多内容见[文档][api]。\n\n[api]: https:\u002F\u002Fexample.com\u002Fdocs\n","md","",[75,76,77,99,106],"code",{"__ignoreMap":73},[78,79,82,86,90,93,96],"span",{"class":80,"line":81},"line",1,[78,83,85],{"class":84},"sVPnS","更多内容见[",[78,87,89],{"class":88},"snQV0","文档",[78,91,92],{"class":84},"][",[78,94,95],{"class":88},"api",[78,97,98],{"class":84},"]。\n",[78,100,102],{"class":80,"line":101},2,[78,103,105],{"emptyLinePlaceholder":104},true,"\n",[78,107,109,112,114,117],{"class":80,"line":108},3,[78,110,111],{"class":84},"[",[78,113,95],{"class":88},[78,115,116],{"class":84},"]: ",[78,118,120],{"class":119},"svczC","https:\u002F\u002Fexample.com\u002Fdocs\n",[10,122,123,124,127],{},"在最后一行到达之前，",[75,125,126],{},"[文档][api]"," 还没有可用的地址；地址到达以后，前面的“文档”应当变成链接。脚注也是如此：正文先出现，解释可能在整篇回答的末尾才到达。",[10,129,130,131,134],{},"旧版已经为这些情况做了处理：先记录引用，等定义到达后再补上链接。困难在于，有些后置定义还会改变",[13,132,133],{},"哪几个方括号组成一个链接","。这时，只给已有节点补地址就不够了。我们需要重新检查前面的语法，同时让页面更新正确的内容。",[25,136,137],{"id":137},"旧实现的两个具体问题",[49,139,140],{"id":140},"同一个链接地址定义了两次",[10,142,143],{},"假设下面的正文和两个地址依次到达：",[68,145,147],{"className":70,"code":146,"language":72,"meta":73,"style":73},"[文档][api]\n\n[api]: \u002Ffirst\n\n[api]: \u002Fsecond\n",[75,148,149,162,166,177,182],{"__ignoreMap":73},[78,150,151,153,155,157,159],{"class":80,"line":81},[78,152,111],{"class":84},[78,154,89],{"class":88},[78,156,92],{"class":84},[78,158,95],{"class":88},[78,160,161],{"class":84},"]\n",[78,163,164],{"class":80,"line":101},[78,165,105],{"emptyLinePlaceholder":104},[78,167,168,170,172,174],{"class":80,"line":108},[78,169,111],{"class":84},[78,171,95],{"class":88},[78,173,116],{"class":84},[78,175,176],{"class":119},"\u002Ffirst\n",[78,178,180],{"class":80,"line":179},4,[78,181,105],{"emptyLinePlaceholder":104},[78,183,185,187,189,191],{"class":80,"line":184},5,[78,186,111],{"class":84},[78,188,95],{"class":88},[78,190,116],{"class":84},[78,192,193],{"class":119},"\u002Fsecond\n",[10,195,196,197,200,201,204],{},"按照 Markdown 的规则，同名定义应使用第一次出现的地址，所以“文档”应当指向 ",[75,198,199],{},"\u002Ffirst","。旧版在合并定义时覆盖了原来的值，导致前面的链接改为 ",[75,202,203],{},"\u002Fsecond","。当前引擎保留首次定义。",[49,206,207],{"id":207},"链接落在了错误的文字上",[10,209,210,211,214],{},"再看一个短例子。先收到第一行，随后收到 ",[75,212,213],{},"baz"," 的定义：",[68,216,218],{"className":70,"code":217,"language":72,"meta":73,"style":73},"[foo][bar][baz]\n\n[baz]: \u002Ftarget\n",[75,219,220,234,238],{"__ignoreMap":73},[78,221,222,224,227,229,231],{"class":80,"line":81},[78,223,111],{"class":84},[78,225,226],{"class":88},"foo",[78,228,92],{"class":84},[78,230,39],{"class":88},[78,232,233],{"class":84},"][baz]\n",[78,235,236],{"class":80,"line":101},[78,237,105],{"emptyLinePlaceholder":104},[78,239,240,242,244,246],{"class":80,"line":108},[78,241,111],{"class":84},[78,243,213],{"class":88},[78,245,116],{"class":84},[78,247,248],{"class":119},"\u002Ftarget\n",[10,250,251,252,255,256,259,260,262],{},"这段文字应该被理解为：保留普通文字 ",[75,253,254],{},"[foo]","，将 ",[75,257,258],{},"[bar][baz]"," 解释为链接，也就是让 ",[13,261,39],{}," 可以点击。",[10,264,265,266,269,270,273,274,276,277,279],{},"旧版提前把 ",[75,267,268],{},"[foo][bar]"," 识别成了一个引用，把 ",[75,271,272],{},"[baz]"," 留作另一个引用。等地址到达后，可点击的变成了 ",[13,275,213],{},"。前面的分组已经确定，补上地址也不能纠正它。当前引擎会依据实际到达的定义重新判断分组，因此链接落在 ",[13,278,39],{}," 上。",[10,281,282],{},"我们用旧版的两种引擎重新跑了这两个例子，结果如下：",[284,285,286,305],"table",{},[287,288,289],"thead",{},[290,291,292,296,299,302],"tr",{},[293,294,295],"th",{},"情况",[293,297,298],{},"旧 Marked",[293,300,301],{},"旧 Micromark",[293,303,304],{},"当前 QuireKit Markdown",[306,307,308,329],"tbody",{},[290,309,310,316,321,325],{},[311,312,313,315],"td",{},[75,314,95],{}," 定义了两次",[311,317,318,319],{},"链接到 ",[75,320,203],{},[311,322,318,323],{},[75,324,203],{},[311,326,318,327],{},[75,328,199],{},[290,330,331,334,337,339],{},[311,332,333],{},"相邻的三个方括号",[311,335,336],{},"baz 可以点击",[311,338,336],{},[311,340,341],{},"bar 可以点击",[25,343,344],{"id":344},"新算法怎样处理流式更新",[49,346,348],{"id":347},"_1-保留实际收到的原文","1. 保留实际收到的原文",[10,350,351],{},"每次追加都写入同一份 Markdown。停止生成时保留已经收到的文字，不为了显示效果补写围栏、括号或 HTML 结束标签。切换到另一条消息时，再建立新的解析会话。",[49,353,355],{"id":354},"_2-让-lezer-找出可以继续使用的解析结果","2. 让 Lezer 找出可以继续使用的解析结果",[10,357,358],{},"Lezer 把标题、段落、列表和链接等结构记录在语法树里。新文字到来后，它结合语法上下文判断哪些旧结果还能使用，哪些需要重新解析。我们复用这套机制来处理流式追加。",[10,360,361],{},"例如，在末尾开始新段落时，前面未受影响的段落可以继续使用。列表、围栏等结构发生变化时，解析器也会按相应的语法处理，而不是仅凭一个空行就断定前面的内容已经结束。",[49,363,365],{"id":364},"_3-后到的定义可以回头修正引用","3. 后到的定义可以回头修正引用",[10,367,368,369,372],{},"新引擎先收集当前文档中的链接定义，再检查哪些段落的引用判断受到影响。像上面的 ",[75,370,371],{},"[foo][bar][baz]","，需要重新判断哪部分是链接；同名定义则按首次出现的值处理。",[10,374,375],{},"它也会记录段落查询过哪些引用，包括当时还不存在的引用。有足够依据确认一个新定义没有影响该段落时，就继续使用旧结果。",[10,377,378,379,381,382,385],{},"下面这张白板把依赖画出来：",[75,380,95],{}," 的定义到达后，需要重新检查用过它的段落 B。普通介绍和只查询 ",[75,383,384],{},"faq"," 的段落，满足复用条件时可以保留。",[45,387],{"name":388},"algorithms\u002Freferences",[49,390,392],{"id":391},"_4-显示内容也要检查才能复用","4. 显示内容也要检查，才能复用",[10,394,395],{},"语法结构没变，链接地址或脚注编号仍可能改变。因此，渲染缓存还会检查这些依赖，以及组件和 HTML 配置。条件满足时保留已有内容；受影响或无法确认的块重新生成。",[10,397,398],{},"这样，AI 继续往下写时，前面可以复用的内容继续保留；后面的定义改变了前文含义时，前面的链接和脚注仍会更新。",[49,400,401],{"id":401},"这些步骤由谁负责",[10,403,404],{},"解析、引用判断和缓存证明在公共内核里。Vue 和 React 负责各自的组件与提交生命周期；网络流和可选图表渲染由应用接入。",[45,406],{"name":407},"algorithms\u002Flayers",[10,409,410,411,416],{},"这三张图都保留了原始图稿。自己的文档也可以接入同样的",[412,413,415],"a",{"href":414},"\u002Fguide\u002Fdiagrams","流程图、思维导图与白板渲染","。",[25,418,419],{"id":419},"看一次流式更新",[10,421,422],{},"点击“开始流式演示”，观察正文先出现、链接和脚注定义随后到达的过程。同一个脚注被引用两次，脚注正文里也有链接。点击“停止”可以保留当前已经收到的内容。",[45,424],{"name":425},"reading\u002Fstream",[10,427,428,429,433],{},"也可以把上面的两个短例子粘到 ",[412,430,432],{"href":431},"\u002Fplayground","Playground","，查看新引擎的输出。",[25,435,436],{"id":436},"速度有什么变化",[10,438,439],{},"旧版的 Marked 本身很快。换用 Lezer，并不意味着所有 Markdown 都会解析得更快。当前优化主要减少两种重复工作：重新解析没有受影响的文字，以及重新生成可以复用的显示内容。",[10,441,442],{},"此前原 demo 的解析测试中，Lezer\u002FMSFE 快于旧 Micromark、慢于旧 Marked。这批数据早于当前渲染缓存，不能当作当前版本的速度排名。",[10,444,445,446,416],{},"大型列表、表格或很长的单段落，仍可能作为一个整体重新处理。判断流式体验时，需要同时看解析、渲染和浏览器更新，不能只看解析器耗时。测量方法见",[412,447,449],{"href":448},"\u002Fguide\u002Fperformance","性能说明",[25,451,452],{"id":452},"使用与迁移",[10,454,455],{},"QuireKit Markdown 支持 Vue 和 React，两者共用同一套解析规则。基本用法仍然是传入 Markdown文字；链接、代码块等可以换成自己的组件，也支持自定义语法和显式开启 HTML。",[10,457,458,459,463,464,468,469,473],{},"新包没有沿用旧 Incremark 的 API，旧插件需要适配。可以从 ",[412,460,462],{"href":461},"\u002Fguide\u002Fgetting-started","Vue 快速开始","或 ",[412,465,467],{"href":466},"\u002Fguide\u002Freact","React 接入","开始，按",[412,470,472],{"href":471},"\u002Fguide\u002Fextensions","自定义语法与节点","迁移扩展。当前仍为未发布的预览版；旧 Incremark 与当前的核心、Vue、React 包均使用 MIT 许可。",[25,475,476],{"id":476},"继续了解实现",[10,478,479],{},"如果你准备扩展解析器或研究缓存，下面这些细节会更有用：",[481,482,483,494,500,512],"ul",{},[484,485,486,489,490,493],"li",{},[13,487,488],{},"语法复用："," CodeMirror 用文本事务和 ",[75,491,492],{},"TreeFragment"," 映射变化。我们在 Lezer 层提供\n顶层块复用信息，保持原生语法和复用判断；引用处理放在 MSFE 的 parser 包装层。",[484,495,496,499],{},[13,497,498],{},"引用判断："," 完整段落记录语法实际查询过的标签及其是否存在。只有这些查询结果未变，\n才复用相应判断；没有完整记录的范围保守检查。地址和 title 的变化另由渲染层检查。",[484,501,502,505,506,509,510,416],{},[13,503,504],{},"渲染缓存："," 检查原生 Tree 身份、源码范围、引用目标、脚注导航和渲染配置。\n自定义节点可以用 ",[75,507,508],{},"cacheKey(document)"," 声明额外依赖；未声明时重新生成。\n具体条件与缓存限额见",[412,511,449],{"href":448},[484,513,514,517,518,416],{},[13,515,516],{},"框架与资源："," Vue 和 React 使用各自的组件。React 在提交后接受候选缓存，资源也在\n客户端提交后加载；切换、卸载和迟到结果按取消与释放规则处理，见 ",[412,519,467],{"href":466},[10,521,522,523,526,527,530,531,537],{},"文中两例于 2026-10-11 验证，旧版为 ",[75,524,525],{},"@incremark\u002Fcore"," 1.0.2、commit ",[75,528,529],{},"98f6b95","。旧结果来自两引擎的 AST 与定义表，当前结果来自 React 服务端渲染，并以独立完整 GFM 解析和",[412,532,536],{"href":533,"rel":534},"https:\u002F\u002Fspec.commonmark.org\u002F0.31.2\u002F#links",[535],"nofollow","CommonMark 引用规则","核对。这两个例子说明了具体问题，版本范围不延伸到后续 Incremark。",[10,539,540,541,546,547,546,552,416],{},"对应源码：",[412,542,545],{"href":543,"rel":544},"https:\u002F\u002Fgithub.com\u002Fkingshuaishuai\u002Fincremark\u002Fblob\u002F98f6b95bba58d0c9e4ba1af37a4d52c2f2fae1df\u002Fpackages\u002Fcore\u002Fsrc\u002Fparser\u002FIncremarkParser.ts",[535],"旧解析会话","、",[412,548,551],{"href":549,"rel":550},"https:\u002F\u002Fgithub.com\u002Fkingshuaishuai\u002Fincremark\u002Fblob\u002F98f6b95bba58d0c9e4ba1af37a4d52c2f2fae1df\u002Fpackages\u002Fcore\u002Fsrc\u002Fextensions\u002Fmarked-extensions\u002FoptimisticReferenceExtension.ts",[535],"旧引用识别",[412,553,556],{"href":554,"rel":555},"https:\u002F\u002Fgithub.com\u002Fkingshuaishuai\u002Fincremark\u002Fblob\u002F98f6b95bba58d0c9e4ba1af37a4d52c2f2fae1df\u002Fpackages\u002Fcore\u002Fsrc\u002Fparser\u002Fmanager\u002FDefinitionManager.ts",[535],"旧定义合并",[558,559,560],"style",{},"html pre.shiki code .sVPnS, html code.shiki .sVPnS{--shiki-default:#24292E;--shiki-light:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .snQV0, html code.shiki .snQV0{--shiki-default:#032F62;--shiki-default-text-decoration:underline;--shiki-light:#032F62;--shiki-light-text-decoration:underline;--shiki-dark:#DBEDFF;--shiki-dark-text-decoration:underline}html pre.shiki code .svczC, html code.shiki .svczC{--shiki-default:#24292E;--shiki-default-text-decoration:underline;--shiki-light:#24292E;--shiki-light-text-decoration:underline;--shiki-dark:#E1E4E8;--shiki-dark-text-decoration:underline}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":73,"searchDepth":101,"depth":101,"links":562},[563,566,567,571,578,579,580,581],{"id":27,"depth":101,"text":27,"children":564},[565],{"id":51,"depth":108,"text":51},{"id":60,"depth":101,"text":60},{"id":137,"depth":101,"text":137,"children":568},[569,570],{"id":140,"depth":108,"text":140},{"id":207,"depth":108,"text":207},{"id":344,"depth":101,"text":344,"children":572},[573,574,575,576,577],{"id":347,"depth":108,"text":348},{"id":354,"depth":108,"text":355},{"id":364,"depth":108,"text":365},{"id":391,"depth":108,"text":392},{"id":401,"depth":108,"text":401},{"id":419,"depth":101,"text":419},{"id":436,"depth":101,"text":436},{"id":452,"depth":101,"text":452},{"id":476,"depth":101,"text":476},"为什么我们重新设计了 Incremark，以及新引擎怎样处理流式 Markdown。",null,"zh-CN",{},7,"\u002Fguide\u002Fcomparison","参考",{"title":5,"description":582},"guide\u002Fcomparison.zh-CN","-MvWAizxDO_vZskg3HQAjzC_0FL71mNKpxQaKVigSNU",1793045321583]