preface

At the heart of Vue.js is a set of “responsive systems”.

“Responsive” means that when data changes, Vue notifies the code that uses the data. For example, if data is used in a view rendering, the view is automatically updated when the data changes.

To take a simple example, for templates:

<div id="root">{{ name }}</div>
Copy the code

Create a Vue component:

var vm = new Vue({
  el: '#root'.data: {
    name: 'luobo'}})Copy the code

After the code is executed, the corresponding position on the page will display: Luobo.

If you want to change the name displayed, just do:

vm.name = 'tang'
Copy the code

This will display the modified name on the page without the need to manually modify the DOM update data.

Next, let’s take a closer look at the data responsiveness principle of Vue and figure out the implementation mechanism of responsiveness.

The basic concept

The core mechanism of Vue’s responsiveness is the observer pattern.

Data is the observed party, and when changes occur, all observers are notified so that the observers can respond, for example, by re-rendering and updating the view.

We call the observer that depends on the data a watcher, so this relationship can be expressed as:

data -> watcher
Copy the code

Data can have multiple observers. How do you record this dependency?

Vue records this dependency by creating a DEP object between Data and Watcher:

data - dep -> watcher
Copy the code

The structure of the DEP is very simple. In addition to uniquely identifying the attribute ID, another attribute is the subs used to record all observers:

  • id - number
  • subs - [Watcher]

Now watcher.

Watcher’s observation object in Vue is exactly an evaluation expression, or function. This expression, or function, is evaluated or executed in the context of a Vue instance. And in doing so, it uses data, which watcher relies on. The property used to record the dependency is DEPS, which corresponds to an array of DEP objects for all the dependent data. The expression or function is eventually recorded in the getter property as an evaluation function, and the result of each evaluation is recorded in the value property:

  • vm - VueComponent
  • deps - [Dep]
  • getter - function
  • value - *

In addition, there is an important property cb, which records that the callback function is called when the getter returns a value different from the current value:

  • cb - function

Let’s use examples to sort out the relationship between data, DEP and Watcher:

var vm = new Vue({
  data: {
    name: 'luobo'.age: 18}})var userInfo = function () {
  return this.name + The '-' + this.age
}

var onUserInfoChange = function (userInfo) {
  console.log(userInfo)
}

vm.$watch(userInfo, onUserInfoChange)
Copy the code

The above code first creates a new Vue instance object vm, containing two data fields: name and age. For each of these fields, Vue creates a corresponding DEP object for logging the Watcher that depends on the data.

We then define an evaluation function, userInfo, which is executed in the context of the corresponding Vue example, that is, this corresponds to the VM when executed.

The onUserInfoChange callback simply prints out the new value obtained by the new Watcher, which is generated by userInfo after execution.

With vm.$watch(userInfo, onUserInfoChange), the vm, getter, and CB are integrated to create a new Watcher. After the creation, Watcher has logged the dependencies internally. In watcher. Deps, the DEP object corresponding to the VM name and age is logged (because userInfo uses these two data).

Next, we modify the data:

vm.name = 'tang'
Copy the code

After execution, the console will print:

tang - 18
Copy the code

Also, changing the age value will eventually trigger onUserInfoChange to print a new result.

Here’s a simple diagram to illustrate the above relationship:

vm.name -- dep1
vm.age  -- dep2
watcher.deps --> [dep1, dep2]
Copy the code

After vm.name is changed, dep1 informs the associated Watcher, and Then Watcher executes the getter to get the new value and passes the new value to cb:

vm.name -> dep1 -> watcher -> getter -> value -> cb
Copy the code

As you may have noticed, userInfo in the above example seems to be used to calculate attributes:

var vm = new Vue({
  data: {
    name: 'luobo',
    age: 18
  },
  computed: {
    userInfo() {
      return this.name + The '-' + this.age
    }
  }
})
Copy the code

In fact, calculated properties are also internally implemented based on Watcher. Each calculated property corresponds to a Watcher, and its getter is the declared function for the calculated property. However, the watcher corresponding to the calculated property is slightly different from the watcher created directly from vm.$watch(). After all, if there is no place to use the calculated property, it can be a bit wasteful to re-evaluate every time the data changes, as we’ll see later in this article.

The relationship between Data, DEP, and Watcher is described above, but the question arises: how is this dependency established? How does Watcher get notified when the data changes?

Let’s dig into the Vue source code to clarify these two issues.

Establish dependencies

Vue source version V2.5.13, the excerpt of the part of the code for easy analysis has been simplified or rewritten.

The responsive core logic is in the “Vue/SRC /core/ Observer” directory of the Vue project.

Let’s go through the example code first, starting with Vue instantiation:

var vm = new Vue(/ *... * /)
Copy the code

With the incoming data initialization code related to respond type, in the “vue/SRC/core/instance/state js” file:

observer/state.js#L149

// new Vue() -> ... -> initState() -> initData()
observe(data)
Copy the code

Function observe() aims to make the entire object passed in responsive by iterating through all of the object’s properties and then executing:

observer/index.js#L64

// observe() -> new Observer() -> observer.walk()
defineReactive(obj, key, value)
Copy the code

DefineReactive () is the core function used to defineReactive data. The main things it does include:

  • Create a new DEP object corresponding to the current data
  • throughObject.defineProperty()Redefine the object property, configure the property set, get, so that the data can be obtained, set can execute Vue code

OK, let’s stop there and finish with Vue instantiation.

Note that all attributes of data passed into Vue are propped onto the newly created Vue instance Object, so that the operation via vm.name is actually data.name, which is also implemented with Object.defineProperty().

Let’s look at how Watcher was created:

vm.$watch(userInfo, onUserInfoChange)
Copy the code

After the above code is executed, it calls:

instance/state.js#L346

// Vue.prototype.$watch()
new Watcher(vm, expOrFn, cb, options)
Copy the code

That is:

new Watcher(vm, userInfo, onUserInfoChange, {/* 略 */})
Copy the code

In addition to logging the VM, getter, cb, and initializing various properties, the most important thing during watcher object creation is that the getter function passed in is called:

observer/watcher.js#L103

// new Watcher() -> watcher.get()
value = this.getter.call(vm, vm)
Copy the code

During the execution of the getter function, the data needed to read is retrieved, which triggers the get method configured earlier via defineReactive() :

if (Dep.target) {
  dep.depend()
}
Copy the code

What is this for?

Back to the watcher.get() method, there is the following code before and after the getter:

pushTarget(this)
// ... 
value = this.getter.call(vm, vm)
// ...
popTarget()
Copy the code

PushTarget () sets the current watcher to dep.target, so that when the corresponding get method is executed on vm.name, the value of dep.target is the same as that of watcher. The dependency is then established with dep.depend().

The logic executed by dep.depend() is more predictable, logging watcher (referenced by dep.target) to dep.subs and deP to watcher.deps — dependency established!

Then look at how the established dependencies are used.

Data change synchronization

Continuing with the previous example, execute the following code:

vm.name = 'tang'
Copy the code

The set method configured via defineReactive() is triggered, and if the data changes then:

// defineReactive() -> set()
dep.notify()
Copy the code

Notifying all dependent methods through a DEP object, deP iterates through the internal subs execution:

// dep.notify()
watcher.update()
Copy the code

Watcher is notified of the data change and can proceed with subsequent processing. I’m not going to expand it.

At this point, the basic mechanism of responsiveness is basically understood. Let’s tidy it up:

  • throughObject.defineProperty()Replace set and GET methods for configuring object attributes to implement “interception”
  • Watcher establishes the dependency by firing the get method of the data when it executes the getter function
  • When the data is written, the set method is triggered to issue notifications with the DEP and watcher updates

It’s easier to understand the official Vue diagram:

Image source: vuejs.org/v2/guide/re… RenderWatcher is a special type of Watcher, and this is how RenderWatcher works, as we’ll see later.

Calculate attribute

Computed properties, mentioned earlier in this article, are also handled as watcher in Vue. The ComputedWatcher is special in that it has no cb (empty function), only getters, and its value is computed and cached only when it is used.

What does that mean?

First, a ComputedWatcher is created without immediately executing the getter (with the lazy option value false), so it does not initially have a dependency on the data it uses.

When a calculated property is “get”, it first executes a predefined ComputedGetter function, which has a special piece of logic:

instance/state.js#L238

function computedGetter () {
  if (watcher.dirty) {
    watcher.evaluate()
  }
  if (Dep.target) {
    watcher.depend()
  }
  return watcher.value
}
Copy the code

Watcher is dirty.

When the watcher corresponding to the calculated property is originally created, it does not execute the getter, and sets dirty to true, so that when the current value of the calculated property is obtained, it executes the getter to get the value, and marks dirty as false. In this way, the value of the calculated property can be returned directly without the need to perform the calculation (getter).

In addition, the watcher that evaluates the property calls watcher.evaluate() further to collect the dependencies. Watcher, which calculates attributes, is notified when the dependent data changes, but watcher marks itself as dirty, not counted. This has the advantage of reducing overhead and performing the calculation only when there is a place to calculate the value of the attribute.

Is there a problem if the dependent data changes and the evaluated property simply flags dirty as true?

The solution to this problem is this part of the code above:

if (Dep.target) {
  watcher.depend()
}
Copy the code

That is, if there is a watcher currently collecting dependencies, Then the watcher of the currently calculated property will indirectly “inherit” the dependency to this watcher via watcher.depend() (watcher.depend() internally is the DEP execution for each watcher.deps record Dep.depend () to make the dependent data dependent on the current Watcher).

So, the watcher that depends on the calculated property is notified directly when the data changes, and the calculated property is evaluated when the calculated property is retrieved.

So, a Watcher that depends on calculated properties can be considered a watcher that depends on a watcher. The most common watcher in Vue is RenderWatcher.

RenderWatcher and asynchronous updates

After reading the previous article, you should have a basic understanding of the Vue responsive principle. So how does Vue apply this to view updates? The answer is RenderWatcher.

RenderWatcher is first and foremost a Watcher, except that like the ComputedWatcher for the calculated properties, it also has some special behavior.

RenderWatcher is created in the mountComponent function:

// Vue.prototype.$mount() -> mountComponent()
let updateComponent = (a)= > {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, null.true /* isRenderWatcher */)
Copy the code

That’s the core code. This watcher is the only RenderWatcher of the Vue instance object, which is logged to vm._watcher in the Watcher constructor (normal Watcher is logged to vm._watchers array).

The watcher also executes watcher.get() at the end of the creation, which executes the getter collection dependency. In this case, the getter is updateComponent, that is, render + update DOM! Also, dependencies are collected on the data used in this process.

So, of course, when data is used in render() and changes occur, RenderWatcher is notified and the view is eventually updated!

However, there is a question here: if you make multiple data changes, don’t you have to perform DOM updates frequently?

This is where RenderWatcher’s special feature comes in: asynchronous updates.

If the data is updated, the dependent watcher will execute watcher.update().

observer/watcher.js#L161

if (this.lazy) {
  this.dirty = true
} else if (this.sync) {
  this.run()
} else {
  queueWatcher(this)}Copy the code

In the first case, lazy is true, which is the calculated property, as mentioned in the previous section, but marks dirty as true and does not evaluate immediately. If sync is true, it doesn’t matter here, but it looks simple enough to do the calculation immediately.

The final situation, which is the RenderWatcher scenario here, is not executed immediately, and it’s not marked as dirty like calculating attributes, but it’s put in a queue.

What does this queue do?

The code is in observer/scheduler.js, which, in a nutshell, implements asynchronous updates.

To understand its implementation, it is necessary to have some understanding of the browser’s Event Loop mechanism. If you’re not familiar with event loops, check out the following article:

Detailed explanation of JavaScript operation mechanism: Again talk about Event Loop – Ruan Yifeng

The event loop mechanism is a bit complicated, but understanding the event loop will give you an insight into the Vue asynchronous update scheme here.

