Reactivity Internals — Series 3

Preview — 3 of 10 questions

Why combine these low-level JS APIs with Vue's reactivity here instead of a plain Map cache?

javascript
import { shallowReactive } from 'vue'

const cache = shallowReactive(new Map())
const registry = new FinalizationRegistry((key) => cache.delete(key))

export function useCachedResource(key, loader) {
  let entry = cache.get(key)
  if (!entry) {
    entry = { value: loader() }
    cache.set(key, entry)
    registry.register(entry, key)
  }
  return entry.value
}
AWeakRef and FinalizationRegistry are required for shallowReactive to work at all — a plain Map cannot be made reactive without them
BFinalizationRegistry runs its callback synchronously the instant an object becomes unreachable, guaranteeing immediate cache cleanup
CThis pattern lets cache entries be garbage-collected once nothing else references them, and cleans the corresponding key out of the reactive cache Map when that happens — a plain Map cache alone would hold every entry forever (a classic memory leak), since a Map's own values keep strong references regardless of whether anything else in the app still needs them
DThis combination is purely stylistic — a plain Map with no special garbage-collection handling would behave identically in every observable way

What does using effect() directly give you that watchEffect() doesn't expose as easily?

javascript
import { effect, reactive } from 'vue'

const state = reactive({ count: 0 })

const runner = effect(() => {
  console.log('count is', state.count)
}, {
  scheduler(job) {
    requestIdleCallback(job)
  },
})
ANothing — effect() and watchEffect() are just two names for the exact same function
Beffect() runs synchronously and immediately on every single reactive property change, with no batching, unlike watchEffect
Ceffect() only works on reactive() objects, never on refs, which is why state.count (not a ref) had to be used here
Deffect() is the low-level primitive watchEffect() (and watch, and computed) are built on top of — it exposes a scheduler option that fully overrides how re-runs are scheduled (here, deferring every re-run to requestIdleCallback instead of Vue's normal microtask-batched default), giving fine-grained control watchEffect's own options don't directly expose. It also returns a raw "runner" function that can be called manually to force an immediate re-run

Do these work as expected on a reactive proxy?

javascript
import { reactive } from 'vue'

const state = reactive({ a: 1, b: 2 })

console.log('a' in state)     // ?
console.log(Object.keys(state)) // ?

delete state.a
console.log(Object.keys(state)) // ?
Areactive()'s Proxy correctly intercepts the has trap (for in) and the ownKeys trap (for Object.keys), so both report accurately: true then ['a', 'b'], and after delete state.a, ['b']. Vue also tracks these operations for reactivity — a computed or watchEffect that reads Object.keys(state) or does 'a' in state correctly re-runs when a key is added or removed later, not just when an existing key's value changes
BBoth in and Object.keys bypass the Proxy entirely and operate on the raw, un-proxied object underneath — they show a before the delete and correctly show it gone after, but neither is tracked reactively
Cin works correctly, but Object.keys always returns an empty array on a reactive Proxy due to how the ownKeys trap is implemented
DNeither operation works on a Proxy at all — both throw TypeError: 'ownKeys' or 'has' trap not supported

Sign up free to play

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