Vue responsive principle

Posted by canadian_angel on Sun, 06 Mar 2022 11:43:52 +0100

One of Vue's most unique features is its non intrusive response system. The data model is just a normal JavaScript object. When you modify them, the view updates. When talking about the principle of Vue responsive implementation, many developers know that the key to implementation is to use object Defineproperty, but how is it implemented? Today, let's explore it.

For ease of understanding, let's start with a small example:

<body>
  <div id="app">
    {{ message }}
  </div>
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue!'
      }
    })
</script>
</body>

We have successfully created the first Vue application! It looks very similar to rendering a string template, but Vue does a lot of work behind it. Now that the data and DOM have been associated, everything is responsive. How do we confirm? Open your browser's JavaScript console (which opens on this page) and modify the app Message, you will see that the above example is updated accordingly. The modified data will be updated automatically. How does Vue do it?
When creating an instance through the Vue constructor, an initialization operation will be performed:

function Vue (options) {
    this._init(options);
}

This_ The init initialization function internally initializes the life cycle, events, rendering functions, States, and so on:

      initLifecycle(vm);
      initEvents(vm);
      initRender(vm);
      callHook(vm, 'beforeCreate');
      initInjections(vm);
      initState(vm);
      initProvide(vm);
      callHook(vm, 'created');

Because the topic of this article is the principle of responsiveness, we only focus on initState(vm). Its key calling steps are as follows:

function initState (vm) {
  initData(vm);
}

function initData(vm) {
  // data is the {message: 'Hello Vue!'} passed in when we create Vue instances
  observe(data, true /* asRootData */);
}

function observe (value, asRootData) {
  ob = new Observer(value);
}

var Observer = function Observer (value) {
  this.walk(value);
}

Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    // Implement responsive key functions
    defineReactive$$1(obj, keys[i]);
  }
};
}

Let's summarize the initState(vm) process above. During initialization, the application data will be detected, that is, an Observer instance will be created, and its constructor will execute the walk method on the prototype. The main function of the walk method is to traverse all the attributes of the data and convert each attribute into a response, which is mainly completed by the defineReactive$ function.

function defineReactive$$1(obj, key, val) {
  var dep = new Dep();
  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;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) { return }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });
}

The defineReactive$ function uses object internally Define property to monitor data changes. Whenever reading data from obj's key, the get function is triggered; Whenever data is set in the key of obj, the set function is triggered. We say that modifying data triggers the set function, so how does the set function update the view? Take the example at the beginning of this article:

<div id="app">
    {{ message }}
</div>

The template uses the data message. When the value of message changes, all views using message in the application can trigger the update. In the internal implementation of Vue, first collect the dependencies, that is, collect the places where the data message is used, and then trigger all the previously collected dependencies when the data changes. That is, we collect dependencies in the above get function and trigger view updates in the set function. Then the next focus is to analyze the get function and set function. Let's first look at the get function. Its key calls are as follows:

get: function reactiveGetter () {
        if (Dep.target) {
          dep.depend();
        }
 }
 
Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
 };
 
Watcher.prototype.addDep = function addDep (dep) {
  dep.addSub(this);
}

 Dep.prototype.addSub = function addSub (sub) {
    this.subs.push(sub);
 };
 among Dep The constructor is as follows:
 var Dep = function Dep () {
   this.id = uid++;
   this.subs = [];
 };

The value of Dep.target in the above code is a Watcher instance. We will analyze when it is assigned later. Let's summarize the work done by the get function in one sentence: add the current Watcher instance (i.e. Dep.target) to the sub array of Dep instance. Before continuing to analyze the get function, we need to find out when the value of Dep.target is assigned to the Watcher instance. Here, we need to start with the function mountComponent:

function mountComponent (vm, el, hydrating) {
  updateComponent = function () {
    vm._update(vm._render(), hydrating);
  };
  new Watcher(vm, updateComponent, noop, xxx);
}
// Under the water constructor
var Watcher = function Watcher (vm, expOrFn, cb) {
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
  }
   this.value = this.get();
}

Watcher.prototype.get = function get () {
   pushTarget(this);
   value = this.getter.call(vm, vm);
}

function pushTarget (target) {
    targetStack.push(target);
    Dep.target = target;
}

From the above code, we know that the mountComponent function will create a Watcher instance. In its constructor, it will eventually call the pushTarget function to assign the current Watcher instance to Dep.target. In addition, we note that the action of creating a Watcher instance occurs inside the function mountComponent, that is, the Watcher instance is a component level granularity, rather than creating a Watcher instance wherever data is used. Now let's take a look at the main calling process of the set function:

set: function reactiveSetter (newVal) {
  dep.notify();
}

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

Watcher.prototype.update = function update () {
  queueWatcher(this);
}

 Watcher.prototype.update = function update () {
   // A global queue is a global array
   queue.push(watcher);
   nextTick(flushSchedulerQueue);
 }
 
 // flushSchedulerQueue is a global function
 function flushSchedulerQueue () {
    for (index = 0; index < queue.length; index++) {
      watcher = queue[index];
      watcher.run();
    }
 }
 
Watcher.prototype.run = function run () {
   var value = this.get();
}

The content of the set function is a little long, but the above code is simplified and should not be difficult to understand. When the application data is changed, the set function is triggered to execute. It will call the notify() method of the Dep instance, and the notify method will call the update method of all watcher instances collected by the current Dep instance again to update all view parts that use the data. Let's continue to see what the update method of the watcher instance does. The update method will add the current watcher to the array queue, and then execute the run method of each Watcher in the queue. The get method on the Wather prototype will be executed inside the run method. The subsequent calls are described in the previous analysis of the mountComponent function, which will not be repeated here. In summary, the final update method will trigger the updateComponent function:

updateComponent = function () {
  vm._update(vm._render(), hydrating);
};

Vue.prototype._update = function (vnode, hydrating) {
  vm.$el = vm.__patch__(prevVnode, vnode);
}

Here we note_ The first parameter of the update function is vnode. Vnode, as its name suggests, means virtual node. It is an ordinary object whose properties save the data needed to generate DOM nodes. When it comes to virtual nodes, do you think of virtual DOM easily? Yes, virtual DOM is also used in Vue. As mentioned earlier, Wather is related to components. Updates inside components are compared and rendered with virtual dom_ The patch function is called inside the update function, which compares the differences between the old and new vnodes, and then finds out the nodes to be updated according to the comparison results.

Note: this analysis example is based on Vue v2 Version 6.14.

Topics: Javascript Vue.js html