Analysis of Vue Computed principle from the perspective of source code

preface

For the calculated attribute, you have to ask it almost every interview. Before that, you have to rely on your own understanding of what is used in the business, but this is still too shallow and often not the answer required by the interviewer. So I just want to read the computed source code and know its working principle, so that I can really understand it.

Let's take a look at initialization first

Follow the source code to find out where Computed is initialized

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

function initMixin(Vue) {
    Vue.prototype._init = function(options) {
	...
	initState()
	...
    }
} 

function initState() {
    ...
    if (opts.computed) initComputed(vm, opts.computed)
    ...
}

As you can see, when we use new Vue(...) When called, initialization occurs. It will be called to the initState() function, which calls initComputed to initialize computed.

What did initComputed do?

Open initComputed to see a long piece of code. In order to understand the code more easily, we can discard some output warning information and server rendering, so as to extract a short piece of code for analysis. Subsequent codes will also be processed accordingly.

const computedWatcherOptions = { lazy: true }

function initComputed (vm, computed) {
  const watchers = vm._computedWatchers = Object.create(null)

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
  
    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
    )

    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } 
  }
}

In order to let everyone understand thoroughly, we will take out some code to explain.

1. This code is to assign a watcher to each computed. Watcher is a very important class in the whole Vue. Some watcher codes will be posted later for analysis.

watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    computedWatcherOptions
)

2. Use the function defineComputed to process computed

if (!(key in vm)) {
    defineComputed(vm, key, userDef)
} 

To sum up, initComputed does the following two things for each computed:

  • Assign Watcher
  • Processing with definComputed

Take out these two things and make a separate analysis

What did Watcher do?

We can see from the initComputed code just now that the parameters passed when creating the Watcher should be as follows (Note: noop = function(a, b, c) {})

new Watcher(vm, function c() { return this.a }, noop, { lazy: true }, undefined)

According to this parameter, throw away some judgments and take a look at Watcher's simplified code

function Watcher(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm;

    this.dirty = this.lazy = !!options.lazy; 

    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    }

    this.value = this.lazy ? undefined : this.get();
}

According to the code, you can see that the watcher does the following things

1. Assign both dirty and lazy to true.
Lazy here indicates that the watcher needs to be cached, and dirty indicates whether to fetch the cached value or recalculate. It can be understood that lazy is a cache switch, and dirty marks whether the cache is still valid.

this.dirty = this.lazy = !!options.lazy; 

2. Cache the computed getter

this.getter = expOrFn;

3. The calculated value will be obtained by get. Here, the condition judgment is formed according to lazy. In this way, the obtained value will not be calculated during initialization, but get() will be called from other places later. Let's look at the get function later.

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

What does definComputed do?

Take a look at the definComputed Code:

var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
};

function defineComputed (
    target,
    key,
    userDef
  ) {

    if (typeof userDef === 'function') {
      sharedPropertyDefinition.get = createComputedGetter(key)
      sharedPropertyDefinition.set = noop;
    } else {
      sharedPropertyDefinition.get = userDef.get
        ?  createComputedGetter(key): noop;
      sharedPropertyDefinition.set = userDef.set || noop;
    }

    if (sharedPropertyDefinition.set === noop) {
      sharedPropertyDefinition.set = function () {};
    }

    Object.defineProperty(target, key, sharedPropertyDefinition);
  }

There are two main things in the above code

  1. Use the createComputedGetter function to create a getter '
  2. Use object Modify the defineproperty to the get ter just created
createComputedGetter

There are a lot of things done in createComputedGetter, which also involves the principle of computed, so I want to talk about it separately.

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

1. Since all created watchers are cached when creating watchers in initComputed, the watcher of the response will be retrieved directly according to the key.

var watcher = this._computedWatchers && this._computedWatchers[key];

2. Dirty here is true by default, so evaluate() will be executed for calculation. Evaluate () does only two things: 1 Use the get() function to get the value, 2 Set the dirty property to false

