The Official Vue guide for “Computed Properties” explains the caching functionality like this:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // a computed getter
    reversedMessage: function () {
      // `this` points to the vm instance
      return this.message.split('').reverse().join('')
    }
  }
})

computed properties are cached based on their dependencies. A computed property will only re-evaluate when some of its dependencies have changed. This means as long as message has not changed, multiple access to the reversedMessage computed property will immediately return the previously computed result without having to run the function again.

In this post, I’ll explain how the caching part is implemented. (May be by next week I’ll make a post about how the computation is done when the dependencies are changed.)

(Check this post to see how the proxy behaviour of the data object is implemented.)

The basic idea is very simple:

function age() {
  console.log('age...');
  return 42;
}

function Person(name) {
  this.name = name;
  this.age = age();
}

john = new John('John');

john.name // 'John'
john.age // 42
john.age // 42

This is a simple Person function with name and age property. Notice that the every time ‘age’ is accessed off an instance (john), the consolg.log('age...') doesn’t get executed. It’s only executed at the very first time john is instantiated. The value returned by the age() function call is simply stored in memory.

In VueJS source code, the computed property’s caching behaviour is implemented using this idea.

As in the previous post, you need not know about VueJS to understand this post. This is just an exercise in reading and understanding an expert’s code.

We’ll be building off the myVue constructor function that we created in the last post that emulates the actual Vue function.

function myVue(opts) {
  this._data = opts.data;
  Object
    .keys(opts.data)
    .forEach(key => {
      sharedPropDef.get = () => this._data[key];
      sharedPropDef.set = (val) => { this._data[key] = val; };
      Object.defineProperty(this, key, sharedPropDef);
    });

  if (opts.computed) initComputed(this, opts.computed);
}

The new addition is the last line. If computed prop is present in the passed options object, then we call the initComputed function.

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

  for (const key in computed) {
    value = computed[key];
    vm._computedWatchers[key] = new Watcher(vm, value);

    if (!(key in vm)) {
      sharedPropDef.set = noop;
      sharedPropDef.get = function () {
        const watcher = vm._computedWatchers && vm._computedWatchers[key];
        if (watcher) return watcher.value;
      };
      Object.defineProperty(vm, key, sharedPropDef);
    } // if
  } // for 
}

The noop and sharedPropDef referenced are declared at the top like so:

const noop = function () {};
const sharedPropDef = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
};

Since we are calling the computed property directly on the Vue instance itself, we know that we’d need a property on the instance for each computed property. The last line does that with Object.defineProperty. We are defining each of those props as props with a dummy setter and a proper getter. (getters and setters).

For each computed property, a watcher instance is created, and stored in an internal map object, inside the Vue instance: _computedWatchers. Later on, we lookup this map using the computed property’s name to fetch its corresponding watcher instance.

The watcher instance is created using the Watcher class. It’s just an ES6 glorified entity defined exactly as the Person constructor function shared earlier.

class Watcher {

  constructor(vm, valueFn) {
    this.vm = vm;
    this.getter = valueFn;
    this.value = this.get(); // this is the key line
  }

  get() {
    let value;
    const vm = this.vm;
    value = this.getter.call(vm);
    return value;
  }

}

You can now see the cached computed property in action:

data = { msg: 'hello' };

computed = {
  reversedMsg() {
    console.log('reversed...'); // printed only once during vm initialization
    return this.msg.split('').reverse().join('');
  },
  upperCased() {
    console.log('uppercased...'); // printed only once during vm initialization
    return this.msg.toUpperCase();
  }
};

opts = { data, computed };

vm = new myVue(opts);

console.log(vm.reversedMsg); // 'olleh'
console.log(vm.reversedMsg); // 'olleh'

console.log(vm.upperCased); // HELLO
console.log(vm.upperCased); // HELLO

The full working example is split as 2 files: vuejs-source.js and watcher.js.