Reactivity system

The simplicity and powerfulness of Vue.js originate from its reactive data binding system. It is designed to be intuitive, and it doesn't require your attention to keep the data and the view in sync. 

Essentially, to archive this ubiquitous reactivity, internally, Vue.js implements a variant of the observer design pattern to collect dependencies of the data and notify watchers when data is changed. During the initialization of a Vue instance, Vue.js makes every property of the data object reactive by using the Object.defineProperty() method to create a getter and a setter function for accessing that property and updating its value respectively. When the render function updates the DOM, it invokes the getter functions of the properties that are used in the template. And for every Vue component instance, Vue.js creates a Render Watcher to collect those properties as the render function's dependencies. And whenever the value of a property needs to be changed, Vue.js will notify the Render Watcher about that and the Render Watcher will trigger the render function to update the DOM. That is basically what reactive means. And the following diagram, which is cited from the official guide of Vue.js, provides a good visual explanation:

Figure 2.2: How Vue.js tracks changes
Source: https://vuejs.org/v2/guide/reactivity.html

Now that you have a basic understanding of its reactivity system, let's take a deep dive into the internal workings of Vue.js to see how it implements this reactivity system. And we will use the Messages App as an example to walk you through this.

Let's start from the root Vue instance, vm. In its options object, we define the following data object and computed object:

data: { messages:[], newMessage:'' },
computed: { addDisabled(){...} }

When Vue.js initializes this instance, it creates vm._data to store the actual data and creates proxies for accessing vm.messages and vm.newMessage by defining getters and setters with Object.defineProperty for these two properties. In this way, when you access vm.messages, you are proxied to vm._data.messages. So, the reactivity of the two properties actually happens in vm._data.

To make vm._data reactive, Vue.js observes it. In other words, Vue.js creates an observer object and assigns that object to vm._data.__ob__ to mark it as being observed. This observer object will walk through all of the properties of vm._data, in our case the messages and newMessage properties. For the newMessage property, Vue.js will simply define a reactive getter and a reactive setter for it because it is a primitive value. The reason they are called reactive getter and reactive setter is that inside the getter function, dependencies are collected, and inside the setter function, watchers are notified of the data change. In this way, the newMessage property becomes observable. On the other hand, for the messages property, besides defining a reactive getter and a reactive setter for it, Vue.js will observe it further because it is an array. Without performing a further observation, changes to the messages array won't be noticed because the reactive getter of the messages property will be invoked only when you access it via vm._data.messages, and its setter will be invoked only when you change it via vm._data.messages = [...] but not via vm._data.messages.push({...}). When Vue.js observes the messages array, it will create another Observer object and assign it to the array object's __ob__ property. And Vue.js also modifies Array.prototype by wrapping the original mutation method, including .push(), .pop(), .shift(), .unshift(), .splice(), .sort(), and .reverse()inside another function in which watchers are notified of the data change. In this way, when you invoke messages.push({...}), the Render Watcher knows that and will trigger a re-render to update the DOM. Because we store objects inside the messages array, besides creating an Observer object for the messages array itself, Vue.js will also observe each message object in the array. It creates an Observer object for each message object in the array. This observer walks through all of the properties of that message object and it finds that the id property, the text property, and the createdAt property are neither an array nor a plain JavaScript object; it will simply define reactive getter and setter functions for them. By now, everyone in the data object of the options are reactive, aka, observable. No matter how complex the structure of the data object is, Vue.js will walk through the entire structure to make sure everything is reactive.

There are two types of changes that Vue.js cannot track. One type of change is inserting an item into an array using the index, for example, vm.messages[itemIndex] = newItemor modifying the length of an array, such as vm.messages.length = newLength. JavaScript doesn't emit any event when you change an array in these two ways. To avoid losing reactivity of the array, use the mutation method instead, for example, vm.messages.push(newItem) and vm.messages.splice(0). The other type of change is adding a new property to an object or deleting a property from an object. For example, adding a replies count to the message object, like this—messageObject.repliesCount = 0, or deleting the createdAt property, like this—delete messageObject.createdAt. Use Vue.set(object, key, value) and Vue.delete(object, key) instead, or the vm.$set(object, key, value) and vm.$delete(object, key) instance methods.

