skip to content

In Vue 3, a click handler assigns three different refs that one component renders; how many times does that component re-render, and why?

level: middleimportance: must knowfreq 60%

answer

  1. count the flushes, not the writes
  2. one update job per component
  3. a flag marks it queued
  4. an await splits the batch

basics

~20 s

Once. Each write triggers the component's render effect, but its update job is already marked as queued after the first, so the later triggers are ignored; the single microtask flush re-renders once with all three new values.

solid answer

~40 s

Once. Every write notifies the component's render effect, whose scheduler calls `queueJob` with the component's **update job**. The first call adds the job to the queue, sets a `QUEUED` flag on it and schedules a microtask flush; the second and third calls see the flag and do nothing. When the handler returns, the flush runs the job once and the render function reads all three current values. The batching boundary is the flush, not the handler: if the handler `await`s between writes, the flush can run in between and the component renders twice. Jobs are also sorted by component id, so a parent updates before its children in the same flush.

code

vue · 27 lines
vue
<script setup lang="ts">
import { ref, onUpdated } from 'vue'

const a = ref(0)
const b = ref(0)
const c = ref(0)
let renders = 0
onUpdated(() => console.log('updates so far', ++renders))

function syncHandler() {
  a.value++
  b.value++
  c.value++ // one queued job -> logs once
}

async function asyncHandler() {
  a.value++
  await new Promise(r => setTimeout(r, 10)) // flush runs here
  b.value++ // new batch -> logs a second time
}
</script>

<template>
  <p>{{ a }} {{ b }} {{ c }}</p>
  <button @click="syncHandler">sync</button>
  <button @click="asyncHandler">async</button>
</template>

go deeper

for a junior

Recall that several changes in one handler lead to a single re-render of the component.

for a middle

Explain the update job, the queued flag in queueJob, the microtask flush, and why an await in the handler splits the batch.

for a senior

Use the parent-first ordering and dirty-check to reason about update storms and to explain why a child did or did not re-render.

for a principal

Guide teams to group related writes before awaits and to measure renders with dev tooling instead of guessing where batching applies.

## One job per component Every mounted Vue 3 component has a **render effect** — the reactive effect that runs its render function — and an **update job** bound to it. When any reactive value the last render read changes, the effect's scheduler does not re-render; it calls `queueJob(job)`. `queueJob` is where deduplication happens: 1. If the job already carries the `QUEUED` flag, the call returns immediately. 2. Otherwise the job is inserted into the queue, sorted by its id (the component's uid), the flag is set, and a flush is scheduled if none is pending. 3. The flush is scheduled as a **microtask** (a resolved promise's `then`), so it runs after the current synchronous code — your whole handler — finishes. Three writes in one handler therefore produce three `queueJob` calls and one queued job. The flush runs it once; the render function reads `a`, `b` and `c` as they are at that moment, and one DOM patch follows. Writes that set a ref to the value it already has do not trigger at all. ## Where the batch ends The batch is everything that happens before the microtask flush runs. That is usually one event handler, but not always: | Handler shape | Renders of the component | |---|---| | `a.value++; b.value++; c.value++` | 1 | | a loop writing the same ref 1,000 times | 1 | | `a.value++; await load(); b.value++` | 2 — the flush runs while the handler awaits | | writes in two separate event handlers | usually 2 — each event's task gets its own flush | An `await` hands control back to the event loop, microtasks run, and the pending flush is one of them. Anything written after the `await` starts a new batch. ## Order within a flush The queue is kept **sorted by component uid**. A parent is always created before its children, so it has the smaller uid and is updated first. That has two consequences: - If the parent's new render passes changed props or slots to a child, the parent's patch updates that child directly. When the child's own queued job comes up later, its render effect is no longer dirty and the job does no work. - If the parent's render removes a child entirely, the child's queued job is disposed and skipped. A child whose props did not change is not re-rendered just because its parent was; Vue compares the child's props and slots and skips it. Re-rendering is driven by what each component's own render read. ## Pre-flush jobs in the same queue Watchers with the default flush timing are queued in the same sorted queue with a **pre** flag and the owning component's id, which places each one just before its component's update job. So a watcher reacting to `a` runs once per flush too, and before the component re-renders. (Choosing between flush timings is a watcher question, not a scheduler one.) ## Why interviewers ask The question tests whether a candidate thinks of Vue as re-rendering per assignment. Good answers name the **update job**, the **queued flag** and the **microtask flush**, and know the async caveat. Useful probes: - 'Why does writing in a loop not freeze the page with renders?' — one job, one flush. - 'When does batching not help?' — across an `await`, or across separate tasks. - 'Does updating a parent re-render every child?' — only children whose props or slots changed.

  • In Vue 3, a parent and its child are both queued in the same flush; what happens to the child's job?
    The queue is sorted by component uid, so the parent runs first. If its render passes new props or slots to the child, the child is patched as part of the parent's update; when the child's own job comes up, its render effect is no longer dirty and nothing re-runs. If the parent removed the child, the job is disposed and skipped.
  • Why does setting a ref to the value it already holds not queue an update?
    A ref's setter compares the new value with the old one and only triggers its subscribers when they differ, so no effect is notified and `queueJob` is never called.

saying these in an interview costs you the question

  • Each ref assignment triggers its own immediate re-render
  • Vue batches only inside event handlers it wraps itself
  • An await in the handler has no effect on batching
  • A parent's update re-renders every child regardless of props
  • Deduplication works by diffing vnode trees before rendering