The scene is introduced

In an SFC (single file Component), we often write logic like this:

<template>
  <div>
    <span>{{ a }}</span>
    <span>{{ b }}</span>
  </div>  
</template>
<script type="javascript">
export default {
  data() {
    return {
      a: 0.b: 0}},created() {
    // some logic code
    this.a = 1
    this.b = 2}}</script>
Copy the code

As you probably know, after assigning this.a and this.b, Vue puts the corresponding DOM update functions for this.a and this.b into a microtask. After the synchronization task of the main thread is complete, the microtask is queued and executed. Let’s take a look at Vue’s official documentation in the section “In Depth responsive Principles – Declaring responsive Property” :

In case you haven’t noticed, Vue executes asynchronously when updating the DOM. As long as it listens for data changes, Vue opens a queue and buffers all data changes that occur in the same event loop.

So how does Vue achieve this capability? To answer this question, we need to delve into the core of the Vue source code — the reactive principle.

Deep response

Let’s first look at what happens after we assign to this.a and this.b. If you are developing using the Vue CLI, there will be an instantiation of new Vue() in main.js. Since Vue’s source code is written using flow, it adds to the cost of understanding. For convenience, we directly look at the vue. Js source code in the DIST folder of the NPM Vue package. Search for ‘function Vue’ and find the following source code:

function Vue (options) {
  if(! (this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  this._init(options);
}
Copy the code

Very simple source code, source code really not as difficult as we imagine! With that surprise in mind, let’s move on to the _init function and see what it does:

Vue.prototype._init = function (options) {
  var vm = this;
  // a uid
  vm._uid = uid$3+ +;var startTag, endTag;
  /* istanbul ignore if */
  if (config.performance && mark) {
    startTag = "vue-perf-start:" + (vm._uid);
    endTag = "vue-perf-end:" + (vm._uid);
    mark(startTag);
  }

  // a flag to avoid this being observed
  vm._isVue = true;
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options);
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    );
  }
  /* istanbul ignore else */
  {
    initProxy(vm);
  }
  // expose real self
  vm._self = vm;
  initLifecycle(vm);
  initEvents(vm);
  initRender(vm);
  callHook(vm, 'beforeCreate');
  initInjections(vm); // resolve injections before data/props
  initState(vm);
  initProvide(vm); // resolve provide after data/props
  callHook(vm, 'created');

  /* istanbul ignore if */
  if (config.performance && mark) {
    vm._name = formatComponentName(vm, false);
    mark(endTag);
    measure(("vue " + (vm._name) + " init"), startTag, endTag);
  }

  if(vm.$options.el) { vm.$mount(vm.$options.el); }}Copy the code

Let’s ignore the above judgments and go straight to the main logic below. As you can see, the _init function executes initLifeCycle, initEvents, initRender, callHook, initInjection, initState, initProvide, and the second callHook function. We know what that means from the name of the function. Roughly speaking, this code is divided into the following two parts

  1. After completing the initialization of the life cycle, event hooks, and render functions, proceed to the beforeCreate life cycle (execute the beforeCreate function).
  2. After initializing the injection value, state, and provision value, enter the Created life cycle (executing the Created function)

The part of the data responsiveness principle we care about is in the initState function. Let’s see what this function does:

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
  if (opts.computed) { initComputed(vm, opts.computed); }
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}
Copy the code

Here we see a few configuration items that we often see when writing AN SFC file: props, Methods, Data, computed, and Watch. Let’s focus on the opts.data section, which executes the initData function:

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  if(! isPlainObject(data)) { data = {}; warn('data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    );
  }
  // proxy data on instance
  var keys = Object.keys(data);
  var props = vm.$options.props;
  var methods = vm.$options.methods;
  var i = keys.length;
  while (i--) {
    var key = keys[i];
    {
      if (methods && hasOwn(methods, key)) {
        warn(
          ("Method \"" + key + "\" has already been defined as a data property."), vm ); }}if (props && hasOwn(props, key)) {
      warn(
        "The data property \"" + key + "\" is already declared as a prop. " +
        "Use prop default value instead.",
        vm
      );
    } else if(! isReserved(key)) { proxy(vm,"_data", key); }}// observe data
  observe(data, true /* asRootData */);
}
Copy the code

