Understanding Vue.js Reactivity in Depth with Object.defineProperty()

Vue.js logo

I’m coming from a Java background, so when I started with JavaScript many years ago it kinda felt weird to not have getters and setters but somehow I got used to it and as time went by I began to like it because it lead to much cleaner code compared to thousands of getter/setter lines in java. For example, check out the following Java Code:

class Person{
  String firstName;
  String lastName;

  // constructor omitted for this demo ;-)

  public void setFirstName(firstName) {
    this.firstName = firstName;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setLastName(lastName) {
    this.lastName = lastName;
  }

  public String getLastName() {
    return lastName;
  }
}

// Create instance
Person bradPitt = new Person();
bradPitt.setFirstName("Brad");
bradPitt.setLastName("Pitt");

A JavaScript developer would never go though the hassle of doing something like this but instead:

var Person = function () {
};

var bradPitt = new Person();
bradPitt.firstName = 'Brad';
bradPitt.lastName = 'Pitt';

It’s much simpler. And simpler is usually better, isn’t it?

Sure it is, but sometimes though, I felt like it would be useful if object properties could be modified when they’re accessed without the caller knowing anything about it. For example, let’s expand our Java code with a new method getFullName():

class Person{
  private String firstName;
  private String lastName;

  // constructor omitted for this demo ;-)

  public void setFirstName(firstName) {
    this.firstName = firstName;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setLastName(lastName) {
    this.lastName = lastName;
  }

  public String getLastName() {
    return lastName;
  }

  public String getFullName() {
    return firstName + " " + lastName;
  }
}

Person bradPitt = new Person();
bradPitt.setFirstName("Brad");
bradPitt.setLastName("Pitt");

// Prints 'Brad Pitt'
System.out.println(bradPitt.getFullName());

fullName is a computed property in this case. It doesn’t really exist as a private attribute but it will always return the correct full name.

C# and implicit getter/setters

Looking at languages like C# there is a feature of implicit getters/setters which I really liked. Here, you can define getters/setters if you want, but you don’t have to, but if you decide to do so the caller won’t have to call a function. Instead, all he/she needs to do is keep accessing the property directly and the getter/setter will be called automagically under the hood:

public class Foo
{
    public string FirstName {get; set;}
    public string LastName {get; set;}
    public string FullName {get { return firstName + " " + lastName }; private set;}
}

… which is pretty cool I think.

Now, if I wanted to achieve something similar in JavaScript, I most of the time ended up with something like this:

var person0 = {
  firstName: 'Bruce',
  lastName: 'Willis',
  fullName: 'Bruce Willis',
  setFirstName: function (firstName) {
    this.firstName = firstName;
    this.fullName = `${this.firstName} ${this.lastName}`;
  },
  setLastname: function (lastName) {
    this.lastName = lastName;
    this.fullName = `${this.firstName} ${this.lastName}`;
  },
};
console.log(person0.fullName);
person0.setFirstName('Peter');
console.log(person0.fullName);

This will print:

"Bruce Willis"
"Peter Willis"

It works but it’s not very “javascripty” to use method names like setXXX(value).

So, the following approach solves this problem:

var person1 = {
  firstName: 'Brad',
  lastName: 'Pitt',
  getFullName: function () {
    return `${this.firstName} ${this.lastName}`;
  },
};
console.log(person1.getFullName()); // Prints Brad Pitt

Here, we’re back to a computed getter. You can set first and last name by simply assigning the values:

person1.firstName = 'Peter'
person1.getFullName(); // returns "Peter Pitt"

So, that’s better but I still don’t like it because we have a method that starts with “getXXX()” which is not very javascripty as well and for many, many years I thought that this is how I’d have to live with JavaScript.

And then there was Vue

After posting quite a lot of Vue.js related video tutorials on my YouTube channel I got used to the ease of use regarding reactivity or as we called it in good old Angular1 times: Data binding. It seems so easy. All you have to do is define something in the data() section of a Vue instance and then bind it to the HTML:

var vm = new Vue({
  data() {
    return {
      greeting: 'Hello world!',
    };
  }
})
{greeting}

This will print “Hello world!‘ to the UI obviously.

Now, if you changed the value of “greeting” the Vue engine will react to this and update the view accordingly.

methods: {
  onSomethingClicked() {
    this.greeting = "What's up";
  },
}

So, for quite some time I’ve been wondering how this works? Is there some kind of event that triggers when an object’s property changes? Or does Vue constantly fire setInterval to check for changes?

Looking into the Vue documentation I understood that changing an object’s property will implicitly call its getter/setters which again notify a watcher which then triggers a re-render as explained in this illustration from the official Vue.js docs:

Vue.js reactivity explained
Vue.js reactivity explained – Source https://vuejs.org

But I still wondered:

