#

#源码分析

99篇文章

Pi 如何用 entry 树保存会话并构建 Context

本文是「Pi 源码拆解」系列第 3 篇。系列目录: 2026 06-19 Pi 源码拆解(一):极简 Coding Agent Harness 的分层设计 06-19 Pi 源码拆解(二): Agent 运行时机制 06-20 Pi 源码拆解(三):Session和Context管理(本篇) 第…

Pi 源码拆解(二): Agent 运行时机制

本文是「Pi 源码拆解」系列第 2 篇。系列目录: 2026 06-19 Pi 源码拆解(一):极简 Coding Agent Harness 的分层设计 06-19 Pi 源码拆解(二): Agent 运行时机制(本篇) 上一篇介绍了 pi 的包结构和「刻意不做」清单。本文分析 的 agent…

Claude Code 六大 Skill 套件源码级对比:谁在解决什么问题,谁在制造新问题

不装套件直接使用 Claude Code 时,常见三类问题。会话变长后,窗口会积累过时信息,模型可能基于错误前提决策。需求描述与模型理解不一致时,产出越复杂,偏差越可能扩大,而且通常在产出完成后才发现。缺少类型检查、测试和浏览器验证时,模型无法获得执行结果的反馈。 这些 Skill…

JSVM3 的预编译、序列化与运行时优化

系列目录 「JSVM3 原理与思考」系列,基于 jsvm3 源码整理: 从 AST 直驱到字节码:JSVM3 的编译与执行分层 JSVM3 如何执行字节码:指令、栈帧与求值栈 JSVM3 如何实现函数调用、作用域与声明提升 JSVM3 如何编译循环、跳转与 try/catch/finally…

用测试定义解释器的语义边界

系列目录 「JSVM3 原理与思考」系列,基于 jsvm3 源码整理: 从 AST 直驱到字节码:JSVM3 的编译与执行分层 JSVM3 如何执行字节码:指令、栈帧与求值栈 JSVM3 如何实现函数调用、作用域与声明提升 JSVM3 如何编译循环、跳转与 try/catch/finally…

JSVM3 如何编译循环、跳转与 try/catch/finally

系列目录 「JSVM3 原理与思考」系列,基于 jsvm3 源码整理: 从 AST 直驱到字节码:JSVM3 的编译与执行分层 JSVM3 如何执行字节码:指令、栈帧与求值栈 JSVM3 如何实现函数调用、作用域与声明提升 JSVM3 如何编译循环、跳转与 try/catch/finally…

JSVM3 如何实现函数调用、作用域与声明提升

系列目录 「JSVM3 原理与思考」系列,基于 jsvm3 源码整理: 从 AST 直驱到字节码:JSVM3 的编译与执行分层 JSVM3 如何执行字节码:指令、栈帧与求值栈 JSVM3 如何实现函数调用、作用域与声明提升(本篇) 第…

TS实现基于字节码的JS 虚拟机

系列目录 「JSVM3 原理与思考」系列,基于 jsvm3 源码整理: 从 AST 直驱到字节码:JSVM3 的编译与执行分层(本篇) JSVM2 系列已经说明过 AST 直驱解释器的实现和边界:Babel 解析出 AST,Visitor 前序遍历节点并在遍历中求值。详见《JSVM2:为什么小程序需要一台 JS…

JSVM2:怎么证明一个 JS 引擎是对的

上一篇说明了 ES2015 的裁剪范围。解释器能否用于生产,取决于其在目标输入分布上的验证结果。jsvm2 使用五层测试和编译期工具链验证语义覆盖范围,本文说明该验证体系。 五层测试体系 测试用例在开发过程中逐步增加,每一层对应一类需要验证的问题。 第一层是规范用例。 目录下放了 7 个文件,文件名就是 ES…

JSVM2:表达式求值的现实主义,左值、运算符与 ES2015 裁剪

前几篇说明了骨架、作用域、控制流和函数。本文说明表达式求值。其主要问题有两个:赋值左值是引用而非值;ES2015 包含大量语法,需要按输入范围裁剪。前半部分讨论左值和运算符,后半部分核对 ES2015 的实际支持范围。 左值是引用 这行代码,右边求值得 1,很容易。左边麻烦。在 jsvm2 里,Identifier…

