使用 useState 管理增量 HTML 片段,配合流式 Markdown 解析器实现渐进式渲染;每产出一个片段即追加并用 useEffect 延迟高亮 code 元素,通过唯一 ID 和 useRef 避免重复处理。

直接用本地局部状态(如 React 的 useState 或 useReducer)管理增量解析结果,配合流式 Markdown 解析器(如 thetarnav/streaming-markdown),就能实现语法高亮的实时渐进式更新——不是等全文解析完再渲染,而是每收到一段合法 Markdown 片段,就立即高亮并追加到已有内容中。
保持 DOM 增量更新而非全量重写
传统做法是把整个 Markdown 字符串传给解析器,生成完整 HTML 后一次性 innerHTML 替换。这会导致闪烁、滚动跳动、焦点丢失。正确方式是:
- 将已渲染的 HTML 片段存为局部状态(例如
const [htmlFragments, setHtmlFragments] = useState([])) - 每次流式解析器产出一个新 HTML 片段(如一个标题、一段代码块),调用
setHtmlFragments(prev => [...prev, newFragment]) - 在 JSX 中用
{htmlFragments.map((f, i) => <div key="{i}" dangerouslysetinnerhtml="{{__html:" f></div>)}渲染,React 会自动复用已有节点,只插入新增部分
代码块高亮必须延迟到片段就绪后再触发
流式解析器输出的代码块 HTML 通常不含高亮样式(仅含 <pre><code class="js">...)。需在片段挂载后,由 highlight.js 或 Prism.js 主动扫描并高亮:
- 不要在解析阶段同步调用
highlightElement(),因为 DOM 节点可能尚未插入 - 改用
useEffect监听htmlFragments变化,对最新一项中的code[class]元素执行高亮 - 示例逻辑:if (newFragment.includes('code class=')) { setTimeout(() => hljs.highlightAll(), 0); }(用
setTimeout确保 DOM 已更新)
避免重复高亮与状态错位
流式更新中,同一段内容可能被多次解析(如编辑器边输边发、网络重试),导致重复插入或高亮错乱:
- 为每个解析片段生成唯一 ID(如基于内容哈希或递增序号),作为
key和状态标识 - 使用
useRef缓存已高亮过的code元素引用,防止同一节点被多次highlightElement() - 若需支持编辑回退,局部状态应记录“已处理字节偏移量”,而非仅依赖 HTML 片段数组
兼顾性能与可读性的轻量级实践
不必强求每个字符都触发更新。合理分块能显著降低开销:
- 解析器接收数据时,按语义边界(如空行、```结束符、#标题行)做简单缓冲,凑够一块再吐出 HTML
- 对纯文本段落,可跳过高亮步骤;仅对
pre > code、blockquote、table等复杂结构启用增强处理 - 移动端或低配设备上,可关闭实时高亮,改用 CSS 变量控制主题色,靠字体和间距提升可读性


















