preface

In the last chapter, we have made a rough analysis of the entire Vue source code (still in the draft box, which needs to be sorted out before being released), but there are still many things that have not been analyzed in depth. I will carry out further analysis through the following important points.

  1. In-depth understanding of Vue responsive principles (data interception)
  2. Dig deeper into how vue.js does “dependency collection” to accurately track all changes
  3. Learn more about the Virtual DOM
  4. Learn more about vue.js batch asynchronous update strategy
  5. In-depth understanding of vue.js internal operation mechanism, understand the principle behind calling each API

In this chapter, we analyze how vue.js performs “dependency collection” to accurately track all changes.

Initialize the Vue

We simply instantiate an instance of Vue. The following are our in-depth thoughts on this simple instance:

// app Vue instance
var app = new Vue({
  data: {
    newTodo: ' ', 
  },

  // watch todos change for localStorage persistence
  watch: {
    newTodo: {
      handler: function (newTodo) {
        console.log(newTodo);
      },
      sync: false, 
      before: function () {

      }
    }
  }  
})
// mount
app.$mount('.todoapp')
Copy the code

initState

We have added a watch attribute configuration above:

We can see from the above code that we have configured a configuration item whose key is newTodo. We can understand from the above code:

When the value of newTodo changes, we need to execute the Hander method, so let’s look at how this works.

Let’s start with the initState method:

  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

InitWatch:

  function initWatch (vm, watch) {
    for (var key in watch) {
      var handler = watch[key];
      if (Array.isArray(handler)) {
        for(var i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]); }}else{ createWatcher(vm, key, handler); }}}Copy the code

From the above code analysis, we can find that watch can have multiple Handers, written as follows:

  watch: {
    todos:
      [
        {
          handler: function (todos) {
            todoStorage.save(todos)
          },
          deep: true
        },
        {
          handler: function (todos) {
            console.log(todos)
          },
          deep: true}},Copy the code

Let’s analyze the createWatcher method:

 function createWatcher (
    vm,
    expOrFn,
    handler,
    options
  ) {
    if (isPlainObject(handler)) {
      options = handler;
      handler = handler.handler;
    }
    if (typeof handler === 'string') {
      handler = vm[handler];
    }
    return vm.$watch(expOrFn, handler, options)
  }
Copy the code

Conclusion:

  1. And from this method, we know that actually ourhanlderIt could be onestring
  2. And thishanderisvmA method on an object that we have examined previouslymethodsMethods inside are eventually mounted invmOn the instance object, you can directly passvm["method"]Visit, so we found againwatchAnother way of writing phi is to give it directlywatchthekeyAssign a string name directly, which can bemethodsA way to set one inside:
watch: {
    todos: 'newTodo'
  },
Copy the code
  methods: {
    handlerTodos: function (todos) {
      todoStorage.save(todos)
    }
  }
Copy the code

Next, call the $watch method

Vue.prototype.$watch = function (
      expOrFn,
      cb,
      options
    ) {
      var vm = this;
      if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
      }
      options = options || {};
      options.user = true;
      var watcher = new Watcher(vm, expOrFn, cb, options);
      if (options.immediate) {
        try {
          cb.call(vm, watcher.value);
        } catch (error) {
          handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\" ")); }}return function unwatchFn() { watcher.teardown(); }};Copy the code

In this method, we see that there is an immediate attribute, which means “immediately”. If we set this to true, the watch hander will be executed immediately. If not, The watcher is executed asynchronously. So this attribute might be useful in some business scenarios.

In this method, a new Watcher object is created. This object is a highlight, and we need to take a look at this object. The code looks like this (delete the code that only keeps the core):

  var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    vm._watchers.push(this);
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if(! this.getter) { this.getter = noop; } } this.value = this.lazy ? undefined : this.get(); };Copy the code

We mainly did the following things:

  1. willwatcherThe object is saved invm._watchersIn the
  2. To obtaingetter.this.getter = parsePath(expOrFn);
  3. performthis.get()To obtainvalue