Now, let's see how Vue.js makes the computed property, addDisabledreactive.

How Vue.js handles computed properties is different from how it handles data objects. For addDisabled, it creates a lazy watcher and puts it into vm._computedWatchers, which is a map implemented with a plain JavaScript object. The property's name addDisabled is the key and the watcher object is the value. This watcher is called a computed watcher. It is lazy because it evaluates the addDisabled property only when the render function touches it. Internally, Vue.js stores the value in the watcher's value property after evaluation. For public access, Vue.js creates a getter function for accessing vm.addDisabled. Inside this getter is where the computed watcher evaluates the computed property and collects dependencies.

In our Messages App, the addDisabled property depends on the messages array and the newMessage string. And it is bound to the disabled attribute of the Submit button. When Vue.js initializes the root Vue instance, both the messages array and the newMessage string are empty, and no changes have been made to these two during initialization. Even though Vue.js has created a getter for vm.addDisabled, access is not possible until Vue.js mounts the root instance to the mounting point.

When Vue.js mounts a component, it will invoke the component's render function. And the render function needs to know whether the Submit button should be disabled or not, so it invokes the addDisabled getter function. That's when the watcher starts the evaluation. Before the evaluation begins, the watcher will set itself as the only target of all of the dependencies collected during the evaluation. And during the evaluation, the following user-defined function is invoked:

addDisabled () {
return this.messages.length >= 10 || this.newMessage.length > 50
}

As mentioned earlier, Vue.js defines reactive getters for this.messages and this.newMessage, making them observable. An observable property can let the target watcher know that it is a dependency and needs to be collected. So, after accessing this.messages and this.newMessage, the computed watcher of addDisabled will collect them as dependencies. And after the evaluation, the watcher will no longer be the target of the dependency collection.

Now, let's see how the Submit button gets disabled by Vue.js.

First of all, when Vue.js renders the DOM, it will figure out which parts of the DOM need to be updated. When the application is under initialization, the mounting point is empty, so it will just create all of the elements required for the initial rendering. In our template, we added a data binding to the textarea using the v-model directive, which supports two-way data binding. To archive that, Vue.js will add a listener to monitor the input event of textarea, so that when you type in textarea, the browser emits the input event, and the listener gets invoked with the latest value of textarea.

The listener passes the new value to the setter function of vm.newMessage. Inside that function, two watchers are notified. The first one is the Render Watcher. Internally, Vue.js puts the Render Watcher to vm._watcher. Instead of triggering the render function, the Render Watcher puts itself into the watcher's queue, which the Vue.js scheduler will schedule for a flush for the next DOM update cycle. The second watcher, which is the addDisabled watcher, gets notified. Instead of putting itself into the queue, it changes its flag, dirtyto true and then awaits evaluation. When the scheduler flushes the watchers' queue, the Render Watcher is invoked. It triggers the render function, and the render function, invokes the addDisabled getter function which triggers an evaluation of addDisabled.

Before the length of vm.newMessage exceeds 50, the result of the evaluation is always false. Vue.js finds that nothing needs to be updated in the DOM, so it won't manipulate the DOM. Once you type in the 51st character, the evaluation result is true, and Vue.js sees the difference and updates the Submit button's disabled property to true.

For each computed property, Vue.js will create a separate watcher. And it will tear down the Render Watcher, as well as all of the computed watchers of an instance when it destroys that Vue instance.

Now, let's see how Vue.js processes props of the MessageList component in our Messages App.

As a matter of fact, during the initializing of a Vue instance, Vue.js initializes props first, before the data object and computed properties. Internally, Vue.js creates an _props object to hold the data specified in props and puts it to vm.$children[0]._props. The vm here is the root Vue instance and $children is an internal property that contains the child component of the root instance. In our case, there is only one child component, which is the MessageList component. And when Vue.js creates the MessageList component instance, it knows that this component needs a messages array to be passed in as items, and it stores items to vm.$children[0]._props.items and creates a proxy for accessing vm.$children[0].items. Keep in mind that we look at this from the application level and the vm here references the root Vue instance. Inside the MessageList component, you access the items property using items in the template and this.items in the methods.

