Skip to content

MaxArt2501/array-observe

Repository files navigation

Array.observe polyfill

This polyfill is actually meant to be a companion to the Object.observe polyfill (or any other Object.observe shim that doesn't already support Array.observe and doesn't natively detect the "splice" event) and it comes with a big warning (read the Under the hood section).

The reason of this split is because of its obtrusive nature.

Object.observe isn't a proposed spec anymore

You might have read this around, but back in November Object.observe proposal was withdrawn from TC39. This also means that Object.observe will be pulled from Chrome and other V8-based environments, and that would imply that developers shouldn't rely on it anymore. Web development evolved in the direction of functional programming and immutable objects, so that's where we all should look at.

Read more on the page for Object.observe.

Installation

This polyfill extends the native Array and doesn't have any dependencies, so loading it is pretty straightforward:

<script src="array-observe.js"></script>

Use array-observe.min.js for the minified version.

Using bower:

$ bower install array.observe

Or in node.js:

$ npm install array.observe

The environment must already support Object.observe (either natively or via polyfill), or else the shim won't be installed.

The "splice" event

According to the spec, Array.observe is basically like calling Object.observe with an acceptTypes option set to [ "add", "update", "delete", "splice" ], where the first three types are already supported by the aforementioned polyfill, and the fourth one isn't.

The "splice" change can't be easily detected by a "dirty checking" loop (it can only be approximated by some kind of "distance" algorithm, which is usually computationally expensive) but, still according to the spec, it's triggered by certain array operations that modify the array itself. Namely, those are the push, pop, shift, unshift and splice methods, plus others like these:

// Suppose beginning with `array` being an empty array

// Defining elements on indexes greater or equal to `array.length`
array[0] = "foo";

// Altering the `length` property (either increasing or decreasing it)
array.length = 3;

// Using `Object.assign`. The following will actually produce two separate
// "splice" events - basically equivalent to
//     array[3] = "bar";
//     array[4] = "baz";
Object.assign(array, { 3: "bar", 4: "baz" });

Additionally, the "splice" event is also triggered when one of the mentioned array methods are called on a non-array object:

var object = {};

Array.prototype.push.call(object, "foo");
// => object === { 0: "foo", length: 1 }

Under the hood

This polyfill wraps the native array push, pop, shift, unshift and splice methods so that they do a performChange call on the array's notifier. It's precisely what the spec says should happen.

Unfortunately, this is certainly an obtrusive way to generate "splice" changes, not to mention it reduces the performance of said methods. Benchmarks show that wrapped methods are from 6 times (for splice) to 400 times slower (for push) - YMMV depending on the executing environment (see the Benchmarks section later and some results).

In order to apply the polyfill also when calling array methods on generic objects, the methods are wrapped directly on Array.prototype, thus affecting all the arrays, even when not observed.

Moreover, this polyfill doesn't trigger a "splice" change when performing an array operation that does not use one of the above methods. That is handled normally in Object.observe, firing the usual "add", "update" and "delete" events.

Workaround for performance

If you don't want the array methods to be wrapped for every array, you can restore the original array methods using the _original property defined on the wrapped methods. You can attach the wrapped method only to the objects you want to observe, redefining Array.observe with something like this:

var wrapped = {},
    methods = [ "push", "pop", "unshift", "shift", "splice" ];

// Restoring the original array methods, and saving the wrapped ones
methods.forEach(function(method) {
    wrapped[method] = Array.prototype[method];
    Array.prototype[method] = wrapped[method]._original;
});

Array.observe = (function(observe) {
    return function(array, handler) {
        // Applying the wrapped methods to the observed array
        methods.forEach(function(method) {
            if (method in array)
                array[method] = wrapped[method];
        });
        observe(array, handler);
    };
})(Array.observe);

Tests

Tests are performed using mocha and assertions are made using expect, which are set as development dependencies. Assuming you're in the project's root directory, if you want to run the tests after installing the package, just do

cd node_modules/array.observe
npm install

Then you can execute npm run test or, if you have mocha installed globally, just mocha from the package's root directory.

For client side testing, just open index.html in your browser of choice.

Benchmarks

Some benchmarks have been created, using benchmark.js, testing the performances of the wrapped array methods (see some results).

After having installed the development dependencies (see above), open the index.html file in the benchmark/ directory in your browser of choice. To test node.js < 0.11.13, run npm run benchmark.

The benchmarks won't start if Array.observe is natively supported.

License

MIT. See LICENSE for more details.