Deep dive into responsive principles
Table of Contents
How to track changes
When you pass a normal JavaScript object into a Vue instance as the data
option, Vue will iterate through all the properties of the object and use Object.defineProperty
to convert all of these properties to getter/setter. Object.defineProperty
is a feature in ES5 that cannot be shimmed, which is why Vue does not support IE8 and lower browsers.
These getters/setters are invisible to the user, but internally they allow Vue to track dependencies and notify changes when properties are accessed and modified. It should be noted here that different browsers format getters/setters differently when printing data objects on the console, so it is recommended to install vue-devtools to obtain a more friendly user interface for inspecting data.
Each component instance corresponds to a watcher instance, which will record the "touched" data attributes as dependencies during the component rendering process. Later, when the dependency's setter fires, the watcher is notified, causing its associated component to re-render.
Notes on detecting changes
Due to the limitations of modern JavaScript (and Object.observe
has also been deprecated), Vue cannot detect the addition or deletion of object properties. Since Vue will perform getter/setter conversion on the property when initializing the instance, the property must exist on the data
object in order for Vue to convert it to reactive. For example:
var vm = new Vue({ data:{ a:1 } }) // `vm.a` 是响应式的 vm.b = 2 // `vm.b` 是非响应式的
For already created instances, Vue does not allow dynamic addition of root-level reactive properties. However, you can add reactive properties to nested objects using the Vue.set(object, propertyName, value)
method. For example, for:
Vue.set(vm.someObject, 'b', 2)
You can also use the vm.$set
instance method, which is also an alias for the global Vue.set
method:
this.$set(this.someObject,'b',2)
Sometimes you may need to assign multiple new properties to an existing object, such as using Object.assign()
or _.extend()
. However, new properties added to the object in this way will not trigger an update. In this case, you should create a new object from the original object together with the properties of the object you want to blend into.
// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })` this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
There are also some array-related considerations, which have been mentioned before in List rendering.
Declare reactive properties
Because Vue does not allow dynamic addition of root-level reactive properties properties, so you must declare all root-level reactive properties before initializing the instance, even if it is just a null value:
var vm = new Vue({ data: { // 声明 message 为一个空值字符串 message: '' }, template: '<div>{{ message }}</div>' }) // 之后设置 `message` vm.message = 'Hello!'
If you do not declare it in the data
option message
, Vue will warn you that the render function is trying to access a property that does not exist.
There are technical reasons behind this restriction. It eliminates a type of edge case in the dependency tracking system and also allows Vue instances to work better with the type checking system. But at the same time there is an important consideration in terms of code maintainability: data
objects are like the schema of the component state. Declaring all reactive properties up front makes the component code easier to understand when it is modified in the future or read by other developers.
Asynchronous update queue
Maybe you haven’t noticed yet, Vue is updating the DOM is executed asynchronously. As long as it listens for data changes, Vue will open a queue and buffer all data changes that occur in the same event loop. If the same watcher is triggered multiple times, it will only be pushed into the queue once. This deduplication during buffering is important to avoid unnecessary calculations and DOM operations. Then, on the next event loop "tick", Vue flushes the queue and performs the actual (deduplicated) work. Vue internally tries to use native Promise.then
, MutationObserver
and setImmediate
for asynchronous queues. If the execution environment does not support it, setTimeout( will be used fn, 0)
instead.
For example, when you set vm.someData = 'new value'
, the component will not re-render immediately. When the queue is flushed, the component is updated on the next event loop "tick". Most of the time we don't need to worry about this process, but if you want to do something based on the updated DOM state, it can be a bit tricky. While Vue.js generally encourages developers to think in a "data-driven" way and avoid direct contact with the DOM, sometimes we have to do so. To wait for Vue to finish updating the DOM after the data changes, you can use Vue.nextTick(callback)
immediately after the data changes. This way the callback function will be called after the DOM update is complete. For example:
<div id="example">{{message}}</div>
var vm = new Vue({ el: '#example', data: { message: '123' } }) vm.message = 'new message' // 更改数据 vm.$el.textContent === 'new message' // false Vue.nextTick(function () { vm.$el.textContent === 'new message' // true })
Using the vm.$nextTick()
instance method within a component is particularly convenient because it does not require a global Vue
, and this
in the callback function Will be automatically bound to the current Vue instance:
Vue.component('example', { template: '<span>{{ message }}</span>', data: function () { return { message: '未更新' } }, methods: { updateMessage: function () { this.message = '已更新' console.log(this.$el.textContent) // => '未更新' this.$nextTick(function () { console.log(this.$el.textContent) // => '已更新' }) } } })
Because $nextTick()
returns a Promise
object, so you can use the new ES2016 async/await syntax accomplishes the same thing:
methods: { updateMessage: async function () { this.message = '已更新' console.log(this.$el.textContent) // => '未更新' await this.$nextTick() console.log(this.$el.textContent) // => '已更新' } }