惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
Jina AI
Jina AI
GbyAI
GbyAI
D
DataBreaches.Net
人人都是产品经理
人人都是产品经理
Hugging Face - Blog
Hugging Face - Blog
V
Visual Studio Blog
P
Proofpoint News Feed
The Cloudflare Blog
H
Help Net Security
MyScale Blog
MyScale Blog
T
The Blog of Author Tim Ferriss
量子位
博客园 - 聂微东
Apple Machine Learning Research
Apple Machine Learning Research
T
Tailwind CSS Blog
博客园 - 三生石上(FineUI控件)
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
MongoDB | Blog
MongoDB | Blog
Last Week in AI
Last Week in AI
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
月光博客
月光博客

码云笔记

Pinia v4 发布:Vue 状态管理进入新时代 Linux LVM:故障排查与数据恢复指南 Java中wait()与sleep()方法的不同点是什么? Bonsai27B 重磅落地!27B 大模型实现手机本地运行 ES2026 第 17 版新增的 5 个新特性 高德重磅上线 ABot‑World Studio,消费级显卡就能搭建专属 3D 虚拟世界 Java中final、finally和finalize的区别是什么? 怎么修改Ollama及其模型的默认路径?Ollama路径修改、迁移与重启的操作教程 iOS27 公开测试版正式上线!AI Siri 与液态玻璃成最大看点 Yuku正式发布!比 Vite+ 快 5.4 倍的前端工具链 Linux lsof 命令详解:进程与文件监控实战 Claude Code CLI命令手册 代码审查重构Bug修复场景化工作流 OpenClaw中连接并使用本地Ollama模型的操作教程 GPT-5.6 今天正式上线:OpenAI 掏出”太阳系”全家桶,Sol / Terra / Luna 齐发,打的却是一张”效率牌” Nacos MySQL持久化部署完整教程 GPT-5.6系列正式上线:OpenAI以效能与性价比重塑AI行业竞争格局 Java中序列化与反序列化的概念是什么? 如何在HTML中实现字体颜色根据背景自动变化? JDK动态代理与CGLIB动态代理的区别是什么? OpenClaw数据加密敏感信息保护的完整教程 砸超亿美元算力!亚马逊秘密启动Moonraker项目,Alexa迎来颠覆性升级 PHP 最轻量、结构化 .docx 到 Markdown 转换器 Elephant TypeScript 7.0 正式发布 蚂蚁灵波开源全球首款 MoE 具身视频大模型 LingBot-Video,为机器人世界模型搭建新底座 SpaceX AI联合Cursor推出Grok 4.5 搅动专业AI赛道格局 英伟达Vera CPU落地商用,改写AI代理时代算力竞争规则 什么是端点检测与响应(EDR)以及它的工作原理介绍 Anime.js 已经 10 岁了!作者查出脑瘤,V5 将开始收费! 星链开启全面升级!SpaceX申请十万颗卫星,构建AI时代全球天基基建 PHP 通用表达式语言 CEL-PHP 2.0.0 正式发布!
现代富文本编辑器为什么最终都会抛弃 DOM?
公民阿赞 普通 · 2026-06-15 · via 码云笔记

AI 概述

早期富文本编辑器直接操作DOM,易出现光标、撤销、特殊节点处理等各类问题。现代编辑器转而维护独立文档状态与AST,将DOM仅作为渲染载体,自主实现撤销栈、适配输入法。其状态驱动的运行逻辑和渲染机制,与React理念相似,也更适配AI编辑等复杂场景。

目录

文章目录隐藏

  1. 1. 原生 API 所存在的问题
  2. 2. 现代编辑器的设计
  3. 3. 现代编辑器越来越像 React
  4. 4. 写在最后

现代富文本编辑器为什么最终都会抛弃 DOM?

很多前端开发第一次接触富文本编辑器时,都会觉得:

<div contenteditable="true"></div>

似乎已经够用了。浏览器已经帮你实现好了:

  • 光标
  • 输入
  • 选区
  • 粘贴
  • 删除
  • undo

甚至还能直接document.execCommand("bold");,于是很多人会天然认为:富文本编辑器本质上不就是“操作 DOM” 吗?

