← 中文资源

简体中文版本

监听文件变化并防抖

通过 notify 接收文件系统事件,用句柄保持监听器存活,并把短时间内的连续事件合并为一批。

阅读英文原文

功能说明

开发服务器、配置热加载和同步工具需要感知文件变化。操作系统通知往往一次修改触发多个事件,例如创建、写入、重命名各一条,编辑器保存还可能先生成临时文件。直接逐条处理会重复构建或读到半写状态。本例把事件收集到通道,主循环记录最后一次事件时间,只有安静超过五百毫秒才认为一批变化结束。

notify 的 watcher 必须在整个监听期间保持存活,一旦被丢弃,底层监听就会停止。WatchHandle 把 watcher 与接收端放在一起,并用下划线前缀字段表明它只为生命周期而存在。演示递归监听当前目录,两秒内没有任何事件时正常退出。

关键代码

watch 创建标准库通道,把发送端移入事件回调,回调忽略错误结果,只转发成功事件。RecommendedWatcher::new 使用默认配置,watch 指定递归模式。主循环用 recv_timeout 每两百毫秒醒来一次,既可处理事件,也能检查静默时间。收到事件就刷新 last 并累加计数;有事件且静默超过五百毫秒时输出批次数量并退出。没有事件且启动超过两秒时输出提示退出,避免演示永久阻塞。

使用注意

编辑器通常采用原子保存,先写临时文件再重命名,因此监听目录可能收到临时文件事件,应过滤扩展名和临时前缀。递归监听大目录会消耗较多文件描述符,应限制深度或使用忽略规则。不同平台的事件类型与合并行为不同,不能假设一定收到写入事件。通道无界,事件暴增时可能积压内存,生产实现应使用有界通道并丢弃重复事件。防抖窗口结束后仍应校验文件是否可读和内容是否稳定。

验证方式

运行脚本后,在另一终端向当前目录写入或修改一个文件,程序应打印若干 raw event,随后在约五百毫秒静默后输出 debounced batch 并退出。不做任何修改时,两秒后输出 no changes observed。连续快速修改多个文件,应只输出一批,且数量反映收到的事件条数。

代码示例

use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::Path;
use std::sync::mpsc::{channel, Receiver};
use std::time::{Duration, Instant};

struct WatchHandle {
    _watcher: RecommendedWatcher,
    events: Receiver<Event>,
}

fn watch(path: &Path) -> notify::Result<WatchHandle> {
    let (sender, events) = channel();
    let mut watcher = RecommendedWatcher::new(
        move |result: notify::Result<Event>| {
            if let Ok(event) = result {
                let _ = sender.send(event);
            }
        },
        Config::default(),
    )?;
    watcher.watch(path, RecursiveMode::Recursive)?;
    Ok(WatchHandle {
        _watcher: watcher,
        events,
    })
}

fn main() -> notify::Result<()> {
    let handle = watch(Path::new("."))?;
    let started = Instant::now();
    let mut last = Instant::now();
    let mut pending = false;
    let mut count = 0;

    loop {
        match handle.events.recv_timeout(Duration::from_millis(200)) {
            Ok(event) => {
                last = Instant::now();
                pending = true;
                count += 1;
                println!("raw event: {:?}", event.kind);
            }
            Err(_) if pending && last.elapsed() >= Duration::from_millis(500) => {
                println!("debounced batch of {count} events ready");
                break;
            }
            Err(_) if !pending && started.elapsed() >= Duration::from_secs(2) => {
                println!("no changes observed");
                break;
            }
            Err(_) => {}
        }
    }
    Ok(())
}