Object.observe is dead, long live ES6 Proxies

Justin Meyer by Justin Meyer

Object.observe is dead, long live ES6 Proxies

Justin Meyer Learn how ES6 Proxies are a much improved version of Object.observe and solve some of the core challenges of JavaScript framework authors.

posted in Open Source ,Development on February 2, 2016 by Justin Meyer

Observables and "computed" values are something that every modern framework provides in some fashion. Frameworks that have explicit observables and computed values (Ember, Knockout, CanJS, etc) can provide high performance updates and, in my opinion, a more natural development experience.

var fullName = can.compute(function(){
  return person.attr("first")+" "+person.attr("last")
});

However, frameworks like Angular and React, that can use plain JS objects, lend themselves to being more popular, but accomplish computed values with sub-optimal dirty checking or diffing.

$scope.fullName = function() {
  return $scope.firstName + " " + $scope.lastName;
}

Object.observe was viewed as a solution for observables but was recently retracted.

I was happy to see this because Object.observe did not help the computed value problem. ES6 already has a data structure that solves observables, computed values, and a wide variety of other framework/library problems - Proxies.

In the remainder of this article, I'll explain how proxies support explicit observables, computed values, and a more natural DOT operator (.) based syntax. Essentially, Proxies make the best of both worlds possible. I'll conclude by politely asking browser vendors, especially mobile browser vendors, to add this feature.

Frameworks with explicit observables

Ember, Knockout, CanJS, and many other frameworks have explicit observables and computed values or properties. For example, in CanJS, the following creates a fullName compute that updates whenever any of its source observables change:

var person = new can.Map({first: "Justin", last: "Meyer"});

var fullName = can.compute(function(){
  return person.attr("first")+" "+person.attr("last")
});

fullName.bind("change", function(ev, newVal, oldVal){
  // newVal => "Vyacheslav Egorov"
  // oldVal => "Justin Meyer"
})

// causes change event above
person.attr({first: "Vyacheslav", last: "Egorov"});

This works by having .attr(prop) tell the computed system that some observable value is being read. The computed system listens for those changes. Computes are like event streams. They are slightly less powerful, but much easier to use.

Knockout is similar:

function AppViewModel() {
    this.firstName = ko.observable('Bob');
    this.lastName = ko.observable('Smith');
    this.fullName = ko.computed(function() {
        return this.firstName() + " " + this.lastName();
    }, this);
}

The developer doesn't have to explicitly tell the compute what is being read. This information is inferred as the function is run. This allows a great deal of flexibility. For instance, plain JavaScript functions can be composed:

var person = new can.Map({
  first: "Justin", 
  last: "Meyer"
});
var hobbiesList = new can.List([
  "basketball",
  "programming"
]);

var fullName = function(){
  return person.attr("first")+" "+person.attr("last")
};

var hobbies = function(){
  return hobbiesList.join(",")
}

var info = can.compute(function(){
  return fullName()+" likes "+hobbies();
});

Direct observables and computes allow frameworks to only update exactly what is needed without having to dirty check or signal an update and diff.

Note: In an article that will be released next week, we will show how observables can be used to update the DOM algorithmically faster than dirty checking (in logarithmic instead of linear time).

Frameworks without explicit observables

Frameworks without explicit observables, like Angular and React are more popular. Users like using the DOT(.) operator. But it comes at a cost.

Angular

In Angular, you can do something like:

$scope.fullName = function() {
  return $scope.firstName + " " + $scope.lastName;
}

This will use dirty checking to continuously read the value of fullName. You can do something like:

var fullName = function() {
  $scope.fullName = $scope.firstName + " " +
                    $scope.lastName;
}
$scope.$watch('firstName', fullName, true);
$scope.$watch('lastName', fullName, true);

but you have to write out the values you care about twice:

  • Once to compose the value ($scope.firstName).
  • Twice to bind the value ($watch('firstName',...).

Gross!

React

With React/JSX, you might do something like:

render () {
  var fullName = this.props.firstName + " " + 
                 this.props.lastName;
  return (
      <div>
        <h2>{fullName}</h2>
      </div>
    );
}

 

Free Javascript checklist. Keep track of your project and ensure high quality.
Instead of dirty checking, React requires a call to setState. This is similar in style to updating an observable's value (like CanJS's .attr). However, setState doesn't produce events. Instead, React re-renders the template to a virtual DOM and calculates DOM updates by taking a difference of the virtual DOM and the actual DOM. The re-render of the entire template and diff are each O(n) at best. With observables, there's no re-render and diff necessary.

 

Enter proxies

Proxies will allow frameworks to provide explicit observables with an Object-like API that uses the DOT (.) operator. For example:

var person = can.observable();
person.first = "Justin";
person.last = "Meyer";

var fullName = can.compute(function(){
  return person.first+" "+person.last
});

// causes change event above
person.first = "Vyacheslav";
person.last = "Egorov";

I showed it this way because users do not want to define all properties up front, making Object.defineProperty and getter/setters an unviable option. I made a version of this that works in browsers that support Proxy here.

With Proxies, Angular could remove the need for dirty checking (assuming $scope is a proxy). React's render function could know when to call itself, removing the meatball in the middle of many flux apps (I can elaborate on this more in the comments).

With Proxies, Knockout, CanJS, and Ember users could use the familiar DOT(.) operator.

Can I use it?

Search for "Proxy" in this compatibility table to see the current state of Proxy support. Desktop support is very promising. Proxies have been in Firefox for a very long time. They are also in Edge and in Chrome Canary. Great job browser teams!

The hold out has been Safari and mobile browsers.

If you're a Safari or mobile browser developer, please consider prioritizing proxies. Adding proxies might have as big of an impact as any other ES6 feature to an average developer. Average developers wouldn't use it directly, but it would be an invaluable tool to library and framework authors.


Also published on Medium.

Create better web applications. We’ll help. Let’s work together.