The parsePath method returns a function:

  var bailRE = /[^\w.$]/;
  function parsePath (path) {
    if (bailRE.test(path)) {
      return
    }
    var segments = path.split('. ');
    return function (obj) {
      for (var i = 0; i < segments.length; i++) {
        if(! obj) {return }
        obj = obj[segments[i]];
      }
      return obj
    }
  }
Copy the code

Call value = this.get. call(vm, vm) in the call this.get() method;

Obj = obj[segments[I]]; To value, such as vm. NewTodo, we have an in-depth understanding of Vue response principle (data interception), we already know that Vue will intercept all data in data, as follows:

    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

So when we call vm.newTodo, the getter is triggered, so let’s take a closer look at the getter methods

getter

The getter code looks like this:

    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
      }
Copy the code
  1. So we get the valuevar value = getter ? getter.call(obj) : val;
  2. callDepThe object’sdependMethods,depThe object is saved intargetProperties of theDep.target.addDep(this);whiletargetIs aWatcherObject with the following 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

The generated Dep object is shown below:

Now that we have completed dependency collection, let’s analyze how to accurately track all changes as data changes.

Track all changes accurately

We can try to modify the value of an attribute in data, such as newTodo, by entering the set method, which looks like this:

      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

Now LET me analyze this method.

  1. First check the new value and the old value. If they are equal, return
  2. calldep.notify();To inform allsubs.subsIs a type isWatcherAn array of objectssubsThe data in it, we analyzed abovegetterLogically maintainedwatcherObject.

The notify method iterates through the subs array and performs update().

  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

If it’s asynchronous, sort it, first in, first out, and iterate through the update() method.

  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

The above method is divided into three cases:

  1. ifwatchConfigure thelazy(lazy), not executed immediately (when will be analyzed later)
  2. If configuredsync(Synchronization) istrueIt is executed immediatelyhandermethods
  3. In the third case, it will be added towatcherQueue (queue)

We’ll focus on the third case, and here’s the queueWatcher source code

  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
  1. First of all,flushingThe default isfalse, so willwatcherStored in thequeueIn the array of.
  2. thenwaitingThe default isfalse, so will goif(waiting)branch
  3. configisVueGlobal configuration, itsasync(Asynchronous) The default value istrue, so it will be executednextTickFunction.

Now let’s analyze the nextTick function

nextTick

The nextTick code is as follows:

  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;
      if (useMacroTask) {
        macroTimerFunc();
      } else{ microTimerFunc(); / /}}$flow-disable-line
    if(! cb && typeof Promise ! = ='undefined') {
      return new Promise(function(resolve) { _resolve = resolve; }}})Copy the code

NextTick mainly does the following:

  1. The parameters to be passedcbThe execution of an anonymous function is stored in acallbacksThe array of
  2. pendinganduseMacroTaskIs the default value offalse, so it will be executedmicroTimerFunc()(the Task)microTimerFunc()Is defined as follows:
if(typeof Promise ! = ='undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)   
    if (isIOS) setTimeout(noop)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}
Copy the code

In fact, use the Promise function (only analyze the Promise compatibility), and Promise is a micro Task that must wait for all macro tasks to be executed, that is, when the main thread is free to execute the micro Task;

Now let’s take a look at the flushCallbacks function:

  function flushCallbacks () {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for(var i = 0; i < copies.length; i++) { copies[i](); }}Copy the code

It’s very simple,

  1. The first is changependingThe status offalse
  2. Traverse the executioncallbacksThe functions in the array, we remember in thenextTickIn the function, we willcbStored in thecallbacksIn the.