但真正开始做编辑器后,你会发现,现代编辑器最后会逐渐演变成:

状态机 + 事务系统 + 渲染引擎 + 协同系统 + 输入法兼容层

尤其 AI 出现之后,这种趋势越来越明显。今天这篇文章,我们就来聊聊为什么现代富文本编辑器,最终一定会从直接操作 DOM 演变成维护自己的文档状态。

1. 原生 API 所存在的问题

来看一个非常真实的场景,比如你正在做 AI 编辑器,用户选中hello world,然后点击AI 改写,模型返回Hi world,很多人第一反应可能是:

const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(
  document.createTextNode("Hi world")
);

看起来似乎没什么问题。但真正运行后,你很快会发现,整个编辑器开始逐渐失控。比如:

  • 光标丢失
  • undo 失效
  • mention 被拆碎
  • 输入法异常
  • selection 错乱

这里最重要的一点是你会第一次意识到,编辑器真正维护的,根本不只是 HTML。

为什么只靠 DOM 会越来越痛苦?

前面这种做法,本质上是用户操作->直接修改 DOM。也就是说,DOM 既是页面展示结果,也是编辑器数据源。这是最传统的编辑器实现方式。

比如:

range.surroundContents(strong);

或者:

editor.innerHTML

直接作为最终文档保存。但问题在于:DOM 并不理解“文档”。它只理解:

  • div
  • span
  • textNode
  • strong

浏览器真正维护的是页面结构,而不是文档语义。这个差别特别重要。

一个特别经典的问题:Mention

比如:

hello @User1

浏览器里的 DOM 可能长这样:

<p>
  hello
  <span data-id="user1">
    @User1
  </span>
</p>

从浏览器角度:

这只是一个 span。

但从编辑器角度,它其实是一个完整的 mention 节点。因为:

@User1

并不只是字符串。它背后还绑定着:

  • 用户 ID
  • 用户资料
  • 通知关系
  • 协同状态

这时候问题来了,如果用户按 Backspace 呢?浏览器默认行为可能会变成@User1->@Us。因为浏览器只知道:删除字符,但业务真正想要的是整个 mention 一起删除。

这时候你会第一次发现 DOM 根本不理解“这是一个 mention”,浏览器看到的是<span>@User1</span>,但编辑器真正关心的是:

{
  type: "mention",
  content: "@User1",
  data: {
    id: "user1"
  }
}

这里其实已经暴露了现代编辑器最核心的问题:DOM 无法表达稳定语义。

来看另一个特别真实的问题。比如用户点击Ctrl + B,浏览器可能生成:

<strong>hello</strong>

但有时候也可能生成:

<span style="font-weight:bold">
  hello
</span>

甚至:

<b>hello</b>

虽然视觉效果完全一样,但 DOM 结构已经不一样了,这时候问题就出现了。

编辑器真正想表达的其实是“这段文字是加粗”,而不是“用了哪种 HTML 标签”。

于是现代编辑器开始意识到 HTML 不是文档本身,HTML 只是展示结果。真正稳定的东西应该是:

{
  type: "text",
  content: "hello",
  styles: {
    bold: true
  }
}

这里语义终于开始和 DOM 分离。

2. 现代编辑器的设计

现代编辑器内部通常会维护一套 AST 抽象语法树,不过更重要的,是需要理解 AST 是编辑器自己的文档语言。

比如浏览器看到的是 <strong>hello</strong>,编辑器看到的是:

{
  type: "text",
  content: "hello",
  styles: {
    bold: true
  }
}

浏览器看到的是 <span data-id="user1">@User1</span>,编辑器看到的是:

{
  type: "mention",
  content: "@User1",
  data: {
    id: "user1"
  }
}

浏览器看到的是 <div><br></div>,编辑器看到的是:

{
  type: "paragraph",
  children: []
}

这里发生了一件特别重要的事情,编辑器开始拥有自己的“世界模型”。它终于不再只是操作 HTML,而是开始理解文档

现代编辑器真正维护的已经不是 DOM

这是现代编辑器最核心的一次变化。

  • 旧世界:DOM === Document
  • 新世界
    • EditorState === Document
    • DOM === Render Target

