|
| 1 | +# Change Detection Caveats in Vue 2 |
| 2 | + |
| 3 | +> This page applies only to Vue 2.x and below, and assumes you've already read the [Reactivity Section](reactivity.md). Please read that section first. |
| 4 | +
|
| 5 | +Due to limitations in JavaScript, there are types of changes that Vue **cannot detect**. However, there are ways to circumvent them to preserve reactivity. |
| 6 | + |
| 7 | +### For Objects |
| 8 | + |
| 9 | +Vue cannot detect property addition or deletion. Since Vue performs the getter/setter conversion process during instance initialization, a property must be present in the `data` object in order for Vue to convert it and make it reactive. For example: |
| 10 | + |
| 11 | +```js |
| 12 | +var vm = new Vue({ |
| 13 | + data: { |
| 14 | + a: 1 |
| 15 | + } |
| 16 | +}) |
| 17 | +// `vm.a` is now reactive |
| 18 | + |
| 19 | +vm.b = 2 |
| 20 | +// `vm.b` is NOT reactive |
| 21 | +``` |
| 22 | + |
| 23 | +Vue does not allow dynamically adding new root-level reactive properties to an already created instance. However, it's possible to add reactive properties to a nested object using the `Vue.set(object, propertyName, value)` method: |
| 24 | + |
| 25 | +```js |
| 26 | +Vue.set(vm.someObject, 'b', 2) |
| 27 | +``` |
| 28 | + |
| 29 | +You can also use the `vm.$set` instance method, which is an alias to the global `Vue.set`: |
| 30 | + |
| 31 | +```js |
| 32 | +this.$set(this.someObject, 'b', 2) |
| 33 | +``` |
| 34 | + |
| 35 | +Sometimes you may want to assign a number of properties to an existing object, for example using `Object.assign()` or `_.extend()`. However, new properties added to the object will not trigger changes. In such cases, create a fresh object with properties from both the original object and the mixin object: |
| 36 | + |
| 37 | +```js |
| 38 | +// instead of `Object.assign(this.someObject, { a: 1, b: 2 })` |
| 39 | +this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 }) |
| 40 | +``` |
| 41 | + |
| 42 | +### For Arrays |
| 43 | + |
| 44 | +Vue cannot detect the following changes to an array: |
| 45 | + |
| 46 | +1. When you directly set an item with the index, e.g. `vm.items[indexOfItem] = newValue` |
| 47 | +2. When you modify the length of the array, e.g. `vm.items.length = newLength` |
| 48 | + |
| 49 | +For example: |
| 50 | + |
| 51 | +```js |
| 52 | +var vm = new Vue({ |
| 53 | + data: { |
| 54 | + items: ['a', 'b', 'c'] |
| 55 | + } |
| 56 | +}) |
| 57 | +vm.items[1] = 'x' // is NOT reactive |
| 58 | +vm.items.length = 2 // is NOT reactive |
| 59 | +``` |
| 60 | + |
| 61 | +To overcome caveat 1, both of the following will accomplish the same as `vm.items[indexOfItem] = newValue`, but will also trigger state updates in the reactivity system: |
| 62 | + |
| 63 | +```js |
| 64 | +// Vue.set |
| 65 | +Vue.set(vm.items, indexOfItem, newValue) |
| 66 | +``` |
| 67 | + |
| 68 | +```js |
| 69 | +// Array.prototype.splice |
| 70 | +vm.items.splice(indexOfItem, 1, newValue) |
| 71 | +``` |
| 72 | + |
| 73 | +You can also use the [`vm.$set`](https://vuejs.org/v2/api/#vm-set) instance method, which is an alias for the global `Vue.set`: |
| 74 | + |
| 75 | +```js |
| 76 | +vm.$set(vm.items, indexOfItem, newValue) |
| 77 | +``` |
| 78 | + |
| 79 | +To deal with caveat 2, you can use `splice`: |
| 80 | + |
| 81 | +```js |
| 82 | +vm.items.splice(newLength) |
| 83 | +``` |
| 84 | + |
| 85 | +## Declaring Reactive Properties |
| 86 | + |
| 87 | +Since Vue doesn't allow dynamically adding root-level reactive properties, you have to initialize Vue instances by declaring all root-level reactive data properties upfront, even with an empty value: |
| 88 | + |
| 89 | +```js |
| 90 | +var vm = new Vue({ |
| 91 | + data: { |
| 92 | + // declare message with an empty value |
| 93 | + message: '' |
| 94 | + }, |
| 95 | + template: '<div>{{ message }}</div>' |
| 96 | +}) |
| 97 | +// set `message` later |
| 98 | +vm.message = 'Hello!' |
| 99 | +``` |
| 100 | + |
| 101 | +If you don't declare `message` in the data option, Vue will warn you that the render function is trying to access a property that doesn't exist. |
| 102 | + |
| 103 | +There are technical reasons behind this restriction - it eliminates a class of edge cases in the dependency tracking system, and also makes Vue instances play nicer with type checking systems. But there is also an important consideration in terms of code maintainability: the `data` object is like the schema for your component's state. Declaring all reactive properties upfront makes the component code easier to understand when revisited later or read by another developer. |
| 104 | + |
| 105 | +## Async Update Queue |
| 106 | + |
| 107 | +In case you haven't noticed yet, Vue performs DOM updates **asynchronously**. Whenever a data change is observed, it will open a queue and buffer all the data changes that happen in the same event loop. If the same watcher is triggered multiple times, it will be pushed into the queue only once. This buffered de-duplication is important in avoiding unnecessary calculations and DOM manipulations. Then, in the next event loop "tick", Vue flushes the queue and performs the actual (already de-duped) work. Internally Vue tries native `Promise.then`, `MutationObserver`, and `setImmediate` for the asynchronous queuing and falls back to `setTimeout(fn, 0)`. |
| 108 | + |
| 109 | +For example, when you set `vm.someData = 'new value'`, the component will not re-render immediately. It will update in the next "tick", when the queue is flushed. Most of the time we don't need to care about this, but it can be tricky when you want to do something that depends on the post-update DOM state. Although Vue.js generally encourages developers to think in a "data-driven" fashion and avoid touching the DOM directly, sometimes it might be necessary to get your hands dirty. In order to wait until Vue.js has finished updating the DOM after a data change, you can use `Vue.nextTick(callback)` immediately after the data is changed. The callback will be called after the DOM has been updated. For example: |
| 110 | + |
| 111 | +```html |
| 112 | +<div id="example">{{ message }}</div> |
| 113 | +``` |
| 114 | + |
| 115 | +```js |
| 116 | +var vm = new Vue({ |
| 117 | + el: '#example', |
| 118 | + data: { |
| 119 | + message: '123' |
| 120 | + } |
| 121 | +}) |
| 122 | +vm.message = 'new message' // change data |
| 123 | +vm.$el.textContent === 'new message' // false |
| 124 | +Vue.nextTick(function() { |
| 125 | + vm.$el.textContent === 'new message' // true |
| 126 | +}) |
| 127 | +``` |
| 128 | + |
| 129 | +There is also the `vm.$nextTick()` instance method, which is especially handy inside components, because it doesn't need global `Vue` and its callback's `this` context will be automatically bound to the current Vue instance: |
| 130 | + |
| 131 | +```js |
| 132 | +Vue.component('example', { |
| 133 | + template: '<span>{{ message }}</span>', |
| 134 | + data: function() { |
| 135 | + return { |
| 136 | + message: 'not updated' |
| 137 | + } |
| 138 | + }, |
| 139 | + methods: { |
| 140 | + updateMessage: function() { |
| 141 | + this.message = 'updated' |
| 142 | + console.log(this.$el.textContent) // => 'not updated' |
| 143 | + this.$nextTick(function() { |
| 144 | + console.log(this.$el.textContent) // => 'updated' |
| 145 | + }) |
| 146 | + } |
| 147 | + } |
| 148 | +}) |
| 149 | +``` |
| 150 | + |
| 151 | +Since `$nextTick()` returns a promise, you can achieve the same as the above using the new [ES2017 async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) syntax: |
| 152 | + |
| 153 | +```js |
| 154 | + methods: { |
| 155 | + updateMessage: async function () { |
| 156 | + this.message = 'updated' |
| 157 | + console.log(this.$el.textContent) // => 'not updated' |
| 158 | + await this.$nextTick() |
| 159 | + console.log(this.$el.textContent) // => 'updated' |
| 160 | + } |
| 161 | + } |
| 162 | +``` |
0 commit comments