In this post, we will more learn about how Vue.js Computed Properties work by writing a very (very very very) simple implementation which achieves similar functionality.

  1. This is just to illustrate how it works. It doesn't support objects, arrays, watching/unwatching and a dozen performance optimizations that exist in vue.js core.
  2. I wrote this after reading the Vue.js source code, based on my very limited understanding. A lot of it might be wrong. Please email me if you find any corrections or if I'm flat out wrong.

JS Properties

JavaScript has a feature Object.defineProperty. It can do quite a bit but let's focus on one thing:

var person = {};

Object.defineProperty (person, 'age', {
  get: function () {
    console.log ("Getting the age");
    return 25;
  }
});

console.log ("The age is ", person.age);

// Prints:
//
// Getting the age
// The age is 25

Even though person.age looks like we're accessing a part of the object, we're internally running a function.

A basic Vue.js Observable

Vue.js has a basic construct which lets you turn a regular object into an "observed" value called an "observable". Here's an over-simplified version of how to add a reactive property

function defineReactive (obj, key, val) {
  Object.defineProperty (obj, key, {
    get: function () {
      return val;
    },
    set: function (newValue) {
      val = newValue;
    }
  })
};

// create an object
var person = {};

// add a reactive property called 'age' and 'country'
defineReactive (person, 'age', 25);
defineReactive (person, 'country', 'Brazil');

// now you can use `person.age` any way you please
if (person.age < 18) {
  return 'minor';
}
else {
  return 'adult';
}

// Set a value as well.
person.country = 'Russia';

Interestingly, the actual value 25 and 'Brazil' is still inside a "closure variable" val and is modified when you set the value. person.country doesn't contain the actual value, instead the getter function's closure contains the value.

Defining a computed property

Let's create a function for defining a computed property defineComputed. This is how a user might use it.

defineComputed (
  person, // the object to create computed property on
  'status', // the name of the computed property
  function () { // the function which actually computes the property
    console.log ("status getter called")
    if (person.age < 18) {
      return 'minor';
    }
    else {
      return 'adult';
    }
  },
  function (newValue) {
    // called when the computed value is updated
    console.log ("status has changed to", newValue)
  }
});

// We can use the computed property like a regular property
console.log ("The person's status is: ", person.status);

Let us write a simple implementation of defineComputed. This will support calling the compute function, we won't support updateCallback right now

function defineComputed (obj, key, computeFunc, updateCallback) {
  Object.defineProperty (obj, key, {
    get: function () {
      // call the compute function and return the value
      return computeFunc ();
    },
    set: function () {
      // don't do anything. can't set computed funcs
    }
  })
}

There are some issues with this:

  1. It'll run the compute function each time the propety is accessed.
  2. It doesn't know when to update.
// We want something like this to happen

person.age = 17;
// console: status has changed to: minor

person.age = 22;
// console: status has changed to: adult

Adding a dependency tracker.

Let's add a global object called Dep

var Dep = {
  target: null
};

This is the 'dependency tracker'. Let's modify the defineComputed function with one key trick.

function defineComputed (obj, key, computeFunc, updateCallback) {
  var onDependencyUpdated = function () {
    // TODO
  }
  Object.defineProperty (obj, key, {
    get: function () {
      // Set the dependency target as this function
      Dep.target = onDependencyUpdated;
      var value = computeFunc ();
      Dep.target = null;
    },
    set: function () {
      // don't do anything. can't set computed funcs
    }
  })
}

Let's now go back to how we define a reactive property.

function defineReactive (obj, key, val) {
  // all computed properties that depend on this
  var deps = [];

  Object.defineProperty (obj, key, {
    get: function () {
      // Check if there's a computed property which 'invoked'
      // this getter. Also check that it's already not a dependency
      if (Dep.target && ) {
        // add the dependency
        deps.push (target);
      }

      return val;
    },
    set: function (newValue) {
      val = newValue;

      // notify all dependent computed properties
      deps.forEach ((changeFunction) => {
        // Request recalculation and update
        changeFunction ();
      });
    }
  })
};

We can update the onDependencyUpdated function in the computed property definition to now trigger the update callback.

var onDependencyUpdated = function () {
  // compute the value again
  var value = computeFunc ();
  updateCallback (value);
}

Putting it all together.

Let's re-visit the definition of our computed property person.status

person.age = 22;

defineComputed (
  person,
  'status',
  function () {
    // compute function
    if (person.age > 18) {
      return 'adult';
    }
  },
  function (newValue) {
    console.log ("status has changed to", newValue)
  }
});

console.log ("Status is ", person.status);

Step 1:

get() is called on the person.status property. This sets Dep.target to it's callback.

Step 2:

get() on person.status calls the compute function. This in turn calls get() on person.age property because the callback function needs this value.

Step 3:

person.age's get() checks with Dep to see if a target is available. And stores it as a dependency

Step 4:

The compute function gets the new value and returns it. But now person.age knows to notify person.status whenever it's value updates.

Live example

JS Bin on jsbin.com

Previous Post