也就是说 DOM 不再是真相。真正的文档开始保存在:

  • AST
  • EditorState
  • Node Tree

这些内部状态里。DOM 只是最终渲染结果而已。

输入过程开始变成:DOM -> AST

来看一个最小实现。

比如:

<div id="editor" contenteditable="true"></div>

监听输入:

editor.addEventListener("input", handleInput);

然后:

function handleInput() {
  state.ast = parseHTML(editor.innerHTML);
}

这里发生的事情其实是:

  1. 浏览器先修改 DOM
  2. 编辑器再解析 DOM
  3. 转换成自己的文档状态

也就是说,DOM 只是输入层。真正保存的数据是 state.ast,这一步特别重要。因为编辑器终于开始“接管浏览器”。

Undo 为什么最终一定要自己实现?

浏览器其实已经有Ctrl + Z,但现代编辑器最后还是会自己维护:

  • undoStack
  • redoStack

为什么?

因为浏览器根本不知道什么叫“一次编辑操作”。比如 AI 改写需求,用户只点击了一次 AI 改写 按钮,但内部可能发生了:

  • 删除
  • 插入
  • mark 更新
  • selection 变化

如果依赖浏览器 undo,最后可能会变成一个字符一个字符撤销。但真正合理的行为应该是整个 AI 改写一次性回退。所以现代编辑器通常会这样:

undoStack.push(
  structuredClone(state.ast)
);

然后:

function undo() {
  state.ast = undoStack.pop();
  updateEditor();
}

这里的核心思想其实是撤销的不是 DOM。而是文档状态。

输入法问题

现代编辑器,通常还需要考虑输入法的问题。

很多人第一次做中文输入法时,编辑器会直接崩掉。因为中文输入并不是按键后立刻产生字符,而是:

n
↓
ni
↓
nih
↓
nihao
↓
你好

中间会经历:

  • compositionstart
  • compositionupdate
  • compositionend

如果你在拼音组合阶段,不停修改文档状态,那么编辑器可能就会出现:

  • 光标乱跳
  • 拼音中断
  • selection 丢失

因此现代编辑器都会维护一个关于是否 composition 的开关值:

state.isComposing = true;

然后:

if (state.isComposing) return;

等输入法真正结束后,再更新 AST。

3. 现代编辑器越来越像 React

讲到这里,你一定会发现,现代编辑器已经越来越像一个运行时,而非简单的 textarea 文本框。

这也是为什么很多人第一次看:

  • Lexical
  • ProseMirror
  • Slate

这些现代编辑器的源码时,都会有一种感觉:“怎么越来越像 React?”

因为它们其实都在做:

State
↓
Diff
↓
Reconcile
↓
Commit DOM

这和 React 非常像。

React 的世界

  1. State
  2. Virtual DOM
  3. Diff
  4. Update DOM

编辑器的世界

  1. EditorState
  2. Node Tree
  3. Reconcile
  4. Update DOM

你会发现,现代编辑器本质上已经变成状态驱动 UI 系统。DOM 不再是直接操作对象,而是渲染结果。

AI 出现之后,这种架构设计就更加显得重要了,无论是哪一种操作:

  • AI Rewrite Paragraph
  • AI Insert Table
  • AI Generate Block
  • AI Rewrite Section

AI 操作的都不是简单的字符,而是对应的文档结构。

4. 写在最后

回头再看整个编辑器的发展过程,你会发现整个过程就是:

  1. 浏览器原生编辑系统
  2. 逐渐失控
  3. 编辑器开始接管控制权
  4. 最终建立自己的文档世界

于是:

  • DOM 不再是真相
  • HTML 不再是文档
  • Selection 不再只是浏览器能力
  • Undo 不再交给浏览器
  • Input 不再完全相信 contenteditable

最后,现代富文本编辑器逐渐变成一套独立文档 runtime。内部的 AST 的真正意义也不仅仅是一种树结构,而是编辑器理解文档的方式。

这也是为什么现代编辑器最后会越来越像 React 的原因。

以上关于现代富文本编辑器为什么最终都会抛弃 DOM?的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。

声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » 现代富文本编辑器为什么最终都会抛弃 DOM?