文件监听与自动保存是现代代码编辑器的基础能力。当外部工具修改文件、Git切换分支或多个编辑器同时工作时,编辑器需要及时感知变更并做出正确响应。本文详细介绍文件监听机制、自动保存策略、冲突处理与性能优化的完整实现。
文件监听架构
监听服务设计
// 文件监听服务
class FileWatcherService {
constructor() {
this.watchers = new Map(); // uri -> watcher
this.listeners = new Map(); // event -> callback[]
this.debounceTimers = new Map(); // uri -> timer
this.throttleMs = 100;
this.excludes = new Set([
'**/node_modules/**',
'**/.git/**',
'**/dist/**',
'**/__pycache__/**'
]);
}
watch(uri, options = {}) {
const path = uriToPath(uri);
// 检查排除规则
if (this.isExcluded(path)) return;
// 已存在则跳过
if (this.watchers.has(uri)) return;
// 创建原生监听
const watcher = createNativeWatcher(path, {
recursive: options.recursive ?? true,
ignoreInitial: true,
ignored: this.excludes
});
watcher.on('change', (eventType, filename) => {
this.handleFileChange(uri, eventType, filename);
});
watcher.on('error', (err) => {
this.emit('error', { uri, error: err });
});
this.watchers.set(uri, watcher);
}
unwatch(uri) {
const watcher = this.watchers.get(uri);
if (watcher) {
watcher.close();
this.watchers.delete(uri);
}
}
}
原生监听适配
// 多平台文件监听适配
function createNativeWatcher(path, options) {
// Linux: inotify (通过 fs.watch)
// macOS: FSEvents (通过 fs.watch)
// Windows: ReadDirectoryChangesW (通过 fs.watch)
const fs = require('fs');
const watcher = fs.watch(path, {
recursive: options.recursive,
persistent: true,
encoding: 'utf8'
});
const emitter = new EventEmitter();
watcher.on('change', (eventType, filename) => {
if (!filename) return;
const fullPath = join(path, filename);
// 过滤临时文件
if (isTempFile(filename)) return;
emitter.emit('change', {
type: eventType, // 'rename' | 'change'
path: fullPath,
timestamp: Date.now()
});
});
watcher.on('error', (err) => {
emitter.emit('error', err);
});
return {
on(event, cb) { emitter.on(event, cb); },
close() { watcher.close(); }
};
}
function isTempFile(filename) {
const patterns = [
/\.swp$/, // Vim swap
/\.tmp$/, // 临时文件
/~$/, // 备份文件
/\.bak$/, // 备份
/^\./, // 隐藏文件(部分)
/4913$/ // Vim 特殊文件
];
return patterns.some(p => p.test(filename));
}
变更事件处理
防抖与合并
// 文件变更防抖处理
class ChangeEventAggregator {
constructor(delayMs = 100) {
this.delayMs = delayMs;
this.pendingEvents = new Map();
this.timer = null;
}
addEvent(event) {
const key = event.path;
if (this.pendingEvents.has(key)) {
// 合并同文件事件:取最新状态
const existing = this.pendingEvents.get(key);
existing.type = event.type; // 保留最新事件类型
existing.timestamp = event.timestamp;
} else {
this.pendingEvents.set(key, { ...event });
}
// 重置定时器
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => this.flush(), this.delayMs);
}
flush() {
const events = [...this.pendingEvents.values()];
this.pendingEvents.clear();
this.timer = null;
for (const event of events) {
this.emit('change', event);
}
}
}
文件状态检测
// 检测文件变更类型
class FileStateChecker {
constructor() {
this.states = new Map(); // uri -> { mtime, size, hash }
}
async checkChange(uri) {
const path = uriToPath(uri);
const oldState = this.states.get(uri);
try {
const stat = await fsPromises.stat(path);
if (!oldState) {
// 新文件
this.states.set(uri, {
mtime: stat.mtimeMs,
size: stat.size
});
return { type: 'created' };
}
// 检查是否真正变化
if (stat.mtimeMs === oldState.mtime &&
stat.size === oldState.size) {
return { type: 'unchanged' };
}
const oldSize = oldState.size;
oldState.mtime = stat.mtimeMs;
oldState.size = stat.size;
if (stat.size === 0) {
return { type: 'deleted' };
}
return {
type: 'modified',
sizeChanged: oldSize !== stat.size
};
} catch (err) {
if (err.code === 'ENOENT') {
this.states.delete(uri);
return { type: 'deleted' };
}
return { type: 'error', error: err };
}
}
}
自动保存
自动保存策略
// 自动保存管理器
class AutoSaveManager {
constructor(documentManager) {
this.docManager = documentManager;
this.config = {
enabled: true,
delay: 1000, // 延迟1秒
afterDelay: true, // 延迟保存
onFocusChange: true, // 焦点变更时保存
// onWindowChange: true // 窗口切换时保存
};
this.saveTimers = new Map();
}
onDocumentChange(uri) {
if (!this.config.enabled || !this.config.afterDelay) {
return;
}
// 清除之前的定时器
if (this.saveTimers.has(uri)) {
clearTimeout(this.saveTimers.get(uri));
}
// 延迟保存
this.saveTimers.set(uri, setTimeout(async () => {
await this.saveDocument(uri);
this.saveTimers.delete(uri);
}, this.config.delay));
}
async saveDocument(uri) {
const doc = this.docManager.getDocument(uri);
if (!doc || !doc.isDirty) return;
try {
// 先写入临时文件,再原子替换
const path = uriToPath(uri);
const tmpPath = path + '.tmp-save';
await fsPromises.writeFile(tmpPath, doc.getText(), 'utf8');
await fsPromises.rename(tmpPath, path);
doc.markClean();
this.emit('saved', { uri });
} catch (err) {
this.emit('saveError', { uri, error: err });
}
}
onFocusChange() {
if (!this.config.enabled || !this.config.onFocusChange) {
return;
}
// 保存所有脏文档
for (const [uri, doc] of this.docManager.documents) {
if (doc.isDirty) {
this.saveDocument(uri);
}
}
}
}
冲突处理
外部变更冲突
// 冲突检测与处理
class ConflictResolver {
constructor(documentManager) {
this.docManager = documentManager;
this.diskVersions = new Map(); // uri -> disk content hash
}
async handleExternalChange(uri) {
const doc = this.docManager.getDocument(uri);
if (!doc) return;
const diskContent = await readFileContent(uri);
const diskHash = hashContent(diskContent);
// 情况1: 编辑器无未保存修改 → 直接加载磁盘版本
if (!doc.isDirty) {
doc.setText(diskContent);
this.diskVersions.set(uri, diskHash);
return;
}
// 情况2: 编辑器有未保存修改 → 冲突
const lastDiskHash = this.diskVersions.get(uri);
const editorHash = hashContent(doc.getText());
// 检查磁盘内容是否和编辑器内容相同
if (diskHash === editorHash) {
doc.markClean();
this.diskVersions.set(uri, diskHash);
return;
}
// 真正的冲突 → 显示选择界面
this.showConflictDialog(uri, {
editorContent: doc.getText(),
diskContent: diskContent,
options: [
{ label: '保留编辑器版本', action: 'keep' },
{ label: '加载磁盘版本', action: 'load' },
{ label: '对比差异', action: 'diff' }
]
});
}
showConflictDialog(uri, conflict) {
// 弹出用户选择对话框
this.emit('conflict', {
uri,
message: `文件已被外部修改: ${uriToPath(uri)}`,
...conflict
});
}
async resolveConflict(uri, action) {
const doc = this.docManager.getDocument(uri);
const diskContent = await readFileContent(uri);
switch (action) {
case 'keep':
// 保留编辑器版本,下次保存时覆盖磁盘
break;
case 'load':
doc.setText(diskContent);
doc.markClean();
break;
case 'diff':
// 打开对比视图
this.emit('openDiff', { uri, diskContent });
break;
}
}
}
性能优化
大量文件监听
// 大型项目文件监听优化
class OptimizedWorkspaceWatcher {
constructor() {
this.watchers = [];
this.fileEvents = []; // 事件队列
this.maxWatchers = 5000; // 监听上限
}
async startWatching(workspaceRoot) {
// 1. 读取 .gitignore 规则
const ignoreRules = await loadGitignore(workspaceRoot);
// 2. 统计文件数量
const fileCount = await countWatchableFiles(
workspaceRoot, ignoreRules
);
if (fileCount > this.maxWatchers) {
// 3. 超出限制: 使用轮询模式
this.startPolling(workspaceRoot, ignoreRules);
} else {
// 4. 正常: 原生监听
this.startNativeWatch(workspaceRoot, ignoreRules);
}
}
startPolling(root, ignoreRules) {
// 低频率轮询,减轻系统负担
const interval = 5000; // 5秒
let lastScan = new Map();
const poll = async () => {
const currentScan = await scanDirectory(root, ignoreRules);
for (const [path, stat] of currentScan) {
const prev = lastScan.get(path);
if (!prev) {
this.emit('created', { path });
} else if (prev.mtime !== stat.mtime) {
this.emit('changed', { path });
}
}
for (const [path] of lastScan) {
if (!currentScan.has(path)) {
this.emit('deleted', { path });
}
}
lastScan = currentScan;
};
this.pollTimer = setInterval(poll, interval);
poll(); // 立即执行一次
}
}
总结
- 文件监听 - 基于原生API实现,支持inotify/FSEvents/ReadDirectoryChangesW
- 事件防抖 - 合并短时间内的多次变更事件,避免重复处理
- 自动保存 - 延迟保存、焦点变更保存、原子写入确保数据安全
- 冲突处理 - 检测外部修改,提供保留/加载/对比三种解决策略
- 性能优化 - 大型项目自动切换轮询模式,限制监听数量
- 临时文件过滤 - 忽略编辑器临时文件和系统隐藏文件
文件监听与自动保存直接影响编辑器的可靠性和用户体验,需要在实时性与性能之间取得平衡。