Eventful Javascript

let data = {child:[]};
treevent.Listen(data, "child[*].name[0]",  
  (path, params, type, index, oldValue, newValue) => {  
   console.log("woah...");  
  }  
 );
data.child.unshift({name: ["first", "last"]})

For any front-end developers out there, you may have noticed the increasing popularity of reactive / event-driven / data-binding architectures. Call it what you like, but things like React, RxJS, Angular, Web Components, Meteor, Firebase, ... etc are all built along similar principles.

Whenever asked about them, I always recommend usage (it's a much nicer style of coding that a non-binding way) but I also tend to complain that 'arrays aren't done properly', which understandably confuses people, as it's intentionally reductionist. There's obvious things, like Firebase's lack of native array support, but I realized that what I more meant was that array mutations weren't handled well, which means that mutation bubbling is not possible, along with a bunch of other things that feel like should work to have a more useful feature set.

Bubbling
What do I mean by bubbling events? Those who have used JS will already be familiar with this property of DOM events: you can listen for clicks on an element, and clicking on any of its children will trigger the listener.

However, I am not aware of anything similar with JS objects. There are use-cases for it - e.g. scheduling an RPC to sync data model changes with a server, it's much nicer to listen once at the model root, rather than once at each object or array within the model. There are ways to get around it (like, use immutable objects, so each child mutation ends up as a reference change at the root too) but they're usually different to the DOM version of listening which has worked well for a while now.

Array bubbling
So, rather than complaining, I thought...I might as well writing something that gives DOM-style bubbling :) While the concept is simple (attach some metadata to each object pointing to its parent - ES6's Symbol is great for this), this is where my Array problem from above comes in. You see, if you insert a value at the start of an array (at [0]), this should not trigger events for every element after it, even though [1] changes, [2] changes, ...etc. The difference here is the difference between Object.observe and Array.observe - the former supports changing values, while the latter needs to also accept insertion and removal (or really splice, which has both).

What's more though, keeping track of your position is harder - not only do you not want to send out O(n) events on an insertion at [0], but you also don't want to have to update O(n) things with their new indices. Thankfully, any balanced binary tree implementation should get around this, at the cost of making all array write operations O(log n). This is bad for large arrays with constant changes (like, time-series data), but ideally these would be avoided in most UIs, and alternative implementations can be provided which optimize by restricting the available API (e.g. deques, rather than random access arrays).

Better paths
Given that I'd decided to implement this, I figured I may as well also add a useful feature I'd been missing from DOM events, and which seemed very useful for JS object events and didn't add much implementation efficiency cost: path wildcards and matchers. The concept comes from the following scenarios:

1) You want to sum the prices of all items in a list. You normally would listen to the list, but in this case, you might be updating other irrelevant properties a lot (like, the name, or maybe the time since last viewed). So instead, you can listen to [items.*.price], to only be notified when a price changes.

2) You have an object representing your entire contact database for a user (using something like falcor), and want to know when people update their photos. You might listen to [contacts.*.photo.url], however inside the event it's convenient to also know whose photo has changed. This is simple processing that has to be performed by the path matcher anyway, so it's nice to be able to listen instead to [contacts.{userId}.photo.url], and {userId} will be filled in on listener trigger.

3) Sometimes, you want to listen to a single property - e.g. root.my.child.path.
However, this needs to trigger when any middle object changes - e.g. if root.my.child.path = true, then root.my.child.path = false, root.my.child = {path: false}, and root.my = {child: {path: false}}, should all trigger a listener on root to [my.child.path].

4) In the opposite case, sometimes you want to listen to all child events (after all, that's what bubbling is for!). For this case, an all-child matcher ('**') can be useful, e.g. your RPC mechanism can merge events under [some.path.**] and that will trigger even when some.path.deep.under[0] changes (and tell you it happened at 'some.path.deep.under.0')

Treevent
All of these (and a little more) have been made possible to some extent in my poorly named Treevent library, available on github. It uses ES6 (for things like Symbol) and typescript (because the benefits of developing in typed languages far, far, far, far outweighs the cost) to provide a Listen method which accepts any non-primitive, non-null JS object and will invoke a listener in response.

The API (and documentation, and tests ...) are still clunky and could do with a lot of improvement, but I'm curious to hear what people think about the idea, and whether there are any good usages already that it could help with, or ideas for new features, or certain obvious flaws with the approach.

For one, I'm aware from the growing move towards immutable objects in JS, which is nice for state management, but I feel partly is done to make subtree events faster via quick tree diffing - it seems better to solve subtree events directly. That said, some sort of treevent solution can also be applied in the immutable world (i.e. properly positioning all the mutations by path), so it'd be interesting to see the two merged as well, as treevent currently just uses the default mutable JS object substrate.



Comments

  1. https://www.chromestatus.com/features/6147094632988672 :(

    ReplyDelete
    Replies
    1. It's unfortunate, but no biggie - there's also stuff like:

      https://github.com/MaxArt2501/object-observe
      https://github.com/polymer/observe-js

      etc, plus if people are moving more towards immutable objects, then there'll be other hooks into mutation operations.

      Delete

Post a Comment

Popular posts from this blog

Beans!

Comment Away!

Sounds good - part 1