Scheduler & Internals

Preview — 3 of 10 questions

Why is Vue 3s Proxy-based reactivity more powerful than Vue 2s Object.defineProperty?

javascript
// Vue 2 limitations with Object.defineProperty:
const vue2State = Vue.observable({ count: 0, user: { name: 'Alice' } })

// ❌ Adding new property — NOT reactive (Vue.set required)
vue2State.newProp = 'value'
// → UI doesn't update; newProp is not observed

// ❌ Deleting property — NOT reactive
delete vue2State.user.name
// → UI doesn't update

// ❌ Array mutations via index — NOT reactive
vue2State.items[0] = 'new'
// → UI doesn't update (push/pop/splice work because Vue patched them)

// Vue 3 with Proxy — all of these work:
const vue3State = reactive({ count: 0, user: { name: 'Alice' } })

vue3State.newProp = 'value'     // ✅ Reactive — Proxy's 'set' trap catches it
delete vue3State.user.name      // ✅ Reactive — Proxy's 'deleteProperty' trap
vue3State.items = []
vue3State.items[0] = 'new'      // ✅ Reactive — Array index assignment tracked

// How Proxy achieves this:
new Proxy(target, {
  get(target, key) { track(target, key); return target[key] },
  set(target, key, value) {
    target[key] = value
    trigger(target, key) // Works for NEW keys too!
    return true
  },
  deleteProperty(target, key) {
    delete target[key]
    trigger(target, key) // Deletion is tracked!
    return true
  },
  has(target, key) { track(target, key); return key in target },
  ownKeys(target) { track(target, 'iterate'); return Reflect.ownKeys(target) }
})
AProxy is faster in all browsers; Object.defineProperty is deprecated
BProxy intercepts any operation on an object (get, set, delete, has, ownKeys) making dynamic property addition and deletion reactive; Object.defineProperty only intercepts predefined properties
CProxy supports TypeScript; Object.defineProperty does not
DProxy uses less memory because it doesn't create getter/setter for every property

How does Vue 3 determine when to invalidate a computed property's cache?

javascript
// Simplified Vue 3 computed internals
class ComputedRefImpl {
  private _value: T
  private _dirty = true // Start dirty — needs first computation
  private effect: ReactiveEffect

  constructor(getter) {
    this.effect = new ReactiveEffect(getter, () => {
      // This scheduler runs when a dependency changes
      if (!this._dirty) {
        this._dirty = true // Mark as dirty — cache is invalid
        triggerRefValue(this) // Notify consumers of this computed
      }
    })
  }

  get value() {
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect.run() // Re-run getter, re-track dependencies
    }
    return this._value
  }
}

// Practical implications:
const count = ref(0)
const name = ref('Alice')
const unrelated = ref('x')

const greeting = computed(() => {
  // During this execution, Vue tracks: count, name
  // 'unrelated' is NOT tracked — the getter never reads it
  return `Hello ${name.value}, count: ${count.value}`
})

greeting.value // Executes getter, tracks count + name, caches result
greeting.value // Returns cached result (no getter execution)
unrelated.value = 'y' // Changes 'unrelated' — NOT tracked by greeting
greeting.value // Still returns cached result — unrelated isn't a dependency!
count.value++  // Triggers the scheduler → _dirty = true
greeting.value // _dirty = true → re-runs getter with new dependencies
ATime-based expiration — computed values older than one render cycle are recomputed
BDependency-based invalidation: Vue tracks which reactive values are accessed during the getter's last execution, and marks the computed as "dirty" when any dependency changes
CReference equality check — if the computed returns the same reference, the cache is valid
DVue compares the computed's return value using JSON.stringify on each render

How does Vue 3's scheduler prevent unnecessary component re-renders when multiple reactive values change synchronously?

javascript
// Vue 3 scheduler internals (simplified)
const queue: SchedulerJob[] = []
let isFlushing = false

function queueJob(job: SchedulerJob) {
  if (!queue.includes(job)) {
    queue.push(job)
    queueFlush()
  }
}

function queueFlush() {
  if (!isFlushing) {
    isFlushing = true
    Promise.resolve().then(flushJobs) // Schedule as microtask
  }
}

function flushJobs() {
  // Sort by component ID (parent before child)
  queue.sort((a, b) => getId(a) - getId(b))

  for (const job of queue) {
    job() // Execute each component's update function
  }

  queue.length = 0
  isFlushing = false
}

// Practical demonstration:
const count = ref(0)
const name = ref('Alice')

// These three changes happen synchronously
count.value++   // → queueJob(MyComponent.update)
count.value++   // → Job already queued — skipped
name.value = 'Bob' // → Job already queued — skipped

// MyComponent.update runs ONCE after this synchronous block
// It sees: count = 2, name = 'Bob'
AVue re-renders immediately on each state change — there is no batching
BVue queues component update jobs in a microtask queue and flushes all updates at once after the current synchronous execution completes
CVue uses requestAnimationFrame to batch updates at 60fps
DVue merges all synchronous state changes into a single change event using a diff algorithm

Sign up free to play

Answer all 10 questions (7 more), see explanations for every answer, and track your score.