When we write the data configuration item, we define it as a function, so we execute getData:

function getData (data, vm) {
  // #7573 disable dep collection when invoking data getters
  pushTarget();
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, "data()");
    return{}}finally{ popTarget(); }}Copy the code

What the getData function does is very simple: it executes the data function in the context of the component instance. Note that the data function is followed by the pushTarget and popTarget functions, which we will cover later.

After executing getData, we return to initData, which has a looped error judgment. So we come to the observe function:

function observe (value, asRootData) {
  if(! isObject(value) || valueinstanceof VNode) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}
Copy the code

The observe function creates an observer-instantiated observer-for the data object. What does that do? Let’s continue looking at the source code:

var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, '__ob__'.this);
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value); }}Copy the code

Normally, since the data function we define returns an object, we’ll leave arrays alone. So continue with the walk function:

Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); }}Copy the code

The defineReactive$$1 function is executed for each enumerable property in the component instance’s data object returned by the data function:

function defineReactive$$1 (obj, key, val, customSetter, shallow) {
  var dep = new Dep();

  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;
  if((! getter || setter) &&arguments.length === 2) {
    val = obj[key];
  }

  varchildOb = ! 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;
      /* 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();
    }
  });
}
Copy the code

In the defineReactive$$1 function, you first instantiate a dependency collector. Then use Object.defineProperty to redefine the getters (the get function above) and setters (the set function above) for the Object properties.

Trigger getter

Getters and setters can be interpreted as callback functions. When the value of an object property is read, the get function (getter) is triggered. When you set a value for a property of an object, the set function (that is, setter) is fired. Let’s go back to our original example:

<template>
  <div>
    <span>{{ a }}</span>
    <span>{{ b }}</span>
  </div>  
</template>
<script type="javascript">
export default {
  data() {
    return {
      a: 0.b: 0}},created() {
    // some logic code
    this.a = 1
    this.b = 2}}</script>
Copy the code

There are values that set the properties A and B of this object, so the setter will fire. Let’s separate out the set function above:

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(); }Copy the code

The setter executes the getter first:

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
}
Copy the code

The getter first checks for the presence of dep.target. The initial value of dep. target was null when the getData function was executed. PushTarget (); popTarget (); pushTarget (); popTarget ();

Dep.target = null;
var targetStack = [];

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

function popTarget () {
  targetStack.pop();
  Dep.target = targetStack[targetStack.length - 1];
}
Copy the code

To properly execute the getter, you need to first execute the pushTarget function. Let’s find out where pushTarget is executed. By searching for pushTarget in vue.js, we find five places, four places to execute, excluding where it is defined.

The first place to execute pushTarget. This is an error handling function that normal logic would not fire:

function handleError (err, vm, info) {
  // Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
  // See: https://github.com/vuejs/vuex/issues/1505
  pushTarget();
  try {
    if (vm) {
      var cur = vm;
      while ((cur = cur.$parent)) {
        var hooks = cur.$options.errorCaptured;
        if (hooks) {
          for (var i = 0; i < hooks.length; i++) {
            try {
              var capture = hooks[i].call(cur, err, vm, info) === false;
              if (capture) { return}}catch (e) {
              globalHandleError(e, cur, 'errorCaptured hook');
            }
          }
        }
      }
    }
    globalHandleError(err, vm, info);
  } finally{ popTarget(); }}Copy the code

The second place where pushTarget is executed. This calls the corresponding hook function. Emitted when the corresponding hook function is executed. However, we now operate between the beforeCreate hook and the Created hook and have not fired yet:

function callHook (vm, hook) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget();
  var handlers = vm.$options[hook];
  var info = hook + " hook";
  if (handlers) {
    for (var i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info); }}if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook);
  }
  popTarget();
}
Copy the code

The third place where pushTarget is executed. This is the function that executes when watcher is instantiated. Checking the previous code, we don’t seem to see the new Watcher action either:

Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\" "));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
}
Copy the code

The fourth place to do pushTarget is the getData function. But the getData function is executed before the defineReactive$$1 function. Dep.target has been reset to null after the getData function is executed.

function getData (data, vm) {
  // #7573 disable dep collection when invoking data getters
  pushTarget();
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, "data()");
    return{}}finally{ popTarget(); }}Copy the code

It seems that firing the setter directly does not make the logic in the getter work. Also, we find that the setter logic can’t go any further if we can’t find the source of the dep. target because there is a dep. target judgment in the setter.

Looking for Dep. Target

So where does the dep.target value come from? Don’t worry, let’s go back to the _init function and continue:

Vue.prototype._init = function (options) {
  var vm = this;
  // a uid
  vm._uid = uid$3+ +;var startTag, endTag;
  /* istanbul ignore if */
  if (config.performance && mark) {
    startTag = "vue-perf-start:" + (vm._uid);
    endTag = "vue-perf-end:" + (vm._uid);
    mark(startTag);
  }

  // a flag to avoid this being observed
  vm._isVue = true;
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options);
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    );
  }
  /* istanbul ignore else */
  {
    initProxy(vm);
  }
  // expose real self
  vm._self = vm;
  initLifecycle(vm);
  initEvents(vm);
  initRender(vm);
  callHook(vm, 'beforeCreate');
  initInjections(vm); // resolve injections before data/props
  initState(vm);
  initProvide(vm); // resolve provide after data/props
  callHook(vm, 'created');

  /* istanbul ignore if */
  if (config.performance && mark) {
    vm._name = formatComponentName(vm, false);
    mark(endTag);
    measure(("vue " + (vm._name) + " init"), startTag, endTag);
  }

  if(vm.$options.el) { vm.$mount(vm.$options.el); }}Copy the code