if (watcher.dirty) {
     watcher.evaluate();
}

evaluate code

Watcher.prototype.evaluate = function evaluate () {
    this.value = this.get();
    this.dirty = false;
}
Uncover secrets (get)

Let's take a look at the code of get

Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      ...
    } finally {
      ...
      popTarget();
    }
    return value
}

Although there is not much code, there are many other functions associated with get. For ease of understanding, let's illustrate with examples

Example:

var app = new Vue({
  el: '#app',
  data: {
    a: 'a'
  },
  computed: {
    c () {
      return this.a
    }
  },
  methods: {
    changeA () {
      this.a = 'changed'
    }
  }
})
<div id="app">
    <div>{{c}}</div>
    <button @click="changeA">Change A</button>
</div>

Page:

Combined with the above example, let's split the get operation
1.pushTarget(this) pushes the current watcher into the stack and makes Dep.target the current watcher, that is, the watcher of c. (it is convenient for subsequent use. If it is a watcher of a certain value, it is abbreviated as watcher(xx).)

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

2. Call the getter function of the current watcher cache. From the example, we can know that getter is

function c() { return this.a }

Therefore, the getter of a will be called.
A used defineProperty to handle initData, and set getter and setter Let's take a look at what the getter of a does, which is also the simplified code:

var value = getter ? getter.call(obj) : val;
if (Dep.target) {
   dep.depend();
}
return value

Currently, Dep.target belongs to watcher(c), so it will go to dep.dependent().

Let's take a look at the source code of dependent()

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

According to the above code:

  • this is a because it came in from a's getter
  • Dep.target is the watcher(c).

Dep.target.addDep(this) is actually the calling code watcher Adddep, and then take a look at the watcher Adddep 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);
      }
    }
}

This code does two things:

  • a is added to the newDeps of c, which will be added to deps later
  • a's subs join the watcher(c).

Therefore, the Dep.target(watcher(c)) data structure should be:

3. Finally, the popTarget() operation is performed to push the watcher(c) out of the stack. At this time, Dep.target is the watcher of the current page. Then it returns value.

So far, the get() function has been executed.
Put this When dirty is set to false, evaluate is completed

Keep going. The createComputedGetter code will come here

if (Dep.target) {
    watcher.depend();
}

Next, watch What does depend () do

watcher.depend()

Take a look at the watcher Dependent () source code:

Watcher.prototype.depend = function depend () {
    var i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
}

Step by step disassembly:

  • this is the watcher(c)
  • It can be seen from the above figure that the deps of the watcher(c) is only a, so it actually takes a to depend()
  • When dependent () is executed, get said that Dep.target will be restored to the original after evaluate, so Dep.target at this time is the page. Therefore, after execution, deps of watcher(page) will be added to a, and subs of a will be added to watcher(page).

The watcher structure of the final page is:

So how is the page updated?

The initData mentioned just now only talks about the getter method of a attribute. When we click change a, the value of a changes and the setter method of a will be called. dep.notify() is called in the setter of A.

Take a look at the notify Code:

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

Take out the current subs from notify. As mentioned above, traverse and call the update of the watcher in the subs.

Take another look at the update function

Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
};
  • When calling c's update, the value of lazy is true, so the value of dirty will also be set to true.
  • When the update of the page is called, the update of the page will be triggered. At this time, the page references c, and the dirty value of c becomes true, and the value will be calculated from a again.

From the above, it is not difficult to see that c has nothing to do with page update. In fact, it only takes a calculated value. What is really related to page update is a.

summary

Because I haven't done much interpretation of the source code, the descriptions in some places may not be very appropriate or detailed. If you don't understand, you can leave a message and tell me. I will answer them one by one within my ability

Keywords: Javascript Front-end Vue Vue.js

Added by e39m5 on Fri, 24 Dec 2021 08:46:20 +0200