RxJS中timer/interval间隔不准,如何实现精确100ms发射的Observable?
Hey there! I totally get the frustration here—when you set a 100ms interval, you expect it to be consistent, not drifting a few ms each time. Let me break down why this happens and how you can fix it.
Why the Drift Happens
JavaScript runs on a single-threaded event loop. Both RxJS's timer and interval methods don't execute their emissions exactly at the specified time—they queue the next emission to run when the event loop is free. If there's any other work happening (like browser rendering, garbage collection, or other callbacks), your observable's emission gets delayed, leading to those extra milliseconds you're seeing. Your measurement code using Date.now() is correctly capturing this drift.
A Drift-Compensating Observable
To fix this, we can create a custom Observable that tracks the expected emission time and adjusts the next delay to compensate for any drift. Here's a TypeScript implementation:
import { Observable } from 'rxjs'; function preciseInterval(initialDelay: number, targetInterval: number) { return new Observable<number>(observer => { let emissionCount = 0; // Calculate the first expected emission time let nextExpectedTime = Date.now() + initialDelay; const emitNext = () => { const currentTime = Date.now(); // Emit the count observer.next(emissionCount++); // Update the next expected time (add target interval, not current delay) nextExpectedTime += targetInterval; // Calculate how long to wait until the next expected time // Use Math.max(0) to avoid negative delays (if we're already past the expected time) const delay = Math.max(0, nextExpectedTime - currentTime); // Schedule the next emission setTimeout(emitNext, delay); }; // Schedule the first emission const timeoutId = setTimeout(emitNext, initialDelay); // Cleanup function to cancel the timeout when unsubscribed return () => clearTimeout(timeoutId); }); }
How to Use It
Replace your original timer call with this custom observable:
import { take } from 'rxjs/operators'; const obsTime = preciseInterval(0, 100).pipe(take(30)); let t0 = Date.now(); const timeIntervals: number[] = []; obsTime.subscribe({ next: () => { const t1 = Date.now(); if (timeIntervals.length > 0) { timeIntervals.push(t1 - t0); console.log(`Interval: ${timeIntervals[timeIntervals.length - 1]}ms`); } t0 = t1; }, complete: () => { const average = timeIntervals.reduce((sum, interval) => sum + interval, 0) / timeIntervals.length; console.log(`Average interval: ${average.toFixed(2)}ms`); } });
Key Notes
- This approach won't give you perfect precision (nothing in JavaScript can, due to the single-threaded event loop), but it will keep the average interval extremely close to your target 100ms by correcting drift on each emission.
- If you have long-running synchronous code blocking the main thread, even this method will struggle. To avoid this, offload heavy tasks to Web Workers (in browsers) or worker threads (in Node.js).
- In Node.js, you could also use
setImmediatefor faster scheduling, but the core drift-compensation logic remains the same.
内容的提问来源于stack exchange,提问作者Picci