We see that at the end of the _init function, the vm.$mount function is executed. What does this function do?

Vue.prototype.$mount = function (el, hydrating) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
}
Copy the code

Let’s move on to the mountComponent function:

function mountComponent (vm, el, hydrating) {
  vm.$el = el;
  if(! vm.$options.render) { vm.$options.render = createEmptyVNode; {/* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0)! = =The '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        );
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        );
      }
    }
  }
  callHook(vm, 'beforeMount');

  var updateComponent;
  /* istanbul ignore if */
  if (config.performance && mark) {
    updateComponent = function () {
      var name = vm._name;
      var id = vm._uid;
      var startTag = "vue-perf-start:" + id;
      var endTag = "vue-perf-end:" + id;

      mark(startTag);
      var vnode = vm._render();
      mark(endTag);
      measure(("vue " + name + " render"), startTag, endTag);

      mark(startTag);
      vm._update(vnode, hydrating);
      mark(endTag);
      measure(("vue " + name + " patch"), startTag, endTag);
    };
  } else {
    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before: function before () {
      if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate'); }}},true /* isRenderWatcher */);
  hydrating = false;

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, 'mounted');
  }
  return vm
}
Copy the code

We were pleasantly surprised to find that there was a New Watcher operation! Is really mountains and rivers doubt no way, another village! The watcher instantiated here is a watcher used to update the DOM. He reads all the values in the Template section of the SFC file in turn. This means that the corresponding getter will fire.

Since new Watcher executes watcher.get, which executes pushTarget, dep. target is assigned. The logic inside the getter executes smoothly.

getter

At this point, we finally get to the heart of Vue’s responsive principle. Let’s go back to the getter and see what the getter does with dep. target:

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
}

Copy the code

Again, let’s skip the details of improving code robustness and get straight to the main line. As you can see, the dep.depend function is executed when dep. target exists. What does this function do? Let’s look at the code:

Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this); }}Copy the code

It’s also very simple. The dep.target. addDep function is executed. But dep. target is actually a Watcher, so we’ll go back to the watcher code:

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); }}}Copy the code

Again, let’s ignore some minor logic and focus on the dep.addSub function:

Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
}
Copy the code

Very simple logic, pushing Watcher as a subscriber into the array cache. At this point, the whole logic of the getter is done. The popTarget function is then executed and dep. target is reset to null

setter

Back to the business code:

