Diff:无处不在的差异计算
从 Git 版本管理到代码审查,从配置变更到数据同步——Diff 是软件开发的基础操作。
| 场景 |
Diff 类型 |
工具 |
| 代码变更 |
行级 Diff |
Git |
| 配置对比 |
键值 Diff |
在线对比工具 |
| JSON 数据 |
结构化 Diff |
json-diff |
| 法律文档 |
语义 Diff |
专用工具 |
| 翻译校对 |
字符级 Diff |
在线对比 |
一、Myers Diff 算法原理
核心思想
Myers 算法寻找将源文本转换为目标文本的最短编辑脚本(SES)——最少插入/删除操作。
源: ABCABBA
目标: CBABAC
编辑脚本:
- A (删除 A)
B (保留 B)
- C (删除 C)
C (保留 C → 插入 C 在前面)
A (保留 A)
B (保留 B)
- B (删除 B)
A (保留 A)
+ C (插入 C)
算法图示
0 1 2 3 4 5 6 7
┌───┬───┬───┬───┬───┬───┬───┬───┐
0 │ → │ ↘ │ │ │ │ │ │ │ A
├───┼───┼───┼───┼───┼───┼───┼───┤
1 │ ↓ │ │ ↘ │ │ → │ ↘ │ │ │ B
├───┼───┼───┼───┼───┼───┼───┼───┤
2 │ │ ↓ │ │ ↘ │ ↓ │ │ ↘ │ │ C
├───┼───┼───┼───┼───┼───┼───┼───┤
3 │ │ │ ↓ │ │ │ ↓ │ │ ↘ │ A
├───┼───┼───┼───┼───┼───┼───┼───┤
4 │ │ │ │ ↓ │ ↘ │ │ ↓ │ │ B
├───┼───┼───┼───┼───┼───┼───┼───┤
5 │ │ │ │ │ ↓ │ │ │ ↓ │ B
├───┼───┼───┼───┼───┼───┼───┼───┤
6 │ │ │ │ │ │ ↓ │ ↘ │ │ A
└───┴───┴───┴───┴───┴───┴───┴───┘
C B A B A C B A
→ = 右移(删除) ↓ = 下移(插入) ↘ = 对角(保留)
时间复杂度
| 情况 |
复杂度 |
说明 |
| 最优 |
O(ND) |
N=文本长度,D=差异数 |
| 最差 |
O((N+M)D) |
完全不同的文本 |
| 典型 |
O(N+D²) |
少量差异 |
二、使用工具库文本对比
操作步骤
- 打开 文本对比工具
- 在左侧粘贴原始文本
- 在右侧粘贴修改后文本
- 查看差异结果:
- 支持字符级差异高亮,精确到每个字符变化
对比模式
| 模式 |
粒度 |
适用场景 |
| 行级 |
按行对比 |
代码变更 |
| 字级 |
按词对比 |
文档修订 |
| 字符级 |
按字符对比 |
精确校对 |
三、JSON Diff 特殊技巧
使用 JSON Diff 工具
- 打开 JSON Diff 工具
- 粘贴两份 JSON 数据
- 查看结构化差异:
- 新增/删除/修改的键
- 值变化(含类型变化)
- 嵌套路径
JSON Diff vs 文本 Diff
// 原始
{ "name": "张三", "age": 25, "tags": ["a", "b"] }
// 修改
{ "name": "张三", "age": 26, "tags": ["a", "c"], "email": "z@test.com" }
| 对比方式 |
结果 |
| 文本 Diff |
按行显示差异,难以理解 |
| JSON Diff |
age: 25→26, tags[1]: "b"→"c", +email: "z@test.com" |
四、统一 Diff 格式
格式规范
--- a/config.json
+++ b/config.json
@@ -1,5 +1,6 @@
{
"name": "my-project",
- "version": "1.0.0",
+ "version": "2.0.0",
"description": "A tool",
- "private": true
+ "private": true,
+ "license": "MIT"
}
| 符号 |
含义 |
--- |
原始文件 |
+++ |
修改文件 |
@@ -a,b +c,d @@ |
差异位置(原始第a行起b行,修改第c行起d行) |
- |
删除的行 |
+ |
新增的行 |
(空格) |
上下文行 |
五、补丁生成与应用
生成补丁
import * as Diff from 'diff';
const oldText = 'Hello World\nGoodbye World';
const newText = 'Hello World\nGoodbye Cruel World';
// 生成补丁
const patch = Diff.createPatch('file.txt', oldText, newText, 'old', 'new');
console.log(patch);
输出:
--- old/file.txt
+++ new/file.txt
@@ -1,2 +1,2 @@
Hello World
-Goodbye World
+Goodbye Cruel World
应用补丁
// 正向应用
const result = Diff.applyPatch(oldText, patch);
// 反向应用(回滚)
const reversed = Diff.applyPatch(newText, Diff.reversePatch(patch));
三向合并
const base = '原始文本';
const branchA = 'A 的修改';
const branchB = 'B 的修改';
const merged = Diff.mergeDiff3(base, branchA, branchB);
// 自动合并,冲突时标记 <<<<<<< / ======= / >>>>>>>
六、实际应用场景
配置文件变更检测
const oldConfig = JSON.parse(fs.readFileSync('config.old.json', 'utf-8'));
const newConfig = JSON.parse(fs.readFileSync('config.new.json', 'utf-8'));
const diff = Diff.diffJson(oldConfig, newConfig);
diff.forEach(part => {
if (part.added) console.log('新增:', JSON.stringify(part.value, null, 2));
if (part.removed) console.log('删除:', JSON.stringify(part.value, null, 2));
});
翻译文件校对
// 对比中英文翻译键的一致性
const zhKeys = Object.keys(flatten(zhTranslation));
const enKeys = Object.keys(flatten(enTranslation));
const missingInEn = zhKeys.filter(k => !enKeys.includes(k));
const missingInZh = enKeys.filter(k => !zhKeys.includes(k));
API 响应回归测试
const expected = await fs.readFile('fixtures/expected.json', 'utf-8');
const actual = await fetch('/api/data').then(r => r.json());
const diff = Diff.diffJson(JSON.parse(expected), actual);
if (diff.some(d => d.added || d.removed)) {
throw new Error('API 响应与预期不符');
}
七、性能优化
| 优化策略 |
效果 |
适用 |
| 限制上下文行数 |
减少输出体积 |
大文件 |
| 哈希预处理 |
跳过相同行 |
超大文件 |
| 分块对比 |
并行处理 |
超长文本 |
| 行级先于字符级 |
减少计算量 |
代码文件 |
// 限制上下文行数
const patch = Diff.createPatch('file', old, new, '', '', { context: 3 });