In RxJS, how do asyncScheduler, asapScheduler and queueScheduler differ in when they run a task scheduled with zero delay?
answer
- timer, microtask, trampoline
- which one runs synchronously
- delay above zero changes two of them
- time operators pick one by default
basics
~20 sWith zero delay, RxJS queueScheduler runs the task synchronously (queuing nested tasks until the current one ends), asapScheduler runs it as a microtask after the current code, and asyncScheduler runs it on a timer, after pending microtasks.
solid answer
~40 s`queueScheduler` is a **trampoline**: a zero-delay task runs synchronously, right when it is scheduled, but a task scheduled from inside a running queue task waits until the current one finishes, so recursion does not grow the call stack. `asapScheduler` schedules on the **microtask** queue, the one promises use, so the task runs after the current synchronous code but before timers. `asyncScheduler` uses a **timer** (`setInterval` under the hood), so the task runs later still. With a delay above zero, `queueScheduler` and `asapScheduler` both fall back to `asyncScheduler`'s timer behaviour. By default RxJS uses no scheduler at all for synchronous creation functions, and `asyncScheduler` for time-based operators such as `interval`, `timer`, `delay` and `debounceTime`. Passing a scheduler to `of` or `from` is deprecated in RxJS 7; `scheduled(input, scheduler)` replaces it.
go deeper
Recall that of() emits synchronously while interval and timer run on asyncScheduler, and that schedulers exist to control when work runs.
Explain the zero-delay behaviour of queue, asap and async, predict the ordering of mixed tasks, and name the scheduled() replacement for the deprecated argument.
Choose a scheduler deliberately, for instance asap to defer a synchronous emission before render, and replace real time with virtual time in tests.
Judge when explicit scheduling is justified at all, since most application code should rely on RxJS defaults and keep timing concerns out of domain logic.
## What a scheduler is in RxJS An RxJS **scheduler** decides **when** a piece of work runs. It has three parts: a queue of tasks, an **execution context** (run now, in a microtask, on a timer, before the next repaint) and a **clock** exposed as `now()`. Operators that deal with time or concurrency accept an optional scheduler argument, usually the last one, and the static `schedule(work, delay, state)` method lets you use one directly. ## The three general-purpose schedulers | Scheduler | Zero-delay task runs | Delay above zero | Typical use | |---|---|---|---| | `queueScheduler` | synchronously, as a trampoline | like `asyncScheduler` | iteration, recursive scheduling | | `asapScheduler` | in a microtask after the current code | like `asyncScheduler` | making a synchronous source asynchronous as early as possible | | `asyncScheduler` | on a timer (`setInterval`) | on a timer | anything measured in time | ### queueScheduler, the trampoline - Scheduling a zero-delay task when no queue task is running executes it **immediately and synchronously**. - Scheduling from **inside** a running queue task pushes the new task onto a queue; it runs after the current task returns. - The result is that a recursively rescheduled task runs as a flat loop, `before 3, after 3, before 2, after 2`, instead of nesting `before 3, before 2, before 1, after 1...`. That prevents stack growth in recursive scheduling. ### asapScheduler, the microtask - A zero-delay task is pushed to the scheduler's queue and a single microtask is requested (RxJS uses a resolved `Promise` for this), which flushes every queued asap task. - It therefore runs after the currently executing synchronous code, **before** any timer callback. The RxJS docs call it the best candidate for "deferring" work that people used to do with `setTimeout(fn, 0)`. - Tasks already queued on `asapScheduler` run first, so it is not a guarantee of being next. ### asyncScheduler, the timer - Every task is backed by a timer, even with zero delay, so it runs only after the current macrotask and all pending microtasks. - It is the **default** for every time-based operator: `interval`, `timer`, `delay`, `debounceTime`, `throttleTime`, `auditTime`, `sampleTime`, `bufferTime`, `timeout`. ## Ordering in practice ```ts import { asapScheduler, asyncScheduler, queueScheduler } from 'rxjs'; asyncScheduler.schedule(() => console.log('async')); asapScheduler.schedule(() => console.log('asap')); queueScheduler.schedule(() => console.log('queue')); console.log('sync'); // queue, sync, asap, async ``` `queue` prints first because it ran during its own `schedule()` call; `asap` beats `async` even though `async` was scheduled first. ## Defaults: the principle of least concurrency RxJS picks the scheduler that introduces the least concurrency that still does the job: 1. **No scheduler** for creation functions that emit a finite set of values: `of`, `from` over an array, `range`. They emit synchronously during `subscribe()`. 2. **`asyncScheduler`** for anything involving time. 3. You opt into a different scheduler explicitly, most often only in tests or for animation. ## The RxJS 7 deprecation The `scheduler` argument of `of`, `from`, `merge`, `concat`, `combineLatest`, `startWith` and `endWith` has been deprecated since 6.5 and is scheduled for removal in version 8. The replacement is the `scheduled()` creation function: `scheduled([1, 2, 3], asyncScheduler)` instead of `of(1, 2, 3, asyncScheduler)`. Time-based creation functions such as `interval` and `timer` keep their scheduler argument. ## The clock is part of the scheduler Each scheduler exposes `now()`, and time-based operators measure time with **their** scheduler's clock rather than reading the wall clock themselves. That is what makes schedulers swappable: pass a virtual-time scheduler to `debounceTime` or `delay` and those operators run against virtual time, with scheduled tasks executed synchronously in time order. The same design explains why `asapScheduler` and `queueScheduler` can fall back to `asyncScheduler` for positive delays: they extend its timer machinery and only change what happens at zero delay. ## When you would actually choose one - **`asapScheduler`**: to make a synchronous emission arrive asynchronously but before the next render or timer, without the delay of a timer. - **`queueScheduler`**: for recursive or iterative scheduling that should not grow the stack. - **`asyncScheduler`**: explicitly only when you need to override a default, for example to pass it to `observeOn`. - In tests, a virtual-time scheduler replaces all of them so timing can be asserted without waiting.
- Why is queueScheduler described as a trampoline, and what problem does it solve?A task scheduled from inside a running queue task is not executed recursively; it is queued and run after the current task returns. Recursive rescheduling therefore becomes a flat loop instead of a deepening call stack, which prevents stack overflow in iterative or recursive scheduling.
- Which scheduler do RxJS time-based operators use when you pass none?`asyncScheduler`. `interval`, `timer`, `delay`, `debounceTime`, `throttleTime`, `auditTime`, `sampleTime`, `bufferTime` and `timeout` all default to it, and accept a different scheduler as their last argument, which is how virtual-time tests or `animationFrameScheduler` are plugged in.
Think of three ways to answer a colleague's request: queueScheduler handles it now but finishes the current sentence before taking the next request, asapScheduler writes it at the top of today's to-do list, and asyncScheduler books a meeting for later.
saying these in an interview costs you the question
- queueScheduler defers every task to a later tick, like setTimeout.
- asapScheduler ignores any delay and always runs the task as a microtask.
- asyncScheduler with zero delay runs before pending promise callbacks.
- Without an explicit scheduler, of(1, 2, 3) emits asynchronously.
- Passing a scheduler to of() is the recommended way to make it asynchronous in RxJS 7.