Reactivity is automatic synchronization between the state and the DOM. That's what the view libraries like Vue and React try to do in their core. They do that in their own ways.
I see Vue's reactivity system as being two fold. One side of the coin is the DOM update mechanism. Let's look into that first.
Let's say you have a component with a template like:
<template>
<div>{{ foo }}</div>
</template>
<script>
export default {
data() {
return {foo: 'bar'};
}
}
</script>
This template gets converted into render function. This happens during build time using vue-loader. The render function for the template above looks something like:
function anonymous(
) {
with(this){return _c('div',[_v(_s(foo))])}
}
Render function runs on the browser and when executed returns a Vnode (virtual node). A virtual node is just a simple JavaScript object that represents the actual DOM node, a blueprint for the DOM node. The above render function when executed would return something like:
{
tag: 'div',
children: ['bar']
}
Vue then creates actual DOM node from this Vnode blueprint and places it into the DOM.
Later, let's say the foo
's value changes and somehow the render function runs again. It will give a different Vnode. Vue then diffs the new Vnode with the old one and patches only the necessary changes required into the DOM.
This gives us a mechanism to update the DOM efficiently taking the latest state of things for a component. If every time the render function of a component gets called when any of its state (data, props etc) changes, we have the full reactivity system.
That's where the other side of Vue's reactivity coin comes in. And that is the reactive getters and setters.
This will be a good time to understand Object.defineProperty API if you are not already aware of that. Because Vue's reactivity system relies on this API.
TLDR; it allows us to override an object's property access and assignment with our own getter and setter functions.
When Vue instantiates your component, it walks through all the properties of your data and props and redefines them using Object.defineProperty
.
What it actually does is, it defines getters and setters for each of the data and props properties. By doing so, it overrides the dot access (this.data.foo) and the assignment (this.data.foo = someNewValue) of that property. So whenever these two actions occur on that property, our overrides get invoked. So we have a hook to do something about them. We will get back to this in a bit.
Also, for each property a new Dep() class instance is created. It's called Dep
because each data or props property can be a dependency to the component's render function.
But first, it's important to know that each component's render function gets invoked within a watcher. So a watcher has an associated component's render function with it. Watcher is used for other purposes as well, but when it is watching a component's render function, it is a render watcher. The watcher assigns itself as the current running watcher, somewhere accessible globally (in Dep.target static property), and then runs the component's render function.
Now we get back to the reactive getters and setters. When you run the render function, the state properties are accessed. E.g. this.data.foo
. This invokes our getter override. When the getter is invoked, dep.depend()
is called. This checks if there is a current running watcher assigned in Dep.target
, and if so, it assigns that watcher as the subscriber of this dep object. It's called dep.depend()
because we are making the watcher
depend on the dep
.
_______________ _______________
| | | |
| | subscribes to | |
| Watcher | --------------> | Dep |
| | | |
|_____________| |_____________|
Which is the same as
_______________ _______________
| | | |
| Component | subscribes to | it's |
| render | --------------> | state |
| function | | property |
|_____________| |_____________|
Later, when the state property gets updated, the setter is invoked and the associated dep object notifies its subscribers about the new value. The subscribers are the watchers which are render function aware and that's how the components render function gets invoked automatically when its state changes.
This makes the reactivity system complete. We have a way to call a component's render function whenever its state changes. And we have a way to efficiently update the DOM once that happens.
This way Vue has created a relationship between a state property and a render function. Vue knows exactly which render function to execute when a state property changes. This scales up really well and basically removes a category of performance optimization responsibility from the hands of developer. Devs don't need to worry about over rendering of components no matter how big the component tree. To prevent this, React e.g. provides PureComponent or shouldComponentUpdate. In Vue, this is just not necessary since Vue knows exactly which component to re-render when any state changes.
But now that we know how Vue makes things reactive, we can think of a way to optimize things a bit. Imagine you have a blog post component. You get some data from the backend and show them on the browser using Vue component. But there is no need for the blog data to be reactive because it most likely won't change. In such situation, we can tell Vue to skip making such data reactive by freezing the objects.
export default {
data: () => ({
list: {}
}),
async created() {
const list = await this.someHttpClient.get('/some-list');
this.list = Object.freeze(list);
}
};
Oject.freeze among other things disables the configurability of an object. You cannot redefine the properties of that object again using Object.defineProperty
. So Vue skips the whole reactivity setup work for such objects.
Besides, going through the Vue source code yourself, there are two extremely good resources available on this topic:
- Vue Mastery's Advanced component course
- FrontendMaster's Advanced Vue.js Features from the Ground Up by Evan You
If you are curious about the internals of a simple virtual DOM implementation, check out the blog post by Jason Yu.
Building a Simple Virtual DOM from Scratch