エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

Vue Reactivity: Present and Future

f:id:nickhall:20181213111533j:plain

Hello again, faithful readers. I'm Nick from the Engineering Group, here to bring you another special English edition of the M3 Tech Blog! This article is Day 15 of the 2018 M3 Advent Calendar.

Continuing with the theme of my last entry, I'd like to use this space to talk a little more about Vue and how it works under the hood. Vue's reactivity system is elegant and intuitive, but certain technical limitations have left a few gotchas that can cause some head scratching and lost debugging time to newcomers. As we head toward Vue 3.0, however, we find that we can soon take advantage of newer browser features that help to alleviate some of the headaches caused by the present system.

Vue today: Object.defineProperty()

f:id:nickhall:20181212234344p:plain

The above image comes directly from the Vue docs on reactivity (日本語 here), required reading for anyone getting started with Vue.

In Vue, data binding is truly reactive -- it doesn't use techniques like dirty flags, but rather directly tracks and updates dependencies at the moment the data is updated. Vue accomplishes this by using Object.defineProperty(), an ES2015 feature (see the docs) to define reactive getters and setters on component data.

The code

The concept can be illustrated fairly succinctly:

let myObj = {}
let myVar = 1

Object.defineProperty(myObj, "myProp", {
  enumerable: true,
  configurable: true,
  get() {
    return `ya got me! ${myVar}`;
  },
  set(v) {
    myVar = v + 10;
  }
});

myObj.myProp
// ya got me! 1

myObj.myProp = 5
myObj.myProp
// ya got me! 15

When you build a component in Vue, you need to pass a data object to declare the local state. Upon initialization, Vue uses this object and iterates over the keys, using Object.defineProperty to attach the previously mentioned getters and setters to each of the values, recursively reaching child objects and array elements.

Using the reactive getter, the caller of the data can be registered as a dependency, allowing the watcher to trigger an update later when the value changes. So if you use one of Vue's computed properties, for example, it's registered as a dependency because it calls the data's attached reactive getter.

Reactive setters, similarly, alert the watcher that the data has changed (along with returning the value, of course). The watcher can then inform all of the registered dependencies that there has been a change and trigger a DOM update.

The gotcha

The system is great! It works! You can get and set your variables and the data is bound and updates beautifully! So what could the catch be, the curious reader might ask. Recall that Vue iterates over the component's data object when it's initialized, adding reactive getters and setters to array elements and object values. For data that exists at the time of creation, things work as expected. But what if you want to add a property to that object, or you want to add an element to that array?

{
  data: {
    messages: ["hello", "m3", "tech blog"]
  },
  methods: {
    replaceMessage() {
      this.messages[0] = "goodbye"; // oops - not reactive!
    }
  }
}

Yes, directly replacing or adding to arrays and property addition and deletion on objects breaks reactivity because these changes do not trigger any of the getters or setters. To circumvent this, you have to call Vue.set() to let Vue know that you're creating some reactive data. With arrays, you should not use the index to set it but rather use methods like splice(), which Vue can track.

Ultimately it is a minor inconvenience, but it can be easy to overlook a small property addition, or overwrite an array without thinking about it too carefully. Surely, you ask, is there no better way?

Vue tomorrow: Enter the proxy

The Vue reactivity system works, but as discussed, some pain points exist. In Vue 3.0, these issues will be addressed, and many of the issues with the current system based on Object.defineProperty() will be remedied.

ES6 introduced a new object called a proxy. As MDN states,

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

Basically, we can use proxy objects to handle Vue's reactivity tasks without the pitfalls that currently exist. With a proxy, detecting direct modification of arrays and object property addition and deletion is easy. To build a proxy, you need a handler, traps and a target. A handler is tells the proxy what behavior you want to wrap and how to handle it. Traps are the individual methods that the proxy handles, like get(), set(), apply(), etc. A target is the object that is being wrapped with the proxy. For full details, see the MDN docs.

The code

So let's reexamine the use cases for Vue reactivity. First, how can we handle directly adding to an array?

var target = [1, 2, 3]

var p = new Proxy(target, {
  set (obj, prop, value) {
    console.log(`Setting index ${prop} to ${value}`)
    obj[prop] = value
  }
})

console.log(p)
p[0] = 123
p[5] = 100
console.log(p)

Running the above code produces the following output:

[ 1, 2, 3 ]
Setting index 0 to 123
Setting index 5 to 100
[ 123, 2, 3, <2 empty items>, 100 ]

Neat! The proxy was able to detect the set operation on the array itself and notify us when a new item was directly added.

What about objects?

var target = { a: 1, b: 2 }

var p = new Proxy(target, {
  set (obj, prop, value) {
    console.log(`Setting key ${prop} to ${value}`)
    obj[prop] = value
  }
})

console.log(p)
p.a = 5
p.c = 10
console.log(p)

Same story, different object. The output is as expected:

{ a: 1, b: 2 }
Setting key a to 5
Setting key c to 10
{ a: 5, b: 2, c: 10 }

Recall that in Vue's current system, at the time the component is initialized Vue walks over an object or array and sets each value as reactive, not the object itself. When adding a new property, you must explicitly tell Vue that the change occurred. With proxies, we can watch the object itself, allowing for easier management of reactive data and eliminating the need to use Vue.set() while at the same time providing improved speed and memory usage.

The gotcha

f:id:nickhall:20181213110919p:plain

IE11 does not support proxy objects. For many this is a dealbreaker as IE11 support is still an unfortunate must for their applications. We cannot allow progress to be halted because of a few stubborn holdouts on the browser front, but in recognizing the need for IE11 support, the Vue team has announced that while Vue 3.0 will use proxy-based reactivity (while dropping IE11 support), they will nonetheless offer a separate (larger) build with a reactivity system that is still compatible with IE11.

In summary...

Vue 3.0 is going to bring a huge number of improvements over Vue 2.0, the reactivity system being just one. Reactivity until now has been, for the most part, easy, but the necessity to understand little caveats about how Object.defineProperty() works add an unfortunate roadblock to a framework otherwise known for being accessible and easy to learn. This improvement is one of many that propel Vue forward toward 3.0 in its goal to be smaller, faster and easier to use.

We're hiring!!

If you like Vue, you'll find yourself in good company here at M3. We sponsored this year's Vue Fes Japan and are always looking to hire strong team members to help us build first-rate front-end products! Why not come by for a casual chat?

jobs.m3.com