Because messages is an array, the value of vm.$children[0]._props.items is actually a reference that points to the place where the messages array's data is stored. If you type in the following expression into Chrome's console, you will see that the result is true:

// In the console, the vm is the root instance,
// and vm.$children[0] is one of MessageList.
vm.$children[0]._props["items"] === vm._data.messages

Vue.js doesn't create an Observer object for vm._props, because the props object should not be modified by the component. And the properties defined in props are considered as read-only. That is, inside the MessageList component, you should not try to modify the data of props. In fact, if you did, Vue.js will throw a warning in the console when the app is not running in production mode. Let's say, accidentally, inside the MessageList component, you change items by replacing it with a new array, like the following:

this.items = [{id: 1, text: 'Hello', createdAt: new Date()}]

If you want to change it in the browser's console, you will need to use the following statement:

vm.$children[0]._props.items = [{id: 1, text: 'Hello', createdAt: new Date()}]
Since we use <script type="module"> in our Messages Apps, let vm = new Vue({...}) means vm is not available in the global scope. In order to inspect vm, you need to change it to window.vm = new Vue({...}).

So, what will happen besides Vue.js throwing a warning after you make the change? First of all, the MessageList component's the Render Watcher will get notified and the UI will get updated. It looks like it works as expected. However, if you type anything into textarea, you will see that the UI restores to the one before the change was made. What happened? Well, Vue.js corrected the mistake. Let's take a look at the warning from Vue.js:

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value, prop being mutated: "items."

This doesn't make sense, does it? On the contrary, it totally makes sense. Here is why. When you assigned the new array to vm.$children[0]._props.items, you invoked the setter function of _props.items. Inside that function, Vue.js created a warning and told _props.itemsHere is the new location of the array. Hold it tight. Someone might ask you for it later. Then, it observed items inside the new array before it notified the watchers that items has been changed. That's all of the things Vue.js did when you assigned that new array to items. The original messages array in the root Vue instance was untouched. So, when you typed in textarea, the render function of the root Vue instance was invoked and it found that the DOM of the <message-list> part was not in sync with the messages data, so it replaced it with a new one that was generated based on the messages data.

Now, let's say that you still want to modify items in the MessageList component. And instead of replacing the entire array, you want to use mutation methods that we mentioned earlier, for example, this.items.splice(0). In this case, Vue.js won't complain about it. And the messages list will disappear in the UI. Also, the render function of the root Vue application won't update the UI with another version of the DOM because, with mutation methods, the change you make will be applied to the messages array itself. So, it seems that using mutation methods to change an array type property of the props of a component is a solid approach. Is that correct?

Not really. In our Messages App, just because it works in that way doesn't mean that it should work in that way. With the items property, the MessageList component expects the parent component to pass the data. From where and how the parent component gets the data, it doesn't know. And there is no need for it to know that. So, how could MessageList know how and where to change the data? All it has is a reference to the messages array. The parent component might change its implementation later to get messages from local storage or a server. Simply using the mutation methods to change the messages array won't preserve the change back to where it is stored. The parent component will refresh the array when it retrieves the data again next time.

Well, what if a few months later, you come back and find that it would make more sense to change the messages array inside the MessageList component? In that case, you should consider moving the code for retrieving the messages array from the parent component to the MessageList component and defining it in the data object instead of using a property.

A property of a component is for passing data down in a one-way direction, from the parent to the child. Also, there are some cases where you use a property for passing the initial value, and you definitely need to modify the data inside the child component. In such a case, you should define a local data property that uses that value passed down as its initial value. Or sometimes, when you only need to change the value when it has been updated, you can use a computed property to make the change so that whenever the source changes, the computed property's value will be updated automatically.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.152.58