这是一个简单的 React 实现,主要包含以下几个部分:
- 项目架构
src/目录包含核心实现:react.js: 实现 createElement 和 Component 类react-dom.js: 实现 DOM 渲染common.js: 包含通用功能如组件渲染和状态管理
compile/目录包含 JSX 编译器实现:tokenizer.js: 词法分析器,将 JSX 代码转换为 token 流parser.js: 语法分析器,将 token 流转换为 ASTcode_gen.js: 代码生成器,将 AST 转换为 JavaScript 代码
- 编译流程
- JSX 编译过程:
- 词法分析:将 JSX 代码分解为 token,如
<div>被解析为[{type: TokenType.angleBracketLeft}, {type: TokenType.string, value: 'div'}, {type: TokenType.angleBracketRight}] - 语法分析:将 token 流转换为 AST,生成类似
{type: AstType.Element, tagName: 'div', attributes: [], children: []}的结构 - 代码生成:将 AST 转换为
React.createElement()调用
- 词法分析:将 JSX 代码分解为 token,如
- 难点和亮点
- JSX 编译器的实现:
- 支持基本的 JSX 语法,包括标签、属性、子元素
- 支持表达式容器
{},可以在 JSX 中使用 JavaScript 表达式 - 支持组件属性和事件处理
- 虚拟 DOM 的简单实现:
- 使用
createElement创建虚拟 DOM 节点 - 支持组件的生命周期和状态管理
- 实现了基本的事件系统
- 使用
- Diff 算法实现思路 目前项目还没有实现 Diff 算法,如果要实现,建议采用以下方案:
function diff(oldVNode, newVNode) {
// 1. 类型不同,直接替换
if (oldVNode.type !== newVNode.type) {
return {
type: 'REPLACE',
newNode: newVNode
}
}
// 2. 文本节点比较
if (typeof oldVNode === 'string' || typeof newVNode === 'string') {
if (oldVNode !== newVNode) {
return {
type: 'TEXT',
text: newVNode
}
}
return null
}
// 3. 属性比较
const propsPatch = diffProps(oldVNode.props, newVNode.props)
// 4. 子节点比较
const childrenPatch = diffChildren(oldVNode.children, newVNode.children)
return {
type: 'PATCH',
props: propsPatch,
children: childrenPatch
}
}
// 属性比较
function diffProps(oldProps, newProps) {
const patches = {}
// 检查更新和新增的属性
for (const key in newProps) {
if (oldProps[key] !== newProps[key]) {
patches[key] = newProps[key]
}
}
// 检查删除的属性
for (const key in oldProps) {
if (!(key in newProps)) {
patches[key] = null
}
}
return patches
}
// 子节点比较
function diffChildren(oldChildren, newChildren) {
const patches = []
// 使用最长公共子序列优化列表差异
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
patches[i] = diff(oldChildren[i], newChildren[i])
}
// 处理新增的节点
for (let i = commonLength; i < newChildren.length; i++) {
patches[i] = {
type: 'ADD',
node: newChildren[i]
}
}
// 处理需要删除的节点
if (oldChildren.length > newChildren.length) {
patches.push({
type: 'REMOVE',
from: newChildren.length,
to: oldChildren.length
})
}
return patches
}
这个 Diff 算法实现了以下特性:
-
节点类型比较:不同类型节点直接替换
-
文本节点优化:文本节点直接比较内容
-
属性比较:检测属性的更新、新增和删除
-
子节点比较:使用最长公共子序列优化列表差异
-
支持节点的增加、删除、更新和替换操作
你提到的这个 Diff 算法确实是 React 等框架中虚拟 DOM Diff 的核心思路,但实际生产级的实现会更复杂。我来详细解释真正的 Diff 算法原理,以及"最长公共子序列"的概念。
1. 真正的 React Diff 算法核心
React 的 Diff 算法基于两个核心假设:
- 相同类型的组件产生相似的树结构:如果组件类型相同,则其 DOM 结构也大概率相似
- Key 稳定性:使用
key可以标识哪些元素是稳定的、可复用的
实际 Diff 过程分为三个层次:
(1) 树差异(Tree Diff)
React 采用层级比较(只比较同层节点),因为:
- 跨层级移动的操作在实际开发中非常罕见
- 如果发现某节点不存在,会直接销毁整个子树
// 伪代码
function diffTrees(oldTree, newTree) {
if (!oldTree || !newTree) {
return { type: 'REPLACE', newTree }
}
if (oldTree.type !== newTree.type) {
return { type: 'REPLACE', newTree }
}
// 继续比较属性和子节点...
}
(2) 组件差异(Component Diff)
- 如果是相同组件:更新 props 并递归 diff 子节点
- 如果是不同组件:直接替换整个组件
(3) 元素差异(Element Diff)
这才是"最长公共子序列"发挥作用的地方。
2. 最长公共子序列(LCS)详解
当比较子节点列表时,直接按索引比较会导致大量不必要的 DOM 操作。例如:
// 旧列表
[A, B, C, D]
// 新列表
[A, D, B, C]
如果简单按索引比较:
- 比较 B ≠ D → 替换
- 比较 C ≠ B → 替换
- 比较 D ≠ C → 替换 这样需要 3 次 DOM 操作。
但用 LCS 算法可以发现:
- 最长公共子序列是 [A, B, C]
- 只需要将 D 移动到 B 前面(1 次 DOM 操作)
3. React 的实际实现
React 采用了一种启发式算法(没有使用标准的 LCS 算法,因为 O(n³) 复杂度太高):
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
// 1. 第一次遍历:查找不需要移动的节点(key 匹配)
// 2. 第二次遍历:处理剩余的节点
// 3. 生成操作标记:插入、移动、删除
}
具体步骤:
- 遍历新列表,用
key查找可复用的节点 - 建立
lastIndex变量,记录当前访问过的旧节点最大索引 - 如果找到可复用节点且其旧索引 < lastIndex → 需要移动
- 剩下的旧节点标记为删除,新节点标记为插入
4. 为什么不用标准 LCS?
虽然 LCS 能产生最优解,但:
- 时间复杂度 O(n³) 太高(n=1000 时,需要 10 亿次操作)
- DOM 操作本身比 JS 计算更昂贵
- 启发式算法在 99% 的场景下足够高效
5. 现代框架的优化
现代框架如 Vue3、SolidJS 等做了更多优化:
- 预处理:跳过静态节点比较
- 区块树(Block Tree):将动态内容标记为区块,只比较区块内节点
- 编译时优化:在编译时分析模板的更新模式
代码示例:更接近真实的 Diff
function reconcileChildren(oldChildren, newChildren) {
const patches = [];
const oldMap = createKeyToOldIdxMap(oldChildren);
let lastPlacedIndex = 0;
for (let i = 0; i < newChildren.length; i++) {
const newChild = newChildren[i];
const oldIdx = oldMap[newChild.key];
if (oldIdx === undefined) {
patches.push({ type: 'INSERT', node: newChild, afterNode: null });
} else {
const oldChild = oldChildren[oldIdx];
if (shouldUpdate(oldChild, newChild)) {
patches.push({
type: 'UPDATE',
oldNode: oldChild,
newNode: newChild
});
}
if (oldIdx < lastPlacedIndex) {
patches.push({ type: 'MOVE', node: newChild, afterNode: null });
}
lastPlacedIndex = Math.max(lastPlacedIndex, oldIdx);
}
}
// 标记删除未使用的旧节点
oldChildren.forEach(child => {
if (!newChildren.some(c => c.key === child.key)) {
patches.push({ type: 'REMOVE', node: child });
}
});
return patches;
}
关键点:
- 使用
key建立旧节点索引图 lastPlacedIndex跟踪是否需要移动- 四种操作类型:INSERT、UPDATE、MOVE、REMOVE
总结对比
| 特性 | 你的示例 Diff | 真实 React Diff |
|---|---|---|
| 时间复杂度 | O(n²) | O(n) |
| 移动检测 | 无 | 使用 lastIndex |
| Key 优化 | 无 | 关键优化手段 |
| 操作类型 | 基础 | 更精细 |
| 跨层级移动 | 不支持 | 自动处理 |
理解这些概念后,你会明白为什么 React 要求给列表项添加稳定的 key,以及为什么有时直接修改数组会导致性能问题。