  • How do the getter/setters get there in the first place?
  • How are these implicitly called?

Well, the first one is quite easy: Vue does that for us. Whenever you add new data, Vue will walk through its properties and add setter/getters there. But how are these called if all you do is foo.bar = 3?

The answer to this question appeared to me after a short conversation with SVG & Vue expert Sarah Drasner on Twitter:

Twitter conversation with Sarah Drasner about Vue reactivity
Twitter conversation with Sarah Drasner about Vue reactivity

So, obviously she also referenced the docs which I’d read before, so I started to dig into Vue’s sources on GitHub to better understand what’s going on. It wasn’t easy since I didn’t know the code and also didn’t really know what to look for. But after a while I remembered that the docs stated something about a method called Object.defineProperty(), so I searched for that and voilà:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const 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) {
      const 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 (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

So, the answer was there all the time in the docs:

“When you pass a plain JavaScript object to a Vue instance as its data option, Vue will walk through all of its properties and convert them to getter/setters using Object.defineProperty. This is an ES5-only and un-shimmable feature, which is why Vue doesn’t support IE8 and below.”

I was simply to stupid to understand what Object.defineProperty() actually does, so let me break it down to you with a simple example:

var person2 = {
  firstName: 'George',
  lastName: 'Clooney',
};
Object.defineProperty(person2, 'fullName', {
  get: function () {
    return `${this.firstName} ${this.lastName}`;
  },
});
console.log(person2.fullName); // Prints "George Clooney"

Remember the implicit getters from C# at the beginning of this article? This is pretty much the same but for JavaScript and it seems it has been around since ES5. All you have to do is define those on an existing object using Object.defineProperty() and when ever you access the property, the getter will be called automagically – and that’s exactly what Vue does internally when you add new data.

Could Object.defineProperty() simplify Vuex?

Having learned all this I’ve been wondering if this couldn’t be something that could be used to simplify Vuex? I mean there are so many new terms in there which are not really necessary and make things overly complicated and hard to understand for beginners in my opinion (same goes for Redux btw):

  • Mutator – Or do you mean (implicit) setter?
  • Getters – Why not use Object.defineProperty() to set an (implicit) getter instead?
  • store.commit() – Why not simply call foo = bar; instead of store.commit("setFoo", bar);?

What do you think? Does Vuex have to be as complicated as it is or could it be simplified with Object.defineProperty()?

Follow me on social media

11 thoughts on “Understanding Vue.js Reactivity in Depth with Object.defineProperty()

  1. @Gustavo Yeah, I saw that but haven’t tried yet. Do you have some hands on experience with it? How is it?

  2. @Gustavo Just saw that it doesn’t really support vue-devtools which was one of the main keypoints introducing Vuex at all, wasn’t it (besides having states globally)?

  3. If you want a “simple” Vuex, you could use a global Vue instance and just use it as you’re alluding to. However, I tried this and ran into problems. The biggest being asynchronous calls. Vuex is a little too wordy I agree, and I’d like to see shortcuts for simple mutations, but once you start using the async-Actions, serial-Mutatations the Vuex model really shines over using a less capable approach.

  4. @Anonymous Sure, Vuex definitely has it’s right to exist. I just wondered why it had to introduce so many new names for existing things? (Or was it Redux’ fault?) For example, a mutator is nothing else but a setter. So why not just call it a setter? Also, if it would be using Object.defineProperty() it would be easier to get and set values by just assigning/reading properties instead of having to call a mutator function each time. Actions are fine though.

  5. Vuex isn’t about just about having a global store for your data…it’s about making changes explicit and easy to track. The mutations give you a data trail which leads up to understanding how the current state came about. It’s about business logic, not about the end result of the state per se.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.