7
头图

One of the most unique features of Vue is its non-intrusive response system. Data models are just plain JavaScript objects. And when you modify them, the view updates. When it comes to Vue's responsive implementation principle, many developers know that the key to implementation is to use Object.defineProperty , but how to implement it specifically, let's find out today.

For simplicity, 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've successfully created our first Vue app! It looks very similar to rendering a string template, but Vue does a lot of work behind the scenes. Now that the data and the DOM are associated, everything is responsive. How can we be sure? Open your browser's JavaScript console (the one that opens on this page), and modify the value of app.message, and you'll see the example above update accordingly. Modifying the data will automatically update, how does Vue do it?
When an instance is created through the Vue constructor, an initialization operation is performed:

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

This _init initialization function will initialize the life cycle, events, rendering functions, states, etc.:

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

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

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

function initData(vm) {
  // data就是我们创建 Vue实例传入的 {message: 'Hello Vue!'}
  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++) {
    // 实现响应式关键函数
    defineReactive$$1(obj, keys[i]);
  }
};
}

Let's summarize the above initState(vm) process. When initializing the state, the data of the application will be detected, that is, an Observer instance will be created, and the walk method on the prototype will be executed inside its constructor. The main function of the walk method is to traverse all the attributes of the data and convert each attribute into reactive, and the work of this conversion is mainly done by the defineReactive$$1 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$$1 function internally uses Object.defineProperty to monitor data changes. Whenever data is read from the key of obj, the get function is triggered; whenever data is set to the key of obj, the set function is triggered. We say that modifying the data triggers the set function, so how does the set function update the view? Take the example at the beginning of this article to analyze:

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

This template uses the data message, and when the value of the message changes, all views in the application that use the message can trigger an update. In the internal implementation of Vue, the dependencies are first collected, that is, the places where the data messages are used are collected, and then when the data changes, all the previously collected dependencies can be triggered once. That is to say, we collect dependencies in the above get function and trigger the view update in the set function. Then the next focus is to analyze the get function and the set function. 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);
 };
 其中 Dep 构造函数如下:
 var Dep = function Dep () {
   this.id = uid++;
   this.subs = [];
 };

The value of Dep.target in the above code is a Watcher instance, and we will analyze when it is assigned later. Let's summarize what the get function does in one sentence: add the current Watcher instance (that is, Dep.target) to the Dep instance's subs array. Before continuing to analyze the get function, we need to figure out when the value of Dep.target is assigned to the Watcher instance. Here we need to start the analysis from the mountComponent function:

function mountComponent (vm, el, hydrating) {
  updateComponent = function () {
    vm._update(vm._render(), hydrating);
  };
  new Watcher(vm, updateComponent, noop, xxx);
}
// Wather构造函数下
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, and in its constructor will eventually call the pushTarget function to assign the current Watcher instance to Dep.target. In addition, we noticed that the action of creating a Watcher instance occurs inside the function mountComponent, which means that the Watcher instance is a component-level granularity, rather than creating a new Watcher instance wherever data is used. Now let's 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 () {
   // queue是一个全局数组
   queue.push(watcher);
   nextTick(flushSchedulerQueue);
 }
 
 // flushSchedulerQueue是一个全局函数
 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 bit 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 the Watcher instances collected by the current Dep instance, so as to update all the 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 Water prototype will be executed inside the run method. The subsequent calls are described in the previous analysis of the mountComponent function, so I won't repeat them here. In summary, the final update method triggers the updateComponent function:

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

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

Here we notice that the first parameter of the _update function is vnode. As the name suggests, vnode means a virtual node. It is an ordinary object whose properties store the data needed to generate a DOM node. When it comes to virtual nodes, you can easily think of virtual DOM. Yes, virtual DOM is also used in Vue. As mentioned earlier, Water is related to components, and updates inside components are compared and rendered using virtual DOM. The _update function internally calls the patch function, which compares the difference between the old and new vnodes, and then finds out the node that needs to be updated according to the comparison result.

Note: The analysis example in this article is based on Vue v2.6.14 version.


Zuckjet
437 声望657 粉丝

学如逆水行舟,不进则退。