Let’s look at the definition of cb. We call nextTick(flushSchedulerQueue); , so cb refers to flushSchedulerQueue, whose code is as follows:

  function flushSchedulerQueue () {
    flushing = true;
    var watcher, id; 
    queue.sort(function (a, b) { return a.id - b.id; });
  
    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
  1. First of all toflushingThe status switch becomestrue
  2. willqueueConducted in accordance with theIDAscending sort,queueIs in thequeueWatcherMethod, will correspond toWatcherPreserved in it.
  3. traversequeueTo execute the correspondingwatchertherunMethods.
  4. performresetSchedulerState()Is to reset the status value, such aswaiting = flushing = false
  5. performcallActivatedHooks(activatedQueue);Update component ToDO:
  6. performcallUpdatedHooks(updatedQueue);Call the lifecycle functionupdated
  7. performdevtools.emit('flush');Refresh the debugging tool.

We iterate through the queue to execute the corresponding Watcher’s run method in 3. We found two Watchers in queue, but we initialized Vue in app.js with watch code as follows:

  watch: { 
    newTodo: {
      handler: function (newTodo) {
        console.log(newTodo);
      },
      sync: false}}Copy the code

NewTodo = newTodo (); newTodo = newTodo (); newTodo = newTodo (); newTodo ();

Conclusion:

  1. In our configurationwatchProperty, generated byWatcherObject, which is only responsible for callinghanlderMethods. Will not be responsible for UI rendering
  2. anotherwatchIn fact, aVueBuilt-in oneWatch(Personal understanding), but when we callVuethe$mountMethod generated when, as we did in ourapp.jsCall this method directly in:app.$mount('.todoapp')The other method does not call the method directly, but instead initializes itVueIn the configuration ofel: '.todoapp'Properties will do. thisWatcherResponsible for the final rendering of the UI, which is very important, and we’ll get into that laterWatcher
  3. $mountMethod is the last method to execute, so it generatesWatcherThe object’sIdIt’s the largest, so we’re traversingqueueBefore, we were going to do an ascending sort, restricting all of themWatchGenerated in configurationWatcherObject is executed last$mountGenerated in theWatcherObject to do UI rendering.

$mount

Let’s look at how the $mount method generates the Watcher object and what its CB is. The code is as follows:

new Watcher(vm, updateComponent, noop, {
      before: function before () {
        if (vm._isMounted) {
          callHook(vm, 'beforeUpdate'); }}},true /* isRenderWatcher */);
Copy the code
  1. From the code above, we can see the last parameterisRenderWatcherThe value set istrueIs a Render WatcherwatchConfigured and generated inWatcherThis value is bothfalse, we are inWatcherAs can be seen in the constructor of:
 if (isRenderWatcher) {
      vm._watcher = this;
    }
Copy the code

If isRenderWatcher is true, mount this particular Watcher directly to the _watcher property of the Vue instance, so in the flushSchedulerQueue method we call callUpdatedHooks, Only this Watcher executes the lifecycle function updated

  function callUpdatedHooks (queue) {
    var i = queue.length;
    while (i--) {
      var watcher = queue[i];
      var vm = watcher.vm;
      if(vm._watcher === watcher && vm._isMounted && ! vm._isDestroyed) { callHook(vm,'updated'); }}}Copy the code
  1. Second parameterexpOrFn, that is,Watcherthegetter, will be instantiatedWatcherIs called whengetMethod, and then executevalue = this.getter.call(vm, vm);In this case, it will be executedupdateComponentMethod, which is a key UI rendering method that we won’t go into here.
  2. The third parameter iscb, an empty method is passed in
  3. The fourth argument passes oneoptionsObject, pass one herebeforeBeforeUpdate, a mid-life function that is executed before the UI is re-rendered

We have analyzed one working process of the Watch above, and now let’s analyze the differences between computed work and the Watch.

computed

First, when the Vue object is instantiated, also in the initState method, computed is handled and the initComputed method is executed with the following code:

  function initComputed (vm, computed) {
    // $flow-disable-line
    var watchers = vm._computedWatchers = Object.create(null);
    // computed properties are just getters during SSR
    var isSSR = isServerRendering();

    for (var key in computed) {
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      if (getter == null) {
        warn(
          ("Getter is missing for computed property \"" + key + "\"."),
          vm
        );
      }

      if(! isSSR) { // create internal watcherfor the computed property.
        watchers[key] = new Watcher(
          vm,
          getter || noop,
          noop,
          computedWatcherOptions
        );
      }

      // component-defined computed properties are already defined on the
      // component prototype. We only need to define computed properties defined
      // at instantiation here.
      if(! (keyin vm)) {
        defineComputed(vm, key, userDef);
      } else {
        if (key in vm.$data) {
          warn(("The computed property \"" + key + "\" is already defined in data."), vm);
        } else if (vm.$options.props && key in vm.$options.props) {
          warn(("The computed property \"" + key + "\" is already defined as a prop."), vm); }}}}Copy the code

The code above is quite long, but we can summarize the following points:

  1. var watchers = vm._computedWatchers = Object.create(null);invmOne is mounted on the instance object_computedWatchersProperty, saved bycomputedAll of the generatedwatcher
  2. And then go through all of themkeyEach key generates onewatcher
  3. var getter = typeof userDef === 'function' ? userDef : userDef.get;You can extend from this codecomputedAs follows:
Computed: {// notation 1: Directly onefunction
    // strLen: function () {
    //   console.log(this.newTodo.length)
    //   returnThis.newtodo.length //}, // it can be an object, but it must have a get method, // But it doesn't make sense to write it as an object, because other attributes are not used. strLen: { get:function () {
        console.log(this.newTodo.length)
        return this.newTodo.length
      }
    }
  }
