Understand Vue Reactivity Implementation Step by Step
This post is the wrap up of my talk at WinnipegJS meetup #6.
In this talk, I’m going to talk about reactivity system in vue. But what is reactivity?
To Me, reactivity simply means when one state changed, the side effect of it get updated automatically.
If I explain it in mathematical definition, for example we have a equation that y = x + 1, x is our independent variable and y is our dependent variable. Why y is called dependent variable? Well because its value depends on value of x!
A well known example of reactivity system is spreadsheet.
Instead of calling the renderY function manually we can create a simple function setState that update our state and run the render function.
We got React!
Is there any way we can call the render function automatically when we update the state?
The answer is Yes!
The defineProperty method of Object is a es5+ only feature. This is why vue 2 doesn’t support IE8 and below.
This API allow us to override some default behavior of object, getter and setter for example.
Also this is why we want the state to be an object. And this is also explains why vue have some shenanigans with Arrays. Because this API is not on Arrays.
Let’s call our render function in the setter !
We grab the value of x at beginning then in the getter we just simply return it’s value and in the setter we run our render function after we update the value of x.
It works, but, our code looks so ugly. It only works for property ‘x’ and wont work for nested object. Let’s refactor our code.
Let’s create the observable function. This function just simply loop through all the object keys, and recursively make all nested object observable.
From the demo you can see in this special case, our code still works. However, on state updated, our code can only do one single job, which is rendering Y, and it will always renderY, no matter which state updated. I consider it’s a bug. So we have to figure out a way to make our observable state keep tracking what code is actually depends on which state.
To track code jobs dependency, let’s create a simple Dep class, so we can use it to create dep instance for each objects key.
As you can see this class is quite simple, in the constructor we just create a jobs set, the reason to use Set is we don’t want add duplicate jobs for same key.
The depend method just add current job to the jobs set, and the notify method to run all added jobs.
Notice that we also have a static class variable job to store the current evaluating job.
Now let’s use this Dep class to update our observable function.
For each key we create a new instance of Dep class to track the depended job.
But how do we know if the current job is actually depended on this object property? Right, we just need run this job once (which we already did in previous demos), when the job runs, if the job trying to access this object property, we knows this job is depended on this property. So we can call dep.depend() to add current job to the Dep instance inside the getter.
And then when this property get updated, we want to re-run all the jobs we stored by calling dep.notify() inside the setter.
This maybe a bit confuse to you, but let’s just put everything together in the demo, so you can see how things going on more clearly.
So as you can see, when renderY job runs, the function trying to get state.x, so the getter for x will be run, therefore, the renderY job will be added to the jobs set for x.
In our demo, we only have one job, renderY. Now I want to have another job to renderX, but I don’t want to copy paste the code to set current job; run the job then clear the job.
Again as a good developer, we should keep ourselves DRY.
So, we can create a simple runner function to do all those dirty work for us.
Let’s go back to our codepen add more runners;
By now we just implemented a very basic version of vue reactivity system. The real vue code is much more complex than this, of course. Cause there are so many edge cases to consider, also the code need to be more organized, not like us to write all the code in one file.
But by any chance you want to check out the vue source code, you will see the Dep class, a function to make object reactive and a watcher class basically doing same work as our simple runner function.
Overall this system based on obejct.defineProperty API works fine in most case. But what’s the shorthand of this system? Do you remember I said the object defineProperty api doesn’t work on Array? What Vue did is override some of prototype method like array.push to make the array reactive. Another thing is, we only make those initial object properties reactive, what about new properties you add to the object later? Vue can’t know the new property, so it can’t make it reactive. So everytime you want to add a new reactive property to data object, you gotta use Vue.set API.
However, all those shenanigans won’t be in Vue 3.0 again. Because Vue 3.0 reactivity system will be completely rewritten with Object Proxy. Currently Vue 3.0 is still in active developing phase, we can’t see the source code yet. However, Evan You the creator of Vue explained how it will work in the workshop I attended at Vue conference toronto few months ago. Now I will show you guys here what I learnt from the workshop.
One More Thing…
Proxy is a new global API introduced in ES6. It’s a language level feature which mean It’s not polyfillable it’s also means Vue 3.0 by default wont support IE11 and below. However you can set your build target to use old reactivity system based on object.defineProperty.
For people never used the Proxy API before, you can read some document about it one MDN or your favourite w3school.
Basically the API takes your original object as the first parameters and the second parameter is a object of handlers or some people like call them traps which including set trap, get trap or delete trap and so on… Then the API just return the an proxied object. Note that the return value and the original object are two different object. Because the Proxy API won’t really modify your object directly like the object defineproperty does. This is also means we can not store the object deps inside the object itself. But don’t worry, we will have a solution of that.
First of all Let’s define our traps. In the getter trap we create the deps if not already created in the storage, we call dep.depend to register the current job as dependency just like we did before. Same in the setter trap we update the value in original target then call notify to run the job.
Then we update our observable function to use Proxy with our handlers. We create a WeakMap here to store observed object, so we won’t observe same object multiple times.
Welcome to leave a comment if something I didn’t explained clearly in this post.