Many front-end JavaScript frameworks (such as Angular, React, and Vue) have their own data-related engines. By understanding relevancy and how it works, you can improve your development skills and use JavaScript frameworks more effectively. In the video and the following article, we built the same type of Reactivity that you see in the Vue source code.

If You’re watching this video instead of reading the article, watch the next video in the series to discuss reactivity and agency with Evan You, creator of Vue.

💡 The Reactivity System

Vue’s responsive system looks amazing when you first see it. Take this simple Vue application:

Somehow, Vue only knows that if the price changes, it should do three things:

  • Update the price values on our web page.
  • Recalculate the expression multiplied by Price * quantity and update the page.
  • Call the totalPriceWithTax function again and update the page.

But wait, you might wonder, how does Vue know what to update when prices change, and how does it keep track of everything?

This is not how JavaScript programming normally works.

If you don’t understand, let’s try to see how regular JavaScript works. For example, if I run this code:

What do you think it prints? Since we are not using Vue, it will print 10.

At Vue, we expect the totals to be updated every time the price or quantity is updated. We want to:

Unfortunately, JavaScript is procedural, not passive, so this doesn’t work in real life. In order for the data to change accordingly, we have to use JavaScript to make things look different.

⚠ ️ problem

We need to save the totals so we can run them again when prices or quantities change.

✅ Solution

First, we need some way to tell our application, “I’m about to run the code, store it, and I may need you to run it at another time.” We will then run the code and run the stored code again if the price or quantity variables are updated.

Notice that we store an anonymous function in the target variable and then call a recording function. Using ES6 arrow syntax I could also write like this:

Notice that we store an anonymous function in the target variable and then call a recording function. Using ES6 arrow syntax I could also write like this:

Recording method:

We are storing the target (in our case {total = price * quantity}), so we can run it later.

This iterates through all the anonymous functions stored in the storage array and executes each of them.

Then in our code, we can:

Easy, right? If you need to read it and try to master it again, the code here is complete. Fyi, if you want to know why, I’ll code this in a specific way.

⚠ ️ problem

We can continue recording targets as needed, but there is a more powerful solution to extend our application. That’s a class that maintains a list of targets that will be notified when we need them to run again.

✅ Solution: Use Class

One way we can begin to solve this problem is to encapsulate this behavior in its own Class, which is a dependent Class that implements the standard programming observer pattern.

So, if we create a JavaScript class to manage our dependencies (which is closer to the way Vue handles things), it might look like this:

Let it run:

It still works, and now our code feels more reliable. The only thing that still feels a little weird is the setup and operation of target ().

⚠ ️ problem

We’ll set up a Dep class for each variable and nicely encapsulate the behavior of creating anonymous functions that need to be monitored for updates. Perhaps the observer function might be designed to deal with this behavior.

(This is just the code above)

We can change it to:

✅ solution: Observer function

In our Watcher feature, we can do a few simple things:

As you can see, the Watcher function takes the myFunc parameter, sets it to our global target property, calls dep.depend () to add the target as a subscriber, calls the target function, and resets the target.

Now when we run the following:

You may be wondering why we implemented target as a global variable instead of passing it into the function we needed. There’s a good reason for this, which will be revealed at the end of our article.

⚠ ️ problem

We have a Dep class, but what we really want is for each variable to have its own Dep. Before we go on, let’s save some data.

Let’s assume that each of our attributes (price and quantity) has its own internal Dep class.

When we run:

Since I accessed the data.price value, I want the Dep class of the price attribute to push our anonymous function (stored in the target) to its subscriber array (by calling dep.depend ()). Because of the access to data.quantity, I also want the Quantity attribute Dep class to push this anonymous function (stored in the target) into its subscriber array.

If I have another anonymous function that calls only data.price, I want to push only to the price attribute Dep class.

When do I want to call dep.notify () on a price subscriber? I want to call them when I set the price. At the end of the article, I want to be able to go to the console and execute:

We need some method to hook a data attribute (such as price or quantity), so we can save the target to our subscriber array when it is accessed, and run the function stored in our subscriber array when it is changed.

✅ Solution: Object.defineProperty ()

We need to know about the object.defineProperty () function, which is simple ES5 JavaScript. It allows us to define getter and setter functions for properties. Before I show you how to use it in a Dep class, let me briefly show you how to use the change function.

As you can see, it only records two lines. However, it doesn’t actually get or set any values, because we overused that feature. Let’s add it back now. Get () expects to return a value, while set () still needs to update a value, so let’s add an internalValue variable to store our current price value.

Now that our GET and set are working, what do you think will be printed to the console?

Therefore, we can be notified when we get and set values. With some recursion, we can run it for all the items in the array

FYI, object.keys (data) returns an array of Object keys.

Now everything has getters and setters, and we see that on the console.

🛠 Putting both ideas together

When a piece of code like this runs and gets the value of the price, we want the price to remember this anonymous function (the target). This way, if the price changes, or is set to a new value, it will trigger the function to run again because it knows that this row depends on it. So you can think of it this way.

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 terms of our Dep Class

Price email exchange (get) => Call dep.depend () to save the current target

Price set => Call dep.notify () on the Price to rerun all targets

Let’s combine these two ideas and finish our final code.

Now let’s see what happens.

Just what we were hoping for! Prices and quantities are indeed responding in real time! As soon as the value of price or quantity is updated, our total code runs again.

This illustration in the Vue documentation should now start to make sense.

Do you see that beautiful purple data circle with getter and setter? It should look familiar! Each component instance has an Observer instance (blue) that collects dependencies from the getter (red line). When the setter is called later, it notifys the observer that the component is being rerendered. Here are some of my own annotated images.

Yeah. Doesn’t it make more sense now.

Obviously, what Vue does is probably more complicated and surprising, but you know the basics now.

⏪ Summary: So what have we learned?

  • How to create a Dep class that collects dependencies (dependencies) and reruns all dependencies (notify).
  • How to create an observer to manage the code we are running that may need to be added as a dependency (target).
  • How to create getters and setters using Object.defineProperty ().