4

Original link: https://ssshooter.com/2021-07-15-how-does-vue-work-1/

I have read a lot of articles on Vue principles for several years. With the help of these articles, I have tried to understand the source code of Vue many times. Finally, I think it’s time to output the content by myself, hoping to be different from other articles. Get everyone familiar with Vue.

This topic is naturally divided into multiple parts to explain the Vue source code. In the first part, let's talk about the most classic Vue responsive principle!

Before formally speaking about the principle, I think I should first explain the following concepts.

Dep

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

The meaning of Dep is naturally dependency (that is, depends on , a term in the computer field).

Just like writing a node.js program, the dependency of the npm repository is often used. In Vue, dependency specifically refers to the data responsive processing. As mentioned later, one of the key functions of responsive processing is defineReactive is mentioned in many Vue principle articles.

After Dep is bound to each responsive data, the responsive data will become a dependency (noun). When introducing Watcher below, it will be mentioned that responsive data may be dependent on watch, computed, and use in templates ( verb).

subs

There is a subs attribute under the Dep object, which is an array. It is easy to guess that it means a list of subscribers. Subscribers may be watch functions, computed functions, and view update functions.

Watcher

Watcher is the subscriber mentioned in Dep (not to be confused with the Observer observer later).

Because the function of Watcher is to respond to Dep updates in a timely manner, just like some App subscription pushes, you (Watcher) subscribe to certain information (Dep), and you will be reminded to read when the information is updated.

deps

Similar to the subs attribute of Dep, the Watcher object also has the deps attribute. This constitutes a many-to-many relationship between Watcher and Dep. The reason for recording each other is that when one party is cleared, the related objects can be updated in time.

How the Watcher is generated

The watch, computed, and rendering templates mentioned many times above produce Watcher, which are all concise and understandable in the Vue source code:

  • mountComponent of vm._watcher = new Watcher(vm, updateComponent, noop);
  • initComputed of watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
  • $watcher of var watcher = new Watcher(vm, expOrFn, cb, options);

Observer

Observer is an observer, he is responsible for recursively observing (or processing) the responsive object (or array) . In the printed example, you can notice that the responsive object will carry a __ob__ , which is the proof that it has been observed. The observer is not as important as Dep and Watcher above, just a little understanding.

walk

Observer.prototype.walk is the core method of recursive processing when Observer is initialized, but this method is used to process objects, and there is also Observer.prototype.observeArray process arrays.

Core process

According to the relationship of the above several concepts, how to match and how to realize data responsive update?

First set our goal: naturally when the data is updated, the view is automatically refreshed and the latest data is displayed.

This is the relationship between Dep and Watcher mentioned above. The data is Dep, and Watcher triggers the page rendering function (this is the most important watcher).

But new questions follow. How does Dep know what Watcher depends on him?

Vue uses a very interesting method:

  • Before running the callback function of Watcher, write down what the current Watcher is (via Dep.target)
  • Responsive data is used in the running callback function, then will inevitably call the getter function of the responsive data
  • In the getter function of the response data, you can record the current Watcher and establish the relationship between Dep and Watcher
  • Later, when the responsive data is updated, will inevitably call the setter function
  • Based on the relationship established before, the callback function of the corresponding Watcher can be triggered in the setter function

Code

The above logic is in the defineReactive function. There are many entries for this observe talk about the more important 060ff61a8ec133 function first.

In the observe function, the new Observer object will be used. Observer.prototype.walk used to process the values in the object one by one, and the defineReactive function is used.

Because the defineReactive is too important and not long, it is more convenient to paste it directly 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 的情况
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      // customSetter 用于提醒你设置的值可能存在问题
      if ('development' !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    },
  })
}

First of all, each responsive value is a "dependency", so in the first step we first borrow the ability of closure to create a Dep for each value. (Up to Vue 3, no closure is needed)

Then look at the three core parameters:

  • obj The object of the value that currently needs to be responsively processed
  • key value of key
  • val current value

This value may have defined its own getter and setter before, so the original getter and setter should be processed first when doing Vue's reactive processing.

It is mentioned in the core process above that the relationship between Dep and Watcher will be established in the getter function, specifically relying on dep.depend() .

Here Dep and Watcher call each other:

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)
}

Through these functions, you can appreciate the Dep and Watcher ... But it seems roundabout, in simple terms, what we actually do is add each other to the many-to-many list as mentioned above.

You can find all the Watchers subscribed to the same Dep in the subs of Dep, and you can also find all the Deps subscribed by the Watcher in the deps of Watcher.

But there is a hidden problem, that is, Dep.target come from? Put it aside first, and then answer it. Let's look at the setter function first, the key 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 is not difficult to understand that Dep reminds everyone in his subscriber list (subs) to update. The so-called subscribers are all subs[i].update() , and 060ff61a8ec2d1 calls Watcher.prototype.update .

Then take a look at what Watcher's update

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

Here I think there are two points worthy of development, so dig some holes😂

  • Pit 1: If it is not a synchronous update here, it will go to queueWatcher, and then asynchronous update will be introduced, which also reduces the difficulty of understanding here. In short, it is good to know that queueWatcher will run after a meal operation.
  • Pit 2: Watcher's cb function may process watch, computed and component update function . Especially important is the component update function, which is also updating the Vue page here, so it is worth expanding here. In order to reduce the difficulty of understanding, just know that the update is triggered here, and the update method will be discussed later.
  • Pit 3: It can be seen that when lazy is not running, the following steps will only mark that the data has been updated, and the new value will be calculated next time.
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)
      }
    }
  }
}

This code is the need to focus now get method for Dep.target been set .

Because only Dep.target exists, after the callback function cb (for example, the page rendering function is a typical Watcher cb) is called, Dep.prototype.depend can really take effect. The logic after that is back to the value of using responsive data, everything is connected! Form a closed loop (funny)! This is the answer to the question left over from depend()

Summarize

  • Dep is associated with data, which means that data can become dependent
  • Watcher is associated with watch, computed, and rendering functions, which means that these functions can become dependent subscribers
  • Observer can be regarded as an entry point for processing Dep, recursively processing responsive data
  • When the callback function of Watcher uses responsive data, it will first set Dep.target
  • Dep.target in the getter function, and establishes the subscriber and dependency relationship with the caller
  • Responsive data traverses subs in the setter function to notify all subscribers of the data update
  • When the subscriber is the view update function ( updateComponent -> _update ), the user can see the page update when the responsive data is updated, thereby achieving the responsive update effect

Although roughly speaking this algorithm is not difficult to understand, there are actually many other mechanisms that cooperate with this algorithm to form a complete Vue. For example, the pits dug above: the implementation of the update queue and component update function itself is worth learning.

In addition, there are more small details in the code, which is left for everyone who is interested to study for themselves.

PS. Because my expressive skills are really not good, coupled with the curse of knowledge, I am not sure if this text can really explain the principle of Vue responsiveness. If there is anything you don’t understand, please raise it in the comment area, thank you Everyone 💡

The only reference link: Vue.js


ssshooter
3.7k 声望1.8k 粉丝