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

Next Post Previous Post