Created
February 4, 2014 09:19
-
-
Save defims/8800484 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Polyfill for ES6 Object.observe() | |
// Limitations: | |
// - No 'splice' (etc); if you're observing an Array, you're gonna have a bad time | |
(function(global) { | |
function assert(e) { if (!e) throw new Error('Assertion failed'); } | |
// http://wiki.ecmascript.org/doku.php?id=harmony:observe | |
// ECMAScript internal approximations | |
function Type(o) { | |
if (o === null) return 'null'; | |
switch (typeof o) { | |
case 'undefined': return 'undefined'; | |
case 'boolean': return 'boolean'; | |
case 'number': return 'number'; | |
case 'string': return 'string'; | |
default: return 'object'; | |
} | |
} | |
assert(Type(null) === 'null'); | |
assert(Type(assert) === 'object'); | |
function IsCallable(o) { return typeof o === 'function'; } | |
assert(!IsCallable(1)); | |
assert(IsCallable(assert)); | |
function ToUint32(x) { return x >>> 0; } | |
assert(ToUint32(-1) === 0xffffffff); | |
assert(ToUint32('abc') === 0); | |
function SameValue(a, b) { | |
if (a === b) return a !== 0 || 1 / a === 1 / b; | |
return a !== a && b !== b; | |
} | |
assert(SameValue(1, 1)); | |
assert(!SameValue(1, '1')); | |
assert(SameValue(0, 0)); | |
assert(!SameValue(0, -0)); | |
assert(!SameValue(null, undefined)); | |
assert(!SameValue(null, NaN)); | |
assert(SameValue(NaN, NaN)); | |
function IsDataDescriptor(desc) { | |
if (desc === undefined) return false; | |
if (!('value' in desc) && !('writable' in desc)) return false; | |
return true; | |
} | |
assert(IsDataDescriptor(Object.getOwnPropertyDescriptor({x: 1}, 'x'))); | |
function GetOwnProperty(o, p) { | |
return Object.prototype.hasOwnProperty.call(o, p) ? o[p] : undefined; | |
} | |
assert(GetOwnProperty({x: 1}, 'x') === 1); | |
assert(GetOwnProperty({}, 'x') === undefined); | |
assert(GetOwnProperty({}, 'toString') === undefined); | |
// ------------------------------------------------------------ | |
// Object.observe: New Internal Properties, Objects and Algorithms | |
// ------------------------------------------------------------ | |
// http://wiki.ecmascript.org/doku.php?id=harmony:observe_internals | |
// [[ObserverCallbacks]] | |
var __ObserverCallbacks__ = []; // per event queue | |
// [[NotifierPrototype]] | |
var __NotifierPrototype__ = {}; | |
// [[NotifierPrototype]].notify | |
__NotifierPrototype__.notify = function() { | |
var changeRecord = arguments[0]; | |
var notifier = this; | |
if (Type(notifier) !== 'object') throw new TypeError; | |
if (!('[[Target]]' in notifier)) return; | |
var type = changeRecord['type']; | |
if (Type(type) !== 'string') throw new TypeError; | |
var changeObservers = notifier['[[ChangeObservers]']; | |
if (!changeObservers.length) return; | |
var target = notifier['[[Target]]']; | |
var newRecord = {}; | |
Object.defineProperty(newRecord, 'object', { | |
value: target, writable: false, enumerable: true, configurable: false | |
}); | |
for (var n in changeRecord) { | |
if (n !== 'object') { | |
var value = changeRecord[n]; | |
Object.defineProperty(newRecord, n, { | |
value: value, writable: false, enumerable: true, configurable: false | |
}); | |
} | |
} | |
Object.preventExtensions(newRecord); | |
__EnqueueChangeRecord__(target, newRecord); | |
}; | |
// [[NotifierPrototype]].performChange | |
__NotifierPrototype__.performChange = function() { | |
var changeType = arguments[0]; | |
var changeFn = arguments[1]; | |
var notifier = this; | |
if (Type(notifier) !== 'object') throw new TypeError; | |
if (!('[[Target]]' in notifier)) return; | |
var target = notifier['[[Target]]']; | |
if (Type(changeType) !== 'string') throw new TypeError; | |
if (!IsCallable(changeFn)) throw new TypeError; | |
__BeginChange__(target, changeType); | |
try { var changeRecord = changeFn.call(undefined); } | |
catch (e) { var error = e; } | |
__EndChange__(target, changeType); | |
if (error !== undefined) throw error; | |
var changeObservers = notifier['[[ChangeObservers]]']; | |
if (!changeObservers.length) return; | |
var newRecord = {}; | |
Object.defineProperty(newRecord, 'object', { | |
value: target, writable: false, enumerable: true, configurable: false | |
}); | |
Object.defineProperty(newRecord, 'type', { | |
value: changeType, writable: false, enumerable: true, configurable: false | |
}); | |
for (var n in changeRecord) { | |
if (n !== 'object' && n !== 'type') { | |
var value = changeRecord[n]; | |
Object.defineProperty(newRecord, n, { | |
value: value, writable: false, enumerable: true, configurable: false | |
}); | |
} | |
} | |
Object.preventExtensions(newRecord); | |
__EnqueueChangeRecord__(target, newRecord); | |
}; | |
// [[GetNotifier]] | |
function __GetNotifier__(o) { | |
var notifier = o['[[Notifier]]']; | |
if (notifier === undefined) { | |
notifier = {}; | |
notifier.__proto__ = __NotifierPrototype__; | |
notifier['[[Target]]'] = o; | |
notifier['[[ChangeObservers]]'] = []; | |
notifier['[[ActiveChanges]]'] = {}; | |
Object.defineProperty(o, '[[Notifier]]', { | |
value: notifier, enumerable: false, configurable: true, writable: true | |
}); | |
} | |
return notifier; | |
} | |
// [[BeginChange]] | |
function __BeginChange__(o, changeType) { | |
var notifier = __GetNotifier__(o); | |
var activeChanges = notifier['[[ActiveChanges]]']; | |
var changeCount = activeChanges[changeType]; | |
if (changeCount === undefined) changeCount = 1; | |
else changeCount = changeCount + 1; | |
activeChanges[changeType] = changeCount; | |
} | |
// [[EndChange]] | |
function __EndChange__(o, changeType) { | |
var notifier = __GetNotifier__(o); | |
var activeChanges = notifier['[[ActiveChanges]]']; | |
var changeCount = activeChanges[changeType]; | |
assert(changeCount > 0); | |
changeCount = changeCount - 1; | |
activeChanges[changeType] = changeCount; | |
} | |
// [[ShouldDeliverToObserver]] | |
function __ShouldDeliverToObserver__(activeChanges, acceptList, changeType) { | |
var doesAccept = false; | |
for (var i = 0; i < acceptList.length; ++i) { | |
var accept = acceptList[i]; | |
if (activeChanges[accept] > 0) return false; | |
if (accept === changeType) | |
doesAccept = true; | |
} | |
return doesAccept; | |
} | |
// [[EnqueueChangeRecord]] | |
function __EnqueueChangeRecord__(o, changeRecord) { | |
var notifier = __GetNotifier__(o); | |
var changeType = changeRecord['type']; | |
var activeChanges = notifier['[[ActiveChanges]]']; | |
var changeObservers = notifier['[[ChangeObservers]]']; | |
for (var i = 0; i < changeObservers.length; ++i) { | |
var observerRecord = changeObservers[i]; | |
var acceptList = observerRecord['accept']; | |
var deliver = __ShouldDeliverToObserver__(activeChanges, | |
acceptList, | |
changeType); | |
if (!deliver) continue; | |
var observer = observerRecord['callback']; | |
observer['[[PendingChangeRecords]]'] = | |
observer['[[PendingChangeRecords]]'] || []; | |
var pendingRecords = observer['[[PendingChangeRecords]]']; | |
pendingRecords.push(changeRecord); | |
} | |
} | |
// [[DeliverChangeRecords]] | |
function __DeliverChangeRecords__(c) { | |
var changeRecords = c['[[PendingChangeRecords]]'] || []; | |
c['[[PendingChangeRecords]]'] = []; | |
var array = []; | |
var n = 0; | |
for (var i = 0; i < changeRecords.length; ++i) { | |
var record = changeRecords[i]; | |
Object.defineProperty(array, String(n), { | |
value: record, writable: true, enumerable: true, configurable: true | |
}); | |
++n; | |
} | |
if (!array.length) return false; | |
try { c.call(undefined, array); } catch (e) {} | |
return true; | |
} | |
// [[DeliverAllChangeRecords]] | |
function __DeliverAllChangeRecords__() { | |
var observers = __ObserverCallbacks__; | |
var anyWorkDone = false; | |
for (var i = 0; i < observers.length; ++i) { | |
var observer = observers[i]; | |
var result = __DeliverChangeRecords__(observer); | |
if (result) anyWorkDone = true; | |
} | |
return anyWorkDone; | |
} | |
// [[CreateChangeRecord]] | |
function __CreateChangeRecord__(type, object, name, oldDesc, newDesc) { | |
var changeRecord = {}; | |
Object.defineProperty(changeRecord, 'type', { | |
value: type, writable: false, enumerable: true, configurable: false | |
}); | |
Object.defineProperty(changeRecord, 'object', { | |
value: object, writable: false, enumerable: true, configurable: false | |
}); | |
if (Type(name) === 'string') { | |
Object.defineProperty(changeRecord, 'name', { | |
value: name, writable: false, enumerable: true, configurable: false | |
}); | |
} | |
if (IsDataDescriptor(oldDesc)) { | |
if (!IsDataDescriptor(newDesc) || | |
!SameValue(oldDesc.value, newDesc.value)) { | |
Object.defineProperty(changeRecord, 'oldValue', { | |
value: oldDesc.value, | |
writable: false, enumerable: true, configurable: false | |
}); | |
} | |
} | |
Object.preventExtensions(changeRecord); | |
return changeRecord; | |
} | |
// [[CreateSpliceChangeRecord]] | |
function __CreateSpliceChangeRecord__(object, index, removed, addedCount) { | |
var changeRecord = {}; | |
Object.defineProperty(changeRecord, 'type', { | |
value: 'splice', writable: false, enumerable: true, configurable: false | |
}); | |
Object.defineProperty(changeRecord, 'object', { | |
value: object, writable: false, enumerable: true, configurable: false | |
}); | |
Object.defineProperty(changeRecord, 'index', { | |
value: index, writable: false, enumerable: true, configurable: false | |
}); | |
Object.defineProperty(changeRecord, 'removed', { | |
value: removed, writable: false, enumerable: true, configurable: false | |
}); | |
Object.defineProperty(changeRecord, 'addedCount', { | |
value: addedCount, writable: false, enumerable: true, configurable: false | |
}); | |
Object.preventExtensions(changeRecord); | |
return changeRecord; | |
} | |
// ------------------------------------------------------------ | |
// Object.observe: Public API Specification | |
// ------------------------------------------------------------ | |
// http://wiki.ecmascript.org/doku.php?id=harmony:observe_public_api | |
function defineAPI(o, name, fn) { | |
if (!Object.getOwnPropertyDescriptor(o, name)) { | |
Object.defineProperty(o, name, { | |
value: fn, writable: true, enumerable: false, configurable: true | |
}); | |
} | |
} | |
defineAPI(Object, 'observe', function observe(o, callback, accept) { | |
if (Type(o) !== 'object') throw new TypeError; | |
if (!IsCallable(callback)) throw new TypeError; | |
if (Object.isFrozen(callback)) throw new TypeError; | |
if (accept === undefined) { | |
var acceptList = ['add', | |
'update', | |
'delete', | |
'setPrototype', | |
'reconfigure', | |
'preventExtensions']; | |
} else { | |
acceptList = []; | |
if (Type(accept) !== 'object') throw new TypeError; | |
var lenValue = accept.length; | |
var len = ToUint32(lenValue); | |
var nextIndex = 0; | |
while (nextIndex < len) { | |
var next = accept[nextIndex]; | |
var nextString = String(next); | |
acceptList.push(nextString); | |
nextIndex = nextIndex + 1; | |
} | |
} | |
var notifier = __GetNotifier__(o); | |
var changeObservers = notifier['[[ChangeObservers]]']; | |
for (var i = 0; i < changeObservers.length; ++i) { | |
var record = changeObservers[i]; | |
if (record.callback === callback) { | |
record.accept = acceptList; | |
return o; | |
} | |
} | |
// Register for polling | |
poly.watch(o, callback); | |
var observerRecord = {}; | |
observerRecord.callback = callback; | |
observerRecord.accept = acceptList; | |
changeObservers.push(observerRecord); | |
var observerCallbacks = __ObserverCallbacks__; | |
if (observerCallbacks.indexOf(callback) !== -1) | |
return o; | |
observerCallbacks.push(callback); | |
return o; | |
}); | |
defineAPI(Object, 'unobserve', function unobserve(o, callback) { | |
if (Type(o) !== 'object') throw new TypeError; | |
if (!IsCallable(callback)) throw new TypeError; | |
var notifier = __GetNotifier__(o); | |
var changeObservers = notifier['[[ChangeObservers]]']; | |
for (var i = 0; i < changeObservers.length; ++i) { | |
var record = changeObservers[i]; | |
if (record.callback === callback) { | |
changeObservers.splice(i, 1); | |
// Unregister for polling | |
poly.unwatch(o, callback); | |
return o; | |
} | |
} | |
return o; | |
}); | |
defineAPI(Array, 'observe', function observe(o, callback) { | |
return Object.observe(o, callback, ['add', 'update', 'delete', 'splice']); | |
}); | |
defineAPI(Array, 'unobserve', function unobserve(o, callback) { | |
return Object.unobserve(o, callback); | |
}); | |
defineAPI(Object, 'deliverChangeRecords', | |
function deliverChangeRecords(callback) { | |
if (!IsCallable(callback)) throw new TypeError; | |
// Slip in another sample | |
poly.sample(); | |
while (__DeliverChangeRecords__(callback)) | |
continue; | |
return; | |
}); | |
defineAPI(Object, 'getNotifier', function getNotifier(o) { | |
if (Type(o) !== 'object') throw new TypeError; | |
if (Object.isFrozen(o)) return null; | |
return __GetNotifier__(o); | |
}); | |
// ------------------------------------------------------------ | |
// Polyfill Hackery | |
// ------------------------------------------------------------ | |
var poly = (function() { | |
var poly = {}; | |
var multimap = []; // Is a set of pairs <o, callback> | |
var snapshots = []; // TODO: use a Map (polyfill) for snapshots | |
poly.watch = function(o, callback) { | |
multimap.push([o, callback]); | |
for (var i = 0; i < snapshots.length; ++i) { | |
if (snapshots[i][0] === o) return; | |
} | |
snapshots.push([o, makeSnapshot(o)]); | |
}; | |
poly.unwatch = function(o, callback) { | |
for (var i = 0; i < multimap.length; ++i) { | |
if (multimap[i][0] === o && multimap[i][1] === callback) { | |
multimap.splice(i, 1); | |
break; | |
} | |
} | |
for (i = 0; i < multimap.length; ++i) { | |
if (multimap[i][0] === o) return; | |
} | |
for (i = 0; i < snapshots.length; ++i) { | |
if (snapshots[i][0] === o) { | |
snapshots.splice(i, 1); | |
break; | |
} | |
} | |
}; | |
function makeSnapshot(o) { | |
var ss = { descriptors: {} }; | |
Object.getOwnPropertyNames(o).forEach(function(name) { | |
ss.descriptors[name] = Object.getOwnPropertyDescriptor(o, name); | |
}); | |
ss.isExtensible = Object.isExtensible(o); | |
ss.prototype = Object.getPrototypeOf(o); | |
return ss; | |
} | |
// TODO: 'splice' (via shimming intrinsics?) | |
poly.sample = function() { | |
snapshots.forEach(function(pair) { | |
var o = pair[0]; | |
var oldSS = pair[1]; | |
var newSS = makeSnapshot(o); // TODO: Incrementally? | |
pair[1] = newSS; | |
Object.keys(oldSS.descriptors).forEach(function(name) { | |
if (name === '[[Notifier]]') return; | |
var oldDesc = GetOwnProperty(oldSS.descriptors, name); | |
var newDesc = GetOwnProperty(newSS.descriptors, name); | |
if (!newDesc) { | |
var r = __CreateChangeRecord__('delete', o, name, oldDesc, newDesc); | |
__EnqueueChangeRecord__(o, r); | |
return; | |
} | |
if (['value', 'get', 'set', 'configurable', 'writable', 'enumerable' | |
].every(function(p) { return oldDesc[p] === newDesc[p]; })) { | |
return; | |
} | |
var changeType = 'reconfigure'; | |
if (IsDataDescriptor(oldDesc) && IsDataDescriptor(newDesc) && | |
!SameValue(oldDesc.value, newDesc.value)) { | |
changeType = 'update'; | |
} | |
r = __CreateChangeRecord__(changeType, o, name, oldDesc, newDesc); | |
__EnqueueChangeRecord__(o, r); | |
}); | |
Object.keys(newSS.descriptors).forEach(function(name) { | |
if (name === '[[Notifier]]') return; | |
var oldDesc = GetOwnProperty(oldSS.descriptors, name); | |
var newDesc = GetOwnProperty(newSS.descriptors, name); | |
if (!oldDesc) { | |
var r = __CreateChangeRecord__('add', o, name, oldDesc, newDesc); | |
__EnqueueChangeRecord__(o, r); | |
} | |
}); | |
if (oldSS.isExtensible !== newSS.isExtensible) { | |
var r = __CreateChangeRecord__('preventExtensions', o); | |
__EnqueueChangeRecord__(o, r); | |
} | |
if (oldSS.prototype !== newSS.prototype) { | |
r = {}; | |
Object.defineProperty(r, 'type', { | |
value: 'setPrototype', | |
writable: false, enumerable: true, configurable: false | |
}); | |
Object.defineProperty(r, 'object', { | |
value: o, | |
writable: false, enumerable: true, configurable: false | |
}); | |
Object.defineProperty(r, 'oldValue', { | |
value: oldSS.prototype, | |
writable: false, enumerable: true, configurable: false | |
}); | |
Object.preventExtensions(r); | |
__EnqueueChangeRecord__(o, r); | |
} | |
}); | |
__DeliverAllChangeRecords__(); | |
}; | |
var POLL_FREQUENCY = 100; // ms | |
setInterval(poly.sample, POLL_FREQUENCY); | |
return poly; | |
}()); | |
}(this)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<script src="observe.js"></script> | |
<output id="out" style="font: 8pt monospace; white-space: pre;"> | |
</output> | |
<script> | |
function log(m) { | |
document.querySelector('#out').appendChild(document.createTextNode(m + '\n')); | |
} | |
function observer(records) { log(JSON.stringify(records, null, ' ')); } | |
// Tests | |
var o = {}; | |
o.toJSON = function() { return '<object>'; }; | |
var o2 = {} | |
Object.observe(o, observer); | |
// Steps are split up so that intermediate states can be sampled. | |
var steps = [ | |
'o.x = 1;' + | |
'o2.x = 2;', // doesn't notify | |
'o.x = 3;' + | |
'o.y = 4;', | |
'var tmp = 5; Object.defineProperty(o, "x", { get: function () { return tmp; }, set: function (v) { tmp = v; } });', | |
'o.x = 6;', // Doesn't notify | |
'o.toString = 7;', | |
'delete o.x;', | |
'Object.unobserve(o, observer);' + | |
'o.y = 8;' // Doesn't notify | |
]; | |
(function go() { | |
var step = steps.shift(); | |
if (!step) return; | |
eval(step); | |
setTimeout(go, 200); // higher than poll frequency in polyfill | |
}()); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment