How does Vue reactivity work

How does Vue reactivity work

·

7 min read

In recent years, I have read a lot of articles about the mechanism of Vue2.0. With their help, I pore over the source code of Vue. And now, I think it's time to export something about it by myself. I hope this post can help you to understand Vue more deeply from a different perspective.

In the first post, we will learn about the most elaborate design—reactivity.

Before we dive into the core code, we should understand those conceptions ↓

Dep

var Dep = function Dep() {
  this.id = uid++
  this.subs = []
}

Dep means dependency. Like writing a node.js program, we will use dependencies on npm. In Vue, what we depend is the data which is reactive. I will soon mention the core function of Vue reactivity—defineReactive.

A reactive data becomes a dependency when it binds with a dep.

subs

Dep object has a subs property, which is an array. As we guess, it's a list of subscribers. There are three kinds of "watcher"—watch, computed, and render function.

Watcher

Watcher is the subscriber of Dep(don't mix it with the Observer).

Dep's update can be responded to by Watcher. It's like you subscribe to a YouTube channel(Dep), you(Watcher) will immediately watch the new clip right after you get the update notification.

deps

The function of deps is similar to that of subs (from Dep). This design provide a many to many relationship between Watcher and Dep. While one of Watcher or Dep is removed, the other one will be updated.

How to create Watcher

There are three kinds of "watcher", which have been mentioned before. We can find how they are created in the source code.

  • mountComponentvm._watcher = new Watcher(vm, updateComponent, noop);
  • initComputedwatchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
  • $watchervar watcher = new Watcher(vm, expOrFn, cb, options);

Observer

The responsibility of Observer is to "observe" data or array recursively. When you log the Vue instance in the console, you may notice that there is a __ob__ property in reactive data, which is the proof of "observed".

walk

Observer.prototype.walk is the core function that Observer uses to process objects. For arrays, Observer.prototype.observeArray is used.

Reactivity processing

Now that we understand those conceptions mentioned above, how can we achieve reactivity with them?

Set our goal first: When reactive data is updated, user can see the newest data (Of course, this process should be automatic.)

That's easy, as we know, reactive data is Dep and the render function (what user can see is generated by this function) is Watcher (and It's the most important Watcher).

But the question is, How Dep know which Watcher is depending it?

The interesting thing comes:

  • Record the current watcher (in Dep.target) before running the callback function of it.
  • The getter function of the reactive data, which is used by the watcher, must be triggered.
  • The Current Watcher is recorded by the getter function, and the relationship between Dep and Watcher is constructed.
  • When we update reactive data, the setter function of it must be triggered.
  • Base on the relationship constructed before, the related Watcher callback function can be triggered in the setter function.

Source code

In fact, the process above is implemented by the defineReactive function. This function is invoked in many places. Let's see the most important—observe function.

observe function create Observer object, then process data using defineReactive in Observer.prototype.walk.

defineReactive is important and concise, so I paste it here.

function defineReactive(obj, key, val, customSetter, shallow) {
  var dep = new Dep()
  depsArray.push({ dep, obj, key })
  var property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get
  var setter = property && property.set

  var childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      var value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      var value = getter ? getter.call(obj) : val
      // NaN situation
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if ('development' !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    },
  })
}

Every single property of a reactive object is a "dependency" so the first step is to create a Dep for it by using a closure. (closure will not be needed in Vue3)

Look at the parameters:

  • obj: the object waiting for reactivity processing
  • key
  • val: current value, which may be defined a getter and a setter

getter

Previous I mention that the getter function constructs the relationship between Dep and Watcher, and more precisely is achieved by dep.depend().

Below is some function about the interoperation of Dep and Watcher:

Dep.prototype.depend = function depend() {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}
Watcher.prototype.addDep = function addDep(dep) {
  var id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}
Dep.prototype.addSub = function addSub(sub) {
  this.subs.push(sub)
}

It seems intricate between Dep and Watcher, but what those functions actually do is to create the many to many relationship.

You can find all subscribers of a Dep in its subs property, and all Deps that a Watcher is watching in its deps property.

There is a question hidden here—where is Dep.target set? Don't hurry; I'll tell you later.

setter

The Key function in the setter function is dep.notify().

Dep.prototype.notify = function notify() {
  // stabilize the subscriber list first
  var subs = this.subs.slice()
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

It's not hard to understand. Dep notify its subscribers (in subs) to update. subs[i].update() invoke Watcher.prototype.update.Let's see what it does:

Watcher.prototype.update = function update() {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

There are two points I think we can dive into:

  • If it updates asynchronously, queueWatcher will run. But it eventually run.
  • The sequence of running watch, computed, and render function is worth noticing.
  • The lazy flag is used for computed values.

I may talk about them in the next few posts.

Watcher.prototype.run = function run() {
  if (this.active) {
    var value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      var oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(
            e,
            this.vm,
            'callback for watcher "' + this.expression + '"'
          )
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

The important thing of this code clip is that Dep.target is set here (run -> get -> pushTarget).

Because of the existence of Dep.target, Dep.prototype.depend invoked by Watcher cb makes sense. That's the answer to the unsolved question.

Wrap up

  • An object bind with a Dep, and it become a dependancy.
  • watch, computed, and render functions are Watchers, and they can be subscribers of dependancy.
  • Observer is an entry to process reactive data recursively.
  • Watcher will set Dep.target before running callback function.
  • The getter function of reactive data perceive its "Caller" from Dep.target and construct relationship with Watcher
  • The setter function of reactive data traverse its subs and notify them to run their update function.
  • While the Watcher runs render function (updateComponent -> _update), user can see the newest data on the web page.

Though understanding the fundamental algorithm is not so hard, there are still many details this post doesn't mention yet. For example, the update queue and the component update function itself are worth studying.

I hope you enjoy this post!

Read this post in Chinese

Reference