JSVM2:解释器的骨架,visitor、Path 与作用域链

上一篇说明了自研原因和整体链路:源码经 @babel/parser 生成 AST,再由解释器求值。本文展开解释器实现。其分发入口只有 5 行,主要结构是 Path 和 Scope:前者参考 Babel,后者对应 ECMA-262 的环境记录。这套结构的雏形见[2020 年的综述](/post/2020/使用TS…

JSVM2:为什么小程序需要一台 JS 虚拟机

2020 年我写过一篇《使用TS 开发 JS 虚拟机》,介绍用 TypeScript 实现基于 AST 的 JS 解释器。项目在 2021 年 7 月正式立项重写,并于 2022 年 2 月发布 1.0.0。本系列整理其实现过程和已知边界;性能与执行模型问题在 JSVM…

React 18 追踪:Fizz,流式 SSR 引擎

此前的文章主要分析客户端 reconciler。服务端渲染在这几个月也发生了变更:6 月 2 日 76f85b3e 将 Fizz bundle 从 experimental 构建移入 stable 构建(入口当时为 react-dom/unstable-fizz.node);6 月 14 日 dbe3363c…

React 18 追踪:事件系统与 Context 传播

第 6 篇分析 重构时,事件入口仍包含一层 。7 月 12 日的 bfa50f8272(Inline discreteUpdates)移除了这层封装:离散事件回调不再预先修改执行上下文,入口函数直接修改模块级变量。事件系统通过事件优先级与调度器连接。本文同时分析 Context 传播,因为两条路径都使用 lane…

React 18 追踪:useOpaqueIdentifier,SSR 一致的 id 怎么生成

写可访问性标记时经常要生成 DOM id: 和 配对, 指向另一个元素的 id。没有框架支持时,一般手写一个计数器: 这套计数器在纯客户端渲染中可用,但 SSR 会产生不一致。例如服务器渲染时计数器从 0 到 5,HTML 包含 field-0 至 field-5;客户端 hydration 也从…

React 18 追踪:Suspense 的挂起与恢复

7 月 master 中,Suspense 有两项相关改动:7 月 20 日的 9ab90de6 将部分可见性切换逻辑从 Suspense fiber 移到内部的 Offscreen fiber;7 月 13 日的 9090257e6e 修复了渲染出错重试后 未恢复的问题。本文梳理组件 throw 出 promise…

React 18 追踪:自动批处理与交错更新队列

以下组件包含一个按钮;点击后创建 setTimeout,并在回调中连续调用两次 setState: React 17 中点击一次会输出两行 render,两次 setState 分别触发一次完整的渲染和提交。React 18 alpha 的结果取决于挂载入口:createRoot…

React 18 追踪:flushSync 统一同步刷新入口

上一篇介绍了 createRoot 转为正式入口。同一周,master 对同步刷新 API 进行了两次相反的修改:7 月 1 日的两个 commit 将 和 两个内部入口合并到 flushSync,7 月 7 日的 commit 又整体回滚这两个改动,且 commit message…

React 18 追踪:Concurrent 工作循环与时间切片

系列前三篇介绍了 Fiber 数据结构和 Lane 模型,分别说明更新如何表示和选择。本文分析被选中的 lane 如何执行,即工作循环(work loop)层的实现;相关源码为 和独立的 包。参考代码为 master 分支 2021-06-24 的 ,并发相关实现位于 fork。 系列目录 日期 标题 06-0…

使用TS 开发 JS 虚拟机

背景 小程序和低代码平台需要执行动态下发的代码。当时调研到的部分 VM 未完整通过 ES5 或 ES2015 测试用例,不能满足生产环境的兼容性要求,因此实现了自己的 JSVM 引擎。 目前已放到 github 上 :jsvm 2 传送门。 目前除了 WithStatement 语句外,其他 es…

多包仓库的构建与发布工程

本文从单个包上移到工程组织层:22 个包各自使用独立 git 仓库,另有 、、 三个辅助仓库。项目未使用 lerna 或 changesets,meta 仓库以一个 384 行脚本统一管理。参考代码为 meta 仓库 的 c7f2f1d,主要分析 ;构建和测试脚本为 的 60d1bac…

test-builder:测试文档怎么写得像代码

上一篇分析了 的插件装配。本篇分析各包测试使用的基础设施。 的测试中常见如下写法: 函数调用构造文档,字符串中的尖括号标签表示位置,第三个参数表示期望文档。 用 和 实现这套接口,共约一百六十行。本文说明 builder 如何从 schema 生成、标签如何换算为位置、mark builder…

columnresizing:列宽拖拽的实现

、 等命令通过 的格网坐标修改文档结构。列宽拖拽不改变结构,只更新 attr。实现位于两个文件: 处理鼠标事件、拖拽状态和手柄装饰; 的表格 NodeView 将文档中的 同步到 的 元素。参考代码为 的 eb522f2;、 分别为 ffad5d9、ca4c78e。 系列目录 日期 标题 05-1…

表格专题(下):addColumn/mergeCells 等编辑命令

的编辑命令集中在 ,包括加列、删行、合并、拆分和导航等二十余个导出。命令通常先用 将选区换算为矩形,在格网坐标中计算目标位置,再换算为文档位置,并通过 、、 修改文档。本文使用前两篇的 和 分析这些命令。参考代码为 的 eb522f2。 系列目录 日期 标题 05-10 ProseMirror…

表格专题(中):CellSelection,矩形的选区

上一篇介绍了表格 schema 和 :合并单元格被计算为格网,、、 在格网坐标上执行。本篇分析 ,说明矩形单元格选区的表示、渲染和文档修改后的映射。参考代码为 的 eb522f2;、 分别为 ffad5d9、ca4c78e。 系列目录 日期 标题 05-10 ProseMirror…

表格专题(上):表格 schema 与 TableMap

本文开始分析 。合并单元格使行列坐标不能直接对应兄弟节点下标,表格选区是矩形,删除列还需要同步修改多个单元格的 attrs。该包将二维表格结构计算为扁平格网;命令先在格网上计算坐标,再换算为文档位置。 是仅定义表格 schema 的历史包,已由 取代。参考代码为 的 eb522f…

search:查找替换插件

markdown 处理文档与外部文本的双向转换。本文说明 prosemirror-search 提供的编辑器查找替换功能。整个包只有两个源文件:query.ts 负责匹配和替换内容的计算,search.ts 负责插件状态、命令和装饰。对外导出 SearchQuery 类、search…

markdown:文档与 Markdown 的双向转换

changeset 用 span 表示两份文档之间的差异。本文说明文档与 Markdown 文本之间的转换。prosemirror-markdown 做两件事:把文档序列化成 Markdown 字符串,把 Markdown 字符串解析回文档。参考代码是 prosemirror-markdown 的 6b95bfe…

changeset:变更集的计算与展示

collab 处理本地 step 的发送和远端 step 的重整。协作场景还需要汇总一段时间内的修改,标明删除和插入的内容。prosemirror-changeset 提供这一计算:输入是起始文档和一串 StepMap…

collab(上):协作编辑的 rebase 原理

prosemirror-collab 不包含网络实现:没有 WebSocket、HTTP 或定时器。它处理客户端的协作状态和 step 重整。源码只有 src/collab.ts 一个文件,185 行,对外是 collab、sendableSteps、receiveTransaction、getVersion…

menu:菜单栏组件体系

此前的扩展包分别处理按键、命令、撤销、输入规则、文档结构和光标位置,基本不负责界面呈现。prosemirror-menu 是系列中的 UI 包,用于将 commands 体系中的命令组织为工具栏。包不大,src 下三个实现文件:menu.ts 定义菜单元素体系,menubar.ts 负责插件装配,icons.ts…

dropcursor:拖拽时的插入位置指示

gapcursor 处理块间的光标位置。dropcursor 在拖拽内容经过编辑器时标出松手后的插入位置。浏览器原生拖拽反馈通常只显示鼠标箭头上的图标;在文档中拖动图片或从外部拖入文件时,文档内不会显示插入位置。dropcursor 画的就是这个插入点指示。整个包只有 src/dropcursor.ts…

gapcursor:光标落不进去的地方怎么办

块级节点之间没有文本容器时,光标需要落在两个块之间的位置。gapcursor 处理这类位置。整个包 src 下只有两个源文件:src/gapcursor.ts 定义选区类型,src/index.ts 定义插件,合计 230 来行。参考代码是 prosemirror-gapcursor 的 72657d…

schema-list:列表节点与最复杂的一批命令

上一篇说明 schema-basic 将三个列表节点交由 schema-list 提供,本文分析该包。参考代码为 prosemirror-schema-list 的 1501619。整个包只有 ,不足二百七十行,导出 orderedList、bulletList、listItem 三个节点 spec…

schema-basic:官方基础文档结构

上一篇引出 schema-basic。此前的 model 文章说明了 NodeSpec 字段和内容表达式如何编译为匹配自动机,但尚未完整分析一份 schema 实例。prosemirror-schema-basic 提供官方基础结构,example-setup 和多数 demo…

inputrules:「# 空格」变成标题是怎么实现的

上一篇分析 history 的 undo/redo 栈,本文分析 inputrules。该包实现多种 Markdown 风格输入:在空段落输入「# 空格」转换为标题,输入「> 空格」转换为引用块,两个减号转换为破折号,直引号转换为弯引号。参考代码为 prosemirror-inputrules 的 e3e554…

commands:命令的签名约定与组合器

上一篇说明 keymap 如何按按键定位 ,本文分析命令本身。参考代码为 prosemirror-commands 的 52a84a8,整个包只有 ,约八百行。文件包含删除与光标命令、结构命令、参数化命令工厂(wrapIn、setBlockType、toggleMark、autoJoin…

扩展(上):keymap,最小的插件

本文开始分析扩展包。上一篇的最小编辑器已经处理输入、删除和加粗;加粗由按钮直接派发命令,快捷键由官方扩展处理。第一个包是 prosemirror-keymap,参考代码为 d60e24…

browser.ts:浏览器差异补丁集

上一篇的坐标换算已经使用了 、 等分支。view 的输入管线、DOMObserver、选区同步和剪贴板模块也包含类似判断。这些判断条件来自 ,探测逻辑集中定义,补丁保留在对应模块。本篇说明该文件及其相邻的 DOM 工具集 。参考代码是 prosemirror-view 的 ca4c78e。 系列目录 日期 标题 0…

domcoords:屏幕坐标与文档位置的双向换算

上一篇讨论剪贴板。view 模块中还包括坐标换算和浏览器差异补丁,本篇讨论坐标换算。 负责文档位置与屏幕坐标的双向转换,点击定位、方向键移动、拖拽落点和选区滚动都依赖这里。参考代码是 prosemirror-view 的 ca4c78e。 文档模型里的 pos…

clipboard:复制粘贴的序列化与解析

view 模块还包括剪贴板、坐标换算和浏览器差异补丁。本篇讨论剪贴板。选区可能从节点中间切开,直接序列化为 HTML 会丢失结构信息;粘贴的 HTML 可能来自编辑器、Word、网页或终端,结构不受当前 schema 控制。prosemirror-view…

Decoration 体系:不修改文档的视觉标注

上一篇讨论的 NodeView 和 MarkView 用于定制文档节点的渲染。占位符、协作者光标、搜索高亮和选中节点边框不属于文档内容;将它们写入文档会影响序列化结果和协作同步。ProseMirror 使用 Decoration…

NodeView 与 MarkView:自定义节点与标记渲染

上一篇讨论 composition 与 IME:组合期间,编辑器需要避开输入法正在修改的节点。本篇讨论常规渲染路径中的 定制接口。默认情况下,文档节点按 schema 的 规格渲染,DOM 由编辑器管理,ViewDesc…

Composition 与 IME:中文输入法事件的处理

上一篇讨论 state 选区与 DOM 选区的双向同步。本篇讨论中文、日文输入法的 composition 场景。从输入「nihao」到上屏「你好」期间,浏览器处于 composition 状态,候选拼音直接写入 contenteditable…

选区同步:state 选区与 DOM 选区的双向对齐

上一篇讨论内容读回:浏览器修改 DOM 后,readDOMChange 将变化转换为 transaction。选区也同时存在于两处。state.selection 用文档位置表示,anchor 和 head 是文档中的数字;DOM Selection 由浏览器持有,使用节点和偏移表示,即 anchorNode…

input.ts:从 keydown 到 dispatchTransaction 的输入管线

上一篇讨论浏览器修改 DOM 后,DOMObserver 如何将变化读回文档。事件到达时,view 也可以先主动处理。本篇讨论 的事件注册机制、keydown 从进入编辑器到生成 transaction 或被放行的处理路径,以及 对默认按键行为的过滤。参考代码是 prosemirror-view 的 ca4c78e…

DOMObserver 与 readDOMChange:将 DOM 变化读回文档

前两篇讨论文档到 DOM 的方向:ViewDesc 树如何将 state.doc 渲染为 DOM,增量更新如何只重画变化部分。用户在 contenteditable 中输入、删除或用输入法上屏时,浏览器会直接修改 DOM,文档模型不会预先得到通知。view 层需要将这些 DOM 变化读回并转换为 transaction…

ViewDesc(下):增量更新怎么做到只改动的部分

上一篇介绍了 中描述树的静态结构:ViewDesc 类族的职责、children 与文档树的对应以及 docViewDesc 的构建。文档变化后,DOM 需要更新以反映新文档。全量替换 innerHTML 会丢失光标和输入法组合状态,也会清空自定义 node view…

ViewDesc(上):文档到 DOM 的描述树

上一篇分析 EditorView 构造函数时,第 4 步调用 将当前文档渲染到 ,并将结果保存为 。 是定义在 的 ViewDesc 描述树。view 层的渲染、选区、坐标换算和 DOM 读回都依赖这棵树。本文说明其静态结构,包括类族职责、children 数组与文档树的对应、顶层 docView…

state 示例:三个插件的实现

state 阶段已介绍 EditorState 的装配与应用、Selection、Transaction,以及两篇 Plugin 系统文章。本篇通过三个插件示例验证这些接口的使用方式:字符统计使用 StateField,只读开关使用 filterTransaction,选区上报使用 view…

Plugin 系统(上):StateField 与插件状态

上一篇说明了一次状态更新如何由旧 state 应用 transaction 得到新 state。这篇讨论插件数据的存放位置。参考代码是 prosemirror-state 的 ffad5d9,主文件 ,其中导出 PluginSpec、Plugin、PluginKey…

Transaction:Transform 加上状态语义

前两篇介绍了 EditorState 和 Selection。EditorState 中的 getter 每次访问都会创建一个 Transaction,dispatch 的事务均由此创建。本文分析 Transaction 类本身,位于 ,文件两百行出头。它继承 transform 阶段介绍的 Transform…

Selection 体系:四种选区与选区书签

上一篇介绍了 EditorState 的四元组,本文展开其中的 selection 字段。参考代码是 prosemirror-state 的 ffad5d9,主文件为 ,整个文件不到 500 行,包含选区基类、三种内置选区、书签机制和一组位置查找函数。标题中的第四种选区 GapCursor…

Transform 类:构建修改的 API 层

这是 transform 阶段的最后一篇。前面五篇分别讨论了 Step 抽象、ReplaceStep 和 Fitter、StepMap、Mapping,以及 structure.ts 的结构判断函数。这些组件通过 Transform 类()提供给命令层。Transform 依次应用 step…

structure.ts:split/join/lift/wrap 的可达性判断

这是 transform 阶段的第四篇。前几篇介绍了 Step 抽象、ReplaceStep 的内部实现和位置映射,分别说明一步修改如何表示、应用和换算位置。本文讨论另一组判断:当前位置能否分段、两个块能否合并、选中的块能否提升、能否包进引用块。这些判断位于 ;该文件还包含 split、join、lift、wrap…

Mapping:多步映射的链式合并,rebase 的地基

上一篇介绍了 StepMap 如何用三元组数组描述单个 step、映射位置,并通过 recover 值记录被删除位置。recover 的消费方位于同一个文件: 的下半部分定义了 Mapping 类,用它可将零到多张 StepMap 组合起来,使位置依次经过多步修改。recover、StepMap.invert…

StepMap:一步修改怎么映射每个位置

这是 transform 阶段的第三篇。前两篇介绍了 Step 抽象和 ReplaceStep 的实现。step 应用后文档位置会变化,旧选区、装饰和并发修改中记录的位置需要换算到新文档。ProseMirror 中有许多以位置为锚点的对象:选区使用 anchor 和 head,装饰锚定在区间上,step 的 from…

ProseMirror transform(上):Step 的接口与实现

前十二篇已经分析整体结构与 prosemirror-model:文档树、位置编号、切片和解析。model 层的约束是文档不可变,Node 不提供原地修改方法。prosemirror-transform 将键入、粘贴、加粗等操作表示为一个或多个 Step,并依次应用到旧文档以生成新文档。本篇分析 Step…

Node 辅助方法与位置约定

本篇是 model 阶段的第九篇。此前已分析 Node 与 Fragment 的结构、Mark、Schema 与内容表达式、ResolvedPos、Slice 与 replace、DOM 序列化与解析以及 diff。 和 中还有三类常用方法:遍历用的 nodesBetween 一族、校验用的 canReplace…

findDiffStart / findDiffEnd:定位两份文档的差异区间

前十篇已经覆盖 model 层的文档存储(Node、Fragment、Mark)、结构约束(Schema)、位置解析(ResolvedPos)、Slice,以及 DOM 的序列化和解析。本篇分析 与 :两个文件合计不到 70 行,用于定位两份文档的差异区间。参考代码是 prosemirror-model…

DOMParser:parseDOM 规则与 HTML 解析

上一篇分析了 toDOM 如何将文档序列化为 DOM 和 HTML。本篇分析反方向的 :外部 DOM,例如剪贴板 HTML、服务端渲染的初始内容或其他编辑器导出的片段,如何按 schema 的 parseDOM 规则解析回文档。DOMParser 负责规则的存储与匹配,ParseContext…

DOMSerializer:文档怎么变成 DOM 和 HTML

前几篇分析了 model 层的存储和修改:Node 与 Fragment 如何存储文档,Mark 如何挂在文本上,Schema 如何约束结构,位置如何解析为路径,以及 Slice 如何切出和插回内容。本篇转向模型输出,分析文档如何转换为浏览器 DOM 和可复制的 HTML。范围是 ,包含 DOMSerializer…

Slice 与 replace:切一块文档出来再塞回去

上一篇说明了 ResolvedPos 如何将数字位置解析为带深度的路径。本篇分析 与 的 copy、cut、slice、replace:从文档切出内容会得到什么对象,插回内容时 ProseMirror 如何保证结果仍符合 schema。这些操作是复制、粘贴、删除和替换的共同底层,transform…

ResolvedPos:一个数字位置怎么变成路径

前面三篇分析了 model 的静态结构:Node 与 Fragment 组成的树、挂在内联节点上的 Mark,以及约束树形状的 Schema。编辑器使用单个数字表示光标和选区端点,state 的 selection 与 transform 的 step…

跑通一个最小 ProseMirror:先看文档长什么样

前两篇介绍了阅读 ProseMirror 的原因和 22 个包的分工。本文先运行一个编辑器示例,在控制台检查文档 JSON 与一次输入产生的 Transaction。后续阅读 model 和 state 源码时,可以将字段与这些运行结果对应。参考代码版本:prosemirror-example-setup 的 b…

ProseMirror 仓库全景:22 个包怎么分工

上一篇讨论了 contenteditable 的限制和 ProseMirror 的应对方式。阅读源码前,先确认代码的位置。ProseMirror 的核心和扩展分成 22 个独立包,本地工作区根目录下有 22 个 prosemirror-* 目录;每个目录各有一个 git 仓库和一份 package.json…

ProseMirror 源码分析开篇:富文本编辑器到底难在哪

手头的项目需要一个富文本编辑器,需求包括表格、自定义节点和撤销功能。我先在 contenteditable 上直接实现了一版,随后发现同样的操作在不同浏览器中会产出不同的 DOM;从 DOM 读回内容时也要处理许多边界情况,一个 bug 的修复还会影响其他场景。问题在于,我把 DOM…