-
-
Save ebidel/1b553d571f924da2da06 to your computer and use it in GitHub Desktop.
// An `Object.observe()` "polyfill" using ES6 Proxies. | |
// | |
// Current `Object.observe()` polyfills [1] rely on polling | |
// to watch for property changes. Proxies can do one better by | |
// observing property changes to an object without the need for | |
// polling. | |
// | |
// Known limitations of this technique: | |
// 1. the call signature of `Object.observe()` will return the proxy | |
// object. The original object needs to be overwritten with this return value. | |
// See usage below. | |
// 2. Changes that happen quickly should be batched into a single | |
// callback. Current this is not the case. The callback gets called | |
// upon every change. | |
// | |
// [1]: https://github.com/jdarling/Object.observe/blob/master/Object.observe.poly.js | |
(function() { | |
'use strict'; | |
// TODO: support 3rd param acceptList | |
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe | |
var observe = function(obj, callback) { | |
if (Object(obj) !== obj) { | |
throw new TypeError('target must be an Object, given ' + obj); | |
} | |
if (typeof callback !== 'function') { | |
throw 'observer must be a function, given ' + callback; | |
} | |
return new Proxy(obj, { | |
set(target, propKey, value, receiver) { | |
var oldVal = target[propKey]; | |
// Don't send change record if value didn't change. | |
if (oldVal === value) { | |
return; | |
} | |
let type = oldVal === undefined ? 'add' : 'update'; | |
var changeRecord = { | |
name: propKey, | |
type: type, | |
object: target | |
}; | |
if (type === 'update') { | |
changeRecord.oldValue = oldVal; | |
} | |
target[propKey] = value; // set prop value on target. | |
// TODO: handle multiple changes in a single callback. | |
callback([changeRecord]); | |
}, | |
deleteProperty(target, propKey, receiver) { | |
// Don't send change record if prop doesn't exist. | |
if (!(propKey in target)) { | |
return; | |
} | |
var changeRecord = { | |
name: propKey, | |
type: 'delete', | |
object: target, | |
oldValue: target[propKey] | |
}; | |
delete target[propKey]; // remove prop from target. | |
// TODO: handle multiple changes in a single callback. | |
callback([changeRecord]); | |
} | |
}); | |
}; | |
if (!Object.observe) { | |
Object.observe = observe; | |
} | |
})(); | |
// ====== Tests ====== // | |
let x = {a: 5}; | |
// If we were observing an object within a (e.g. x.a), that would | |
// need to also be the return variable and the argument to O.o(). | |
// Note: using the native O.o(), you do not need to overwrite | |
// the original object with the return value. | |
x = Object.observe(x, function(changes) { | |
changes.forEach(function(c, i) { | |
console.log(c); | |
}); | |
}); | |
x.a = 10; // update | |
x.a = 10; // asserts no change record | |
x.b = 100; // add | |
delete x.b; // delete | |
delete x.b; // asserts no change record |
Also the native Object.observe
does return, so returning the Proxied
object is good.
@eorroe right on Object.prototype
. I updated the gist to match that. The native does return, but for this technique to work, you need overwrite the original object with the proxied return value. At least that's the only way I could get things to cook.
Nice! As noted, proxies are ~copies of the original objects and changes need to be made to the proxied object rather than the original one. As such, you'll get close to the original behaviour but not 100% there (proxy traps would run after the change vs batching from O.o). Your implementation might also want to consider adding in the inverse (unobserve()
) API method, getNotifier
for creating user defined notifications (which should work with proxies), deliverChangeRecords
(deliver notifications collected for the handler sync).
@MaxArt2501 wrote a well done Object.observe
polyfill over at https://github.com/MaxArt2501/object-observe which IIRC involved some similar research around proxies for O.o: https://github.com/MaxArt2501/object-observe/blob/master/doc/index.md
proxies are ~copies of the original objects and changes need to be made to the proxied object rather than the original one
This is why ones needs to overwrite the original object with the proxied version:
let x = {a: 5};
x = Object.observe(x, function(changes) {
changes.forEach(function(c, i) {
console.log(c);
});
});```
Hacky, but it works.
Also turns out @arv did a fairly spec complete version in 2012: https://mail.mozilla.org/pipermail/es-discuss/2012-July/024111.html
Hey folks, we've just published a Proxy
polyfill, which could work in older browsers to support this style of Object.observe
polyfill: except that deleteProperty
isn't supported, at least for now.
Object.observe
is not a method for each object, (shouldn't be onObject.prototype
) justObject