Copy the code
  1. If it is not a server rendering, one is generatedwatcherObject and is saved invm._computedWatchersProperty, but this has nothing to do withwatchThe generatedwatcherOne important difference is that you pass a propertycomputedWatcherOptionsObject that is configured with a lazy: ture

In Watcher’s constructor, we have the following logic:

this.value = this.lazy
      ? undefined
      : this.get();
Copy the code

Because this.lazy is true, this.get() will not be executed; Therefore, the corresponding methods configured in computed are not executed immediately.

  1. defineComputed(vm, key, userDef);Is tocomputedProperties directly mounted invmUp, you can go straight throughvm.strLenIn this method, however, there is a distinction between server rendering and not server rendering, which is performed immediatelycomputedGets the value, but on the Web it is not executed immediately, but givengetAssign a function:
  function createComputedGetter (key) {
    return function computedGetter () {
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        if (watcher.dirty) {
          watcher.evaluate();
        }
        if (Dep.target) {
          watcher.depend();
        }
        return watcher.value
      }
    }
  }
Copy the code

If we reference computed properties in our template, such as:

{{strLen}}

, $mount is called to render the template, and the above computedGetter method is executed to obtain the value:

 Watcher.prototype.evaluate = function evaluate () {
    this.value = this.get();
    this.dirty = false;
  };
Copy the code

This.get () is the same as this.get() in watch analysis above.

Think about:

We have basically analyzed the basic process of computed logic above, but we still seem to have no connection. How can we notify computed updates when the value in our data changes? Our computed data is as follows:

  computed: {
    strLen: function () {
      return this.newTodo.length
    }, 
  }
Copy the code

What about strLen’s method when we change this.newTodo?

The answer:

  1. Above we have analyzed our in ourtemplateIn the referencestrLen, such as<div>{{strLen}}</div>, in the$mountTo render the templatestrLenAnd then it will be executedcomputedGetterMethod to get the value, and then callgetThe method, which is uscomputedConfigured functions:
  computed: {
    strLen: function () {
      return this.newTodo.length
    }
  },
Copy the code
  1. When the above method is executed, the reference is calledthis.newTodo, will enterreactiveGetterMethods (In-depth understanding of Vue responsive principles (data interception))
     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
      }
Copy the code

The current Watcher object is added to the DEP.subs queue.

  1. whenthis.newTodoWhen the value changes, it is executedreactiveSetterMethod when executeddep.notify();“, will be executedcomputedInside the way to achieve whendataWhen the value inside changes, it references thisdataProperties of thecomputedIt will be executed immediately.
  2. If we define itcomputedBut there is no reference to this anywherecomputedEven if the correspondingdataProperty changes are not executedcomputedMethod, even if executed manuallycomputedMethods, such as:app.strLenIt’s not going to work because it’sWatchertheaddDepMethod that has been judged for the currentwatcherNot a new onewatcher
  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