If you watch this video instead of reading the article, watch the next video in the series discussing reactivity and proxies with Evan You, the creator of Vue.
💡 The Reactivity System
Vue’s reactivity system can look like magic when you see it working for the first time. Take this simple Vue app:
Somehow Vue just knows that if
price changes, it should do three things:
- Update the
pricevalue on our webpage.
- Recalculate the expression that multiplies
quantity, and update the page.
- Call the
totalPriceWithTaxfunction again and update the page.
But wait, I hear you wonder, how does Vue know what to update when the
price changes, and how does it keep track of everything?
If it’s not obvious to you, the big problem we have to address is that programming usually doesn’t work this way. For example, if I run this code:
What do you think it’s going to print? Since we’re not using Vue, it’s going to print
In Vue we want
total to get updated whenever
quantity get updated. We want:
We need to save how we’re calculating the
total, so we can re-run it when
First off, we need some way to tell our application, “The code I’m about to run, store this, I may need you to run it at another time.” Then we’ll want to run the code, and if
quantity variables get updated, run the stored code again.
We might do this by recording the function so we can run it again.
Notice that we store an anonymous function inside the
target variable, and then call a
record function. Using the ES6 arrow syntax I could also write this as:
The definition of the
record is simply:
We’re storing the
target (in our case the
total = price * quantity
}) so we can run it later, perhaps with a
replay function that runs all the things we’ve recorded.
This goes through all the anonymous functions we have stored inside the storage array and executes each of them.
Then in our code, we can just:
Simple enough, right? Here’s the code in it’s entirety if you need to read through and try to grasp it one more time. FYI, I am coding this in a particular way, in case you’re wondering why.
We could go on recording targets as needed, but it’d be nice to have a more robust solution that will scale with our app. Perhaps a class that takes care of maintaining a list of targets that get notified when we need them to get re-run.
✅ Solution: A Dependency Class
One way we can begin to solve this problem is by encapsulating this behavior into its own class, a Dependency Class which implements the standard programming observer pattern.
Notice instead of
storage we’re now storing our anonymous functions in
subscribers. Instead of our
record function we now call
depend and we now use
notify instead of
replay. To get this running:
It still works, and now our code feels more reusable. Only thing that still feels a little weird is the setting and running of the
In the future we’re going to have a Dep class for each variable, and it’ll be nice to encapsulate the behavior of creating anonymous functions that need to be watched for updates. Perhaps a
watcher function might be in order to take care of this behavior.
So instead of calling:
(this is just the code from above)
We can instead just call:
✅ Solution: A Watcher Function
Inside our Watcher function we can do a few simple things:
As you can see, the
watcher function takes a
myFunc argument, sets that as a our global
target property, calls
dep.depend() to add our target as a subscriber, calls the
target function, and resets the
Now when we run the following:
You might be wondering why we implemented
target as a global variable, rather than passing it into our functions where needed. There is a good reason for this, which will become obvious by the end of our article.
We have a single
Dep class, but what we really want is each of our variables to have its own Dep. Let me move things into properties before we go any further.
Let’s assume for a minute that each of our properties (
quantity) have their own internal Dep class.
Now when we run:
data.price value is accessed (which it is), I want the
price property’s Dep class to push our anonymous function (stored in
target) onto its subscriber array (by calling
data.quantity is accessed I also want the
quantity property Dep class to push this anonymous function (stored in
target) into its subscriber array.
If I have another anonymous function where just
data.price is accessed, I want that pushed just to the
price property Dep class.
When do I want
dep.notify() to be called on
price’s subscribers? I want them to be called when
price is set. By the end of the article I want to be able to go into the console and do:
We need some way to hook into a data property (like
quantity) so when it’s accessed we can save the
target into our subscriber array, and when it’s changed run the functions stored our subscriber array.
✅ Solution: Object.defineProperty()
As you can see, it just logs two lines. However, it doesn’t actually
set any values, since we over-rode the functionality. Let’s add it back now.
get() expects to return a value, and
set() still needs to update a value, so let’s add an
internalValue variable to store our current
Now that our get and set are working properly, what do you think will print to the console?
So we have a way to get notified when we get and set values. And with some recursion we can run this for all items in our data array, right?
Object.keys(data) returns an array of the keys of the object.
Now everything has getters and setters, and we see this on the console.
🛠 Putting both ideas together
When a piece of code like this gets run and gets the value of
price, we want
price to remember this anonymous function (
target). That way if
price gets changed, or is set to a new value, it’ll trigger this function to get rerun, since it knows this line is dependent upon it. So you can think of it like this.
Get => Remember this anonymous function, we’ll run it again when our value changes.
Set => Run the saved anonymous function, our value just changed.
Or in the case of our Dep Class
Price accessed (get) => call
dep.depend() to save the current
Price set => call
dep.notify() on price, re-running all the
Let’s combine these two ideas, and walk through our final code.
And now look at what happens in our console when we play around.
Exactly what we were hoping for! Both
quantity are indeed reactive! Our total code gets re-run whenever the value of
quantity gets updated.
This illustration from the Vue docs should start to make sense now.
Do you see that beautiful purple Data circle with the getters and setters? It should look familiar! Every component instance has a
watcher instance (in blue) which collects dependencies from the getters (red line). When a setter is called later, it notifies the watcher which causes the component to re-render. Here’s the image again with some of my own annotations.
Yeah, doesn’t this make a whole lot more sense now?
Obviously how Vue does this under the covers is more complex, but you now know the basics.
⏪ So what have we learned?
- How to create a Dep class which collects a dependencies (depend) and re-runs all dependencies (notify).
- How to create a watcher to manage the code we’re running, that may need to be added (target) as a dependency.
- How to use Object.defineProperty() to create getters and setters.
If you enjoyed learning with me on this article, the next step in your learning path is to learn about Reactivity with Proxies. Definitely check out my my free video on this topic on VueMastery.com where I also speak with Evan You, the creator of Vue.js.
Originally published at www.vuemastery.com.