In Vue 2, why did `this.items[2] = 'x'` and `this.items.length = 0` leave the view stale, and what did you write instead?
answer
- arrays were patched, not proxied
- seven wrapped mutation methods
- index write bypasses them all
- splice, or Vue.set with an index
basics
~20 sVue 2 saw array changes only through its wrapped mutation methods: push, pop, shift, unshift, splice, sort and reverse. Index assignment and length writes bypass them. Use splice(2, 1, 'x') or this.$set(items, 2, 'x'), and splice(0) or a new array to empty it.
solid answer
~40 sVue 2 reactivity used `Object.defineProperty`, and it did not install accessors on array indexes: doing so would be costly for large lists, could not cover indexes created later, and `length` cannot be made an accessor at all. Instead Vue 2 swapped each observed array's prototype for one whose seven mutation methods (`push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse`) call the real method and then notify watchers. `this.items[2] = 'x'` and `this.items.length = 0` go through none of them, so nothing re-renders. The Vue 2 fixes are `this.items.splice(2, 1, 'x')` or `this.$set(this.items, 2, 'x')`, `this.items.splice(0)` to empty, or assigning a new array, since the `items` key itself has a setter. Vue 3's Proxies detect index and length writes directly.
code
js · 16 lines// Vue 2 Options API
export default {
data() {
return { items: ['a', 'b', 'c'] }
},
methods: {
replaceThird() {
// this.items[2] = 'x' -> silent
this.items.splice(2, 1, 'x') // or this.$set(this.items, 2, 'x')
},
empty() {
// this.items.length = 0 -> silent
this.items.splice(0) // or this.items = []
}
}
}go deeper
Know the rule for Vue 2 arrays: change them with push, splice and friends or replace them; never assign an index or set length and expect the view to follow.
Explain the prototype swap: Vue 2 wrapped seven mutation methods to notify watchers because indexes had no accessors and length cannot be one.
When upgrading, remove splice and Vue.set workarounds safely and catch the reverse trap, array watchers in the Options API that now need deep to fire on mutation.
Treat the array caveats as evidence of what an ES5-only engine cost: method patching and conventions instead of interception, a limitation only Proxies could lift.
## Why arrays were a special case in Vue 2 Vue 2 intercepted state changes with `Object.defineProperty` getters and setters. That works for the known keys of a plain object, but arrays raise two problems: - **`length` cannot become an accessor.** An array's `length` is a non-configurable own property, so no getter/setter can be installed on it. - **Indexes were deliberately left alone.** Defining accessors on every index is expensive for long lists, and it could never cover indexes created later by a longer assignment, so Vue 2 did not convert them. So Vue 2 intercepted the **methods** that change an array instead of its slots. ## The seven wrapped mutation methods When Vue 2 observed an array, it replaced that array's prototype with an object overriding seven methods: `push`, `pop`, `shift`, `unshift`, `splice`, `sort` and `reverse`. Each override: 1. calls the real `Array.prototype` method; 2. makes any newly inserted items reactive (for `push`, `unshift` and `splice`); 3. notifies the array's dependency list, which queues a re-render. Any write that does not go through one of those seven methods, and does not replace the whole array, is invisible. ## What Vue 2 saw and missed | Vue 2 write on reactive `this.items` | Detected? | Why | |---|---|---| | `this.items.push(x)` | Yes | Wrapped mutation method | | `this.items.splice(i, 1, x)` | Yes | Wrapped mutation method | | `this.items = this.items.filter(f)` | Yes | The `items` key's own setter receives a new array | | `this.items[i] = x` | No | Plain index write: no accessor, no method | | `this.items.length = 0` | No | `length` is not an accessor | | `this.items[i].done = true` (element is an object with `done`) | Yes | `done` existed at conversion, so the element's own setter fires | The last row trips people up in interviews: the caveat is about writing the **slot** (the index), not about changing a property of an element that Vue 2 had already converted. ## The Vue 2 fixes - **Replace one element:** `this.items.splice(i, 1, x)`, or `this.$set(this.items, i, x)`, which for arrays uses `splice` under the hood. - **Truncate or empty:** `this.items.splice(newLength)` (or `splice(0)`), or assign a new array: `this.items = []`. - **Filter, map, slice:** non-mutating methods return a new array; assign the result back so the `items` setter fires. ```js // Vue 2 Options API methods: { rename(i, label) { // this.items[i] = label // silent in Vue 2 this.items.splice(i, 1, label) // detected }, clear() { // this.items.length = 0 // silent in Vue 2 this.items.splice(0) // detected } } ``` ## Traps interviewers probe - **`sort()` and `reverse()` are detected.** They mutate in place, but they are two of the seven wrapped methods, so the view follows. - **Non-mutating calls do nothing on their own.** `this.items.filter(f)` returns a new array and leaves `this.items` unchanged; without assigning the result back, there is simply no change to detect. - **Nested arrays follow the same rule.** An array inside an object, or inside another array, is observed the same way, so `this.grid[r][c] = x` is silent too, and `this.grid[r].splice(c, 1, x)` is the fix. - **Adding a key to an element object is still the object caveat.** Writing `this.items[0].newKey = 1` on an element that lacked `newKey` needs `this.$set(this.items[0], 'newKey', 1)`. ## What changed in Vue 3 Vue 3 makes arrays reactive with **Proxies**, whose traps see index assignments and `length` writes, so `items[i] = x` and `items.length = 0` update the view and `Vue.set` was removed. One change runs in the other direction and bites during upgrades: with the Options API `watch` option or `vm.$watch`, a watcher on an array in Vue 3 fires **only when the array is replaced**, not when it is mutated, unless you add `deep` (`deep: 1` in Vue 3.5+, `deep: true` in 3.0-3.4). A Vue 2 watcher on `items` fired on `push`. The migration build reports this as `WATCH_ARRAY`.
- Why did `this.items[0].done = true` update the view in Vue 2 when `this.items[0] = x` did not?Because the element was an object Vue 2 had already converted: `done` existed at conversion, so it had its own getter/setter and the write notified watchers. The caveat covered writing the slot itself, the index, which never had an accessor. Adding a new key to that element object would have been silent again.
- After upgrading to Vue 3, does an Options API `watch` on an array still fire when you call `push`?Not by default. In Vue 3 the `watch` option and `vm.$watch` on an array trigger only when the array is replaced. To also fire on mutation add `deep`: `deep: 1` in Vue 3.5+ fires on replacement and mutation, while `deep: true` (the only choice in 3.0-3.4) also fires on changes nested inside elements. In Vue 2 the same watcher fired on push.
saying these in an interview costs you the question
- Vue 2 detected index assignment like items[0] = x on reactive arrays
- Setting items.length = 0 cleared a reactive list in Vue 2
- Replacing a Vue 2 array with a filtered copy did not update the view
- Vue 2 found array changes by diffing each array on every tick
- Vue 3 still needs splice or Vue.set for index writes