skip to content

In Vue 2, why did `this.items[2] = 'x'` and `this.items.length = 0` leave the view stale, and what did you write instead?

level: juniorimportance: should knowfreq 48%

answer

  1. arrays were patched, not proxied
  2. seven wrapped mutation methods
  3. index write bypasses them all
  4. splice, or Vue.set with an index

basics

~20 s

Vue 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 s

Vue 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
js
// 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

for a junior

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.

for a middle

Explain the prototype swap: Vue 2 wrapped seven mutation methods to notify watchers because indexes had no accessors and length cannot be one.

for a senior

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.

for a principal

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