skip to content

Why did Vue 3 replace Vue 2's Object.defineProperty reactivity with Proxies, which caveats disappeared, and why did that end IE11 support?

level: seniorimportance: should knowfreq 52%

answer

  1. per-key accessors versus whole-object traps
  2. adding, deleting, indexing now visible
  3. eager walk becomes lazy wrapping
  4. cannot be polyfilled in ES5

basics

~20 s

Getter/setters only cover keys that exist when converted, so Vue 2 missed added or deleted keys and array index or length writes. Proxies trap every key, making Vue.set unnecessary, but Proxy cannot be polyfilled, so Vue 3 cannot run on IE11.

solid answer

~50 s

`Object.defineProperty` intercepts one existing key at a time, so Vue 2 had to walk all state eagerly at creation, still missed added and deleted keys and array index or `length` writes, and needed `Vue.set`, `Vue.delete` and patched array methods to compensate. A Proxy wraps the whole object: its `set` trap distinguishes adding a key from changing one, `deleteProperty` sees deletes, and `has`/`ownKeys` track `in` checks and iteration. Nested objects are wrapped lazily on access. So the caveats and the helpers went away. The cost is that a Proxy cannot be emulated in ES5 code, and IE11 has none; Vue 3 targets browsers with native ES2016 support, so teams needing IE11 had to stay on Vue 2. When migrating, also note Vue 3 no longer converts your raw object in place: writes through a raw reference are not tracked.

code

js · 14 lines
js
// Options API, same code in Vue 2 and Vue 3
export default {
  data() {
    return { state: null }
  },
  mounted() {
    const raw = { n: 0 }
    this.state = raw

    raw.n++        // Vue 2: tracked (raw was converted in place)
                   // Vue 3: NOT tracked (this.state is a proxy; raw is untouched)
    this.state.n++ // tracked in both versions
  }
}

go deeper

for a junior

Recall the headline: Vue 3 uses Proxies, so adding keys and writing array indexes just work, and Vue.set is gone; the price was losing IE11.

for a middle

Contrast per-key accessors installed at creation with whole-object traps for set, deleteProperty, has and ownKeys, and name each Vue 2 caveat that disappears.

for a senior

Explain what changes for a migrating codebase: removing $set calls, the raw-reference trap, array watchers needing deep, and why Vue 2.7 still carries the caveats.

for a principal

Discuss the platform tradeoff: a cleaner, lazier engine in exchange for dropping browsers without Proxy, and how a product's browser matrix decides whether Vue 3 is even an option.

## Two ways to intercept property access JavaScript offers two ways to run code when a property is read or written: - **Getter/setter accessors** via `Object.defineProperty` (ES5). They are installed **per key**, on a property that already exists. - **Proxies** (`new Proxy(target, handler)`, ES2015). A proxy wraps **the whole object**, and its traps run for any key, including keys that do not exist yet. Vue's documentation states it directly: Vue 2 used getter/setters exclusively because of browser support limits, while Vue 3 uses Proxies for reactive objects and getter/setters for refs (a ref is an object with a `value` accessor). ## What the defineProperty model cost Vue 2 1. **Added and deleted keys were invisible**, hence `Vue.set`/`this.$set` and `Vue.delete`/`this.$delete`. 2. **Array index and `length` writes were invisible**, hence the patched mutation methods and the `splice` idioms. 3. **Conversion was eager and recursive**: every nested object in `data` was walked at creation, a cost that scaled with state size even for parts never rendered. 4. **Your raw object was modified in place**: Vue 2 installed accessors and a hidden observer on the very object you passed in. 5. **`Map` and `Set` contents were not observed at all.** ## What Proxies changed In Vue 3 the reactive handler traps the operations the old model could not see: - the **`set` trap** records whether a key existed before the write and triggers an *add* or a *set* accordingly, so new keys and array indexes notify dependents; - the **`deleteProperty` trap** triggers on `delete`; - the **`has` and `ownKeys` traps** track `key in obj` checks and key iteration, so a `v-for` over an object's keys re-runs when a key is added; - nested objects are wrapped **lazily**, when first read through the proxy, instead of being walked at creation. | Situation | Vue 2 (getter/setter) | Vue 3 (Proxy) | |---|---|---| | `obj.newKey = 1` | Silent; needs `Vue.set` | Tracked | | `delete obj.key` | Silent; needs `Vue.delete` | Tracked | | `arr[i] = x`, `arr.length = 0` | Silent; needs `splice` | Tracked | | Nested objects | Converted eagerly at creation | Wrapped on first access | | Original object | Mutated in place | Left untouched; you get a proxy | Because the caveats are gone, `Vue.set`, `Vue.delete`, `vm.$set` and `vm.$delete` were removed. The migration build keeps them as plain assignments that warn. ## Why that meant dropping IE11 A Proxy's behaviour cannot be recreated by a polyfill: ES5 code can only install accessors on keys that already exist, which is exactly the Vue 2 model with all its caveats. IE11 ships no Proxy, so a Vue 3 build for it would have needed a second, Vue 2-style reactivity engine with Vue 2's limitations. The Vue team planned such a build and then officially dropped it. Vue 3 supports browsers with native ES2016 support; the docs are explicit that if you must support IE11 you stay on Vue 2, which reached end of life on 31 December 2023. ## What this means during a migration - **`this.$set(obj, k, v)` becomes `obj[k] = v`**, and `this.$delete` becomes `delete`. Keys declared as `null` and object-replacement workarounds can stay; they are harmless. - **Stop mutating raw references.** In Vue 2, after `this.state = raw`, writing `raw.n++` still updated the view because `raw` itself had been converted. In Vue 3, `this.state` holds a proxy of `raw` and `raw` is untouched, so writes through `raw` are not tracked. Always write through `this` or the reactive object. - **Array watchers change the other way**: an Options API `watch` on an array now needs `deep` to fire on mutation. - **Vue 2.7 does not help here**: it backported the Composition API but kept the getter/setter engine, so its caveats remain until you are on Vue 3. When an interviewer asks "why proxies?", the strong answer names the trade on both sides: the Proxy engine removed a whole family of silent bugs and the up-front conversion cost, and in exchange it made native Proxy support a hard requirement. For most products that requirement stopped mattering once IE11 left their browser matrix; for the few that still had to serve it, the answer was to stay on Vue 2 rather than ship Vue 3 with a degraded engine.

  • If Vue 3 is Proxy-based, why are getter/setters still part of its reactivity?
    Vue 3 uses Proxies for reactive objects but implements `ref()` as an object with a `value` getter/setter, which tracks reads and triggers on assignment. That works because a ref has exactly one known key, `value`, so the Vue 2 limitation of existing keys only does not bite. A ref holding an object makes that object deeply reactive through `reactive()`, so its keys get Proxy tracking.
  • Is upgrading to Vue 2.7 enough to get rid of `Vue.set` in a codebase?
    No. Vue 2.7 backported the Composition API, including `ref`, `reactive` and `<script setup>`, but its reactivity still runs on `Object.defineProperty`. Added keys and array index writes are as invisible as in 2.6, so the set/delete helpers are still needed. Only moving to Vue 3 removes the caveats.

saying these in an interview costs you the question

  • Vue 3 switched to Proxies purely for speed; the caveats were unrelated
  • A Proxy polyfill would have let Vue 3 run reactively on IE11
  • Vue 3 still converts the raw object in place like Vue 2 did
  • Vue 2.7 uses Proxies because it backports the Composition API
  • Vue 3 dropped getter/setters from its reactivity entirely