<template>
  <div>
    <span>{{ a }}</span>
    <span>{{ b }}</span>
  </div>  
</template>
<script type="javascript">
export default {
	data() {
    return {
      a: 0.b: 0}},created() {
    // some logic code
    this.a = 1
    this.b = 2}}</script>
Copy the code

In the Created lifecycle, we fire the setter twice, which executes the following logic:

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(); }Copy the code

Here, we only need to focus on the last function executed by the setter: dep.notify(). Let’s see what this function does:

Dep.prototype.notify = function notify () {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  if(! config.async) {// subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort(function (a, b) { return a.id - b.id; });
  }
  for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); }}Copy the code

Each element of this. subs is a Watcher. In the getter section above, we collected only one Watcher. Because setters are fired twice, the subs[0].update() function, watcher.update(), is executed twice. Let’s see what this function does:

Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this); }}Copy the code

By convention, we jump directly into the queueWatcher function:

function queueWatcher (watcher) {
  var id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    if(! flushing) { queue.push(watcher); }else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      var i = queue.length - 1;
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1.0, watcher);
    }
    // queue the flush
    if(! waiting) { waiting =true;

      if(! config.async) { flushSchedulerQueue();return} nextTick(flushSchedulerQueue); }}}Copy the code

Since the ID is the same, Watcher’s callback will only be pushed to queue once. Here we see a familiar face again: nextTick.

function nextTick (cb, ctx) {
  var _resolve;
  callbacks.push(function () {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick'); }}else if(_resolve) { _resolve(ctx); }});if(! pending) { pending =true;
    timerFunc();
  }
  // $flow-disable-line
  if(! cb &&typeof Promise! = ='undefined') {
    return new Promise(function (resolve) { _resolve = resolve; }}})Copy the code

Once the nextTick function wraps the callback again, it executes timerFunc()

var timerFunc;

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise! = ='undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  timerFunc = function () {
    p.then(flushCallbacks);
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) { setTimeout(noop); }}; isUsingMicroTask =true;
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  var counter = 1;
  var observer = new MutationObserver(flushCallbacks);
  var textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true
  });
  timerFunc = function () {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = function () {
    setImmediate(flushCallbacks);
  };
} else {
  // Fallback to setTimeout.
  timerFunc = function () {
    setTimeout(flushCallbacks, 0);
  };
}
Copy the code

The timerFunc function is a smooth degradation of microtasks. Depending on the level of support in his environment, he calls Promise, MutationObserver, setImmediate, and setTimeout in that order. And execute the callback function in the corresponding microtask or simulated microtask queue.

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow();
  flushing = true;
  var watcher, id;

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  // created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  // user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  // its watchers can be skipped.
  queue.sort(function (a, b) { return a.id - b.id; });

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index];
    if (watcher.before) {
      watcher.before();
    }
    id = watcher.id;
    has[id] = null;
    watcher.run();
    // in dev build, check and stop circular updates.
    if(has[id] ! =null) {
      circular[id] = (circular[id] || 0) + 1;
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? ("in watcher with expression \"" + (watcher.expression) + "\" ")
              : "in a component render function."
          ),
          watcher.vm
        );
        break}}}// keep copies of post queues before resetting state
  var activatedQueue = activatedChildren.slice();
  var updatedQueue = queue.slice();

  resetSchedulerState();

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue);

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush'); }}Copy the code

The core logic of the callback is to execute the watcher.run function:

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); }}}}Copy the code

Execute this.cb, watcher’s callback. At this point, all logic is done.

conclusion

Let’s go back to the business scenario:

<template>
  <div>
    <span>{{ a }}</span>
    <span>{{ b }}</span>
  </div>  
</template>
<script type="javascript">
export default {
  data() {
    return {
      a: 0.b: 0}},created() {
    // some logic code
    this.a = 1
    this.b = 2}}</script>
Copy the code

Although we fired the setter twice, the corresponding render function was executed only once in the microtask. That is, after the dep.notify function sends a notification, Vue de-iterates the corresponding Watcher, queues it, and finally performs a callback.

As you can see, both assignments actually trigger the same render function that updates multiple DOM. This is known as batch updating the DOM.