RenderWatcher executes its getter, the updateComponent function, asynchronously based on an event loop, and when RenderWatcher updates () multiple times, updateComponent is executed only once. This solves the performance problem.

The new problem, however, is that data changes are not directly reflected in the DOM, but rather wait for asynchronous updates to be executed, which is why Vue provides the nextTick() interface and requires developers to put DOM operations into the nextTick() callback.

Vuex, Vue – the Router

Vuex and VUE-Router in Vue suite also realize functions based on Vue’s responsive mechanism.

Let’s start with Vuex, code version V3.0.1.

Vuex

In Vuex applications, all components can be referenced to the global store via this.$store, and the store data can be synchronized when the data changes. This is a responsive application.

This.$store is implemented as a global mixin with the following code:

src/mixin.js#L26

this.$store = options.store || options.parent.$store
Copy the code

This will initialize the $store property for each component’s beforeCreate.

Reactive processing of store data is achieved by instantiating a Vue object:

src/store.js#L251

// new Store() -> resetStoreVM()
store._vm = new Vue({
  data: {
    ? state: state
  },
  computed / / store. Getters
})
Copy the code

With the introduction above, this is easy to understand. Because state and processing are reactive data, and getters are created as computed properties, the use of these data creates a dependency that responds to changes in the data.

Vue-Router

In vue-Router, the more important data is $route, that is, the current page route data. When the route changes, different components need to be displayed instead (router-view component implementation).

Vm.$route is actually derived from vue. prototype, but the value corresponds to router.history.current.

For the global mixin beforeCreate, initialize the current value for each key on the mixin beforeCreate for each key on the mixin beforeCreate for each key on the mixin beforeCreate for each key on the mixin

V2.8.1 / SRC/install. Js# L27

// beforeCreate
Vue.util.defineReactive(this.'_route'.this._router.history.current)
Copy the code

So this._route is responsive, so if the page route changes, how do you modify _route here?

The answer is in VueRouter’s init() :

history.listen(route= > {
  this.apps.forEach((app) = > {
    app._route = route
  })
})
Copy the code

A Router object may be associated with multiple VUE instance objects (called app in this case), all of which are notified of each route change.

Again, let’s look at where we use vm.$route, which is VueRouter’s two components:

Both components rely on $route in Render () to render according to the value of route. Here the specific process is not expanded, interested can take a look at the relevant source code (V2.8.1 / SRC/Components), the principle of RenderWatcher has been introduced in the section.

Practice: watch – it

Understanding the above so much, also want to try their own, the Vue responsive related core logic stripped out, do a simple data responsive library. Because of the focus on data only, the parts related to the Vue component/instance object are removed during the stripping process, including watcher.vm, so that the watcher.getter calculation does not specify a context object.

Those who are interested in seeing the code directly can go to the luobotang/ Watch-it.

Watch-it includes only data-responsive functionality, exposing four interfaces:

  • defineReactive(obj, key, val): Configures a reactive data attribute for the object
  • observe(obj): Configure a data object to be reactive, internally performing defineReactive on all attributes
  • defineComputed(target, key, userDef): Creates watcher internally by configuring a calculated property for the object
  • watch(fn, cb, options)Watcher is created internally by calling cb when the evaluation function changes data

Take a look at an example:

const { observe, watch } = require('@luobotang/watch-it')

const data = {
  name: 'luobo'.age: 18
}

observe(data)

const userInfo = function() {
  return data.name + The '-' + data.age
}

watch(userInfo, (value) => console.log(value))
Copy the code

This way, when the data is modified, the new userInfo value is printed.

I also built a simple Vue without the virtual DOM, and implemented a DEMO, using only a reactive mechanism:

watch-it/example/

The source code is here:

luobotang/watch-it/example/vue.js

conclusion

OK, the above is all about the Vue responsive principle, of course, just my understanding and practice.

I learned a lot in sorting out and writing these contents, and I hope they will be helpful to you.

Level is limited, mistakes and omissions are unavoidable, welcome to point out.

Finally, thanks for reading!