[[harmony:observe]]
ES Wiki
 
Trace: » observe
Table of Contents
  • Observe
    • Updates
    • Goals
    • Example
    • Details
        • New public API
          • Object.observe
          • Object.unobserve
          • Object.deliverChangeRecords
          • Object.getNotifier
        • New internal properties and objects
          • [[ObserverCallbacks]]
          • [[Notifier]]
          • [[PendingChangeRecords]]
          • [[NotifierPrototype]]
          • [[NotifierPrototype]].notify
        • New Internal Algorithms
          • [[GetNotifier]]
          • [[EnqueueChangeRecord]]
          • [[DeliverChangeRecords]]
          • [[DeliverAllChangeRecords]]
          • [[CreateChangeRecord]]
        • Modifications to existing internal algorithms
          • [[DefineOwnProperty]]
          • [[Delete]]
        • Changing the [[Prototype]]

Observe

UI frameworks often want to provide an ability to databind objects in a datamodel to UI elements. A key component of databinding is to track changes to the object being bound. Today, JavaScript framework which provide databinding typically create objects wrapping the real data, or require objects being databound to be modified to buy in to databinding. The first case leads to increased working set and more complex user model, and the second leads to siloing of databinding frameworks.

A solution to this is to provide a runtime capability to observe changes to an object.

Updates

1/30/3013:

  1. Suppress oldValue when the changeRecord is of type ‘reconfigured’ and the oldValue and present value are the same. Note that CreateChangeRecord now takes both oldDesc and newDesc as arguments.

12/21/2012:

  1. Object.deliverChangeRecords now calls [[DeliverChangeRecords]] repeatedly until there no pending records to deliver.

11/19/2012:

  1. Object.observer/unobserve now return the object (for consistency with Object.freeze, etc...).

11/13/2012:

  1. In Object.unobserve, passing a non-function as the callback now throws (for symmetry with Object.observe).

10/28/2012:

  1. In CreateChangeRecord, renamed fourth argument to “oldDesc” (from “desc”) for clarity.
  2. In CreateChangeRecord, shallow freeze created changeRecord (consistent with Notifier.prototype.notify).

10/23/2012:

  1. In notify, moved the check for type not being a string above the check for empty observers so the error will throw regardless of if there are observers.
  2. Change the spec language of notify to make it clear to return before creating the changeRecord if there are no observers.

9/11/2012:

  1. Added a section that we need to schedule a {type: “prototype“, object: ..., oldValue: ...} change record when the [[Prototype]] internal property is changed.

7/18/2012:

  1. Only fire “updated” changes when value changes (using SameValue) and no other configuration change happens.

7/17/2012: Based on feedback from Mark Miller, Tom van Cutsem and Andreas Rossberg:

  1. anyWorkDone should be or’ed with the previous value.
  2. Refactored into [[GetNotifier]] to ensure it is always initialized.
  3. “descriptor” is now always “reconfigured”
  4. Enforce that changeRecord is frozen in [[NotifierPrototype]].notify.
  5. Never pass a descriptor in the change record. Only include oldValue if it was a data property before the change.
  6. Remove redundant IsCallable check in unobserve.
  7. Ensure that O is an object in Object.getNotifier.

7/4/2012: Per security feedback from Mark Miller

  1. Object.getNotifier() of frozen object, returns null.
  2. Object [[Notifier]] is spec’d to be lazily created to avoid infinite regress.
  3. All changeRecords are shallowly frozen, and “reconfigured” changeRecords have their oldValue (property descriptor) frozen.
  4. Validate “object” field of changeRecord for notifyFunction so as to ensure that notifier of an object can only broadcast changes of its [[Target]].
  5. Validate “type” and “name” fields of changeRecord for notifyFunction and perform a shallow freeze to prevent unintentional delivery of shared mutable state.
  6. Object.retrieveChangeRecords → Object.deliverChangeRecords (invoke the function rather than return records).
  7. Object callbacks invokations silently ignore all thrown exceptions and return values.
  8. Frozen callback function objects cannot be registered as change observers (Object.observe) throws TypeError.

7/2/2012:

  1. Remove changeRecord validation. Since there is no way to validate “oldValue”, arbitrary data can always be passed.
  2. Added Object.getNotifier(obj).notify(changeRecord) separation, so as to allow freezing of an object to prevent side-channel, but retaining the ability to notify if getNotifier() was called prior to freezing.

5/17/2012:

  1. Call the callbacks with a ChangeRecord object describing the change.
  2. Add sanity checks in [[ToChangeRecord]] which is used in Object.notifyObservers.
  3. Add Object.retrieveChangeRecords which allows synchronous access to the pending changes.

3/16/2012: Two requests from feedback from Rafael Weinstein and Erik Arvidsson:

  1. Pass old property values as well as new to [[FireChangeListeners]]
  2. Schedule change events to be delivered asynchronously “at the end of the turn”

Goals

The desired characteristics of a solution are:

  1. No wrapper or proxy objects needed, providing memory efficiency and object identity
  2. Change notifications on add/delete of a property on an object
  3. Change notifications on modifications to property descriptor of properties on an object
  4. The ability for an object to manually indicate when an accessor property has changed
  5. Efficiently implementable in engines
  6. Simple, targeted, extension to current ES
  7. Asynchronous notification of changes, but allow synchronous fetching of changes pending delivery

Example

The following examples shows the intended behaviour of the above spec.

function observer(records) {
  console.log(records);
}
 
// Tests
var o = {};
var o2 = {}
Object.observe(o, observer);
 
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

Would produce something like:

[
{
  type: "new",
  object: o,
  name: "x"
},
{
  type: "updated",
  object: o,
  name: "x",
  oldValue: 1
},
{
  type: "new"
  object: o,
  name: "y"
},
{
  type: "reconfigured",
  object: o,
  name: "x"
},
{
  type: "new",
  object: o,
  name: "toString"
},
{
  type: "deleted",
  object: o,
  name: "x"
  // "oldValue" will only be present if the deleted property was a data property.
]

Details

New public API

Object.observe

A new function Object.observe(O, callback) is added, which behaves as follows:

  1. If Type(O) is not Object, throw a TypeError exception.
  2. If IsCallable(callback) is not true, throw a TypeError exception.
  3. If IsFrozen(callback) is true, throw a TypeError exception.
  4. Let notifier be the result of calling [[GetNotifier]], passing O.
  5. Let changeObservers be [[ChangeObservers]] of notifier.
  6. If changeObservers already contains callback, return.
  7. Append callback to the end of the changeObservers list.
  8. Let observerCallbacks be [[ObserverCallbacks]].
  9. If observerCallbacks already contains callback, return.
  10. Append callback to the end of the observerCallbacks list.
  11. Return O.
Object.unobserve

A new function Object.unobserve(O, callback) is added, which behaves as follows:

  1. If Type(O) is not Object, throw a TypeError exception.
  2. If IsCallable(callback) is not true, throw a TypeError exception.
  3. Let notifier be the result of calling [[GetNotifier]], passing O.
  4. Let changeObservers be [[ChangeObservers]] of notifier.
  5. If changeObservers does not contain callback, return.
  6. Remove callback from the changeObservers list.
  7. Return O.
Object.deliverChangeRecords

A new function Object.deliverChangeRecords(callback) is added, which behaves as follows:

  1. If IsCallable(callback) is not true, throw a TypeError exception.
  2. Call [[DeliverChangeRecords]] with arguments: callback repeatedly until it returns false (no records were pending for delivery and callback no invoked).
  3. Return.
Object.getNotifier

A new function Object.getNotifier(O) is added, which behaves as follows:

  1. If Type(O) is not Object, throw a TypeError exception.
  2. If O is frozen, return null.
  3. Return the result of calling [[GetNotifier]], passing O.

New internal properties and objects

[[ObserverCallbacks]]

There is now an ordered list, [[ObserverCallbacks]] which is shared per event queue. It is initially empty.

Note: This list is used to provide a deterministic ordering in which callbacks are called.

[[Notifier]]

Every object O now has a [[Notifier]] internal property which is initially undefined.

Note: This gets lazily initialized to a notifier object which is an object with the [[NotifierPrototype]] as its [[Prototype]].

[[PendingChangeRecords]]

Every function now has a [[PendingChangeRecords]] internal property which is an ordered list of ChangeRecords. It is initially empty.

Note: This list gets populated with change records as the objects that this function is observing are mutated. It gets emptied when the change records are delivered.

[[NotifierPrototype]]

This object is used as the [[Prototype]] of all the notifiers that are returned by Object.getNotifier(O). It has one function called notify which is defined below.

?.??.?? [[NotifierPrototype]] is defined as follows:

  1. Let notifierPrototype be the result of the abstract operation ObjectCreate (15.12).
  2. Call the [[DefineOwnProperty]] internal method of notifierPrototype with arguments “notify”, the PropertyDescriptor {[[Value]]: notifyFunction, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true}, and false.
  3. Let [[NotifierPrototype]] be notifierPrototype.
[[NotifierPrototype]].notify

The notify function of the [[NotifierPrototype]] is defined as follow:

Let notifyFunction be a function when invoked does the following:

  1. Let changeRecord be the first argument to the function.
  2. Let notifier be [[This]].
  3. If Type(notifier) is not Object, throw a TypeError exception.
  4. If notifier does not have an internal property [[Target]] return.
  5. Let type be the result of calling the [[Get]] internal method of changeRecord with “type”.
  6. If Type(type) is not string, throw a TypeError exception.
  7. Let changeObservers be the result of getting the internal property [[ChangeObservers]] of notifier.
  8. If changeObservers is empty, return.
  9. Let target be [[Target]] of notifier.
  10. Let newRecord be the result of the abstract operation ObjectCreate (15.12).
  11. Call the [[DefineOwnProperty]] internal method of newRecord with arguments “object”, the Property Descriptor {[[Value]]: target, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false}, and true.
  12. For each enumerable property name N in changeRecord,
    1. If N is not “object”, then
      1. Let value be the result of calling the [[Get]] internal method of changeRecord with N.
      2. Call the [[DefineOwnProperty]] internal method of newRecord with arguments N, the Property Descriptor {[[Value]]: value, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false}, and true.
  13. Set the [[Extensible]] internal property of newRecord to false.
  14. Let changeObservers be [[ChangeObservers]] of notifier.
  15. Call the [[EnqueueChangeRecord]], passing newRecord and changeObservers as arguments.

New Internal Algorithms

[[GetNotifier]]

There is now a [[GetNotifier]] internal algorithm:

?.??.?? [[GetNotifier]] (O)

  1. Let notifier be [[Notifier]] of O.
  2. If notifier is undefined:
    1. Let notifier be the result the abstract operation ObjectCreate (15.12).
    2. Set the [[Prototype]] of notifier to [[NotifierPrototype]].
    3. Set the internal property [[Target]] of notifier to be O.
    4. Set the internal property [[ChangeObservers]] of notifier to be a new empty list.
    5. Set [[Notifier]] of O to notifier.
  3. return notifier.

Note: The [[GetNotifier]] lazily sets the [[Notifier]] internal property.

[[EnqueueChangeRecord]]

There is now an abstract [[EnqueueChangeRecord]] internal algorithm:

?.??.?? [[EnqueueChangeRecord]] (R, T)

When the [[EnqueueChangeRecord]] internal algorithm is called with ChangeRecord R and ChangeObservers T, the following steps are taken:

  1. For each observer in T, do:
    1. Let pendingRecords be the result of getting [[PendingChangeRecords]] for observer.
    2. Append R to the end of pendingRecords.
[[DeliverChangeRecords]]

There is an abstract [[DeliverChangeRecords] internal algorithm.

?.??.?? [[DeliverChangeRecords]] (C)

When the [[DeliverChangeRecords]] internal algorithm is called with callback C, the following steps are taken:

  1. Let changeRecords be [[PendingChangeRecords]] of C.
  2. Clear the [[PendingChangeRecords]] of C.
  3. Let array be the result of the abstraction operation ArrayCreate (15.4) with argument 0.
  4. Let n be 0.
  5. For each record in changeRecords, do:
    1. Call the [[DefineOwnProperty]] internal method of array with arguments ToString(n), the PropertyDescriptor {[[Value]]: record, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}, and false.
    2. Increment n by 1.
  6. If array is empty, return false.
  7. Call the [[Call]] internal method, (silently ignoring any thrown exception or return value) of C, passing undefined as the this parameter, and a single argument, array.
  8. Return true.

Note: The user facing function Object.deliverChangeRecords returns undefined to prevent detection if anything was delivered or not.

[[DeliverAllChangeRecords]]

There is an abstract [[DeliverAllChangeRecords]] internal algorithm:

?.??.?? [[DeliverAllChangeRecords]]

When the [[DeliverAllChangeRecords]] internal algorithm is called, the following steps are taken:

  1. Let observers be the result of getting [[ObserverCallbacks]]
  2. Let anyWorkDone be a boolean value, initially set to false.
  3. For each observer in observers, do:
    1. Let result be the result of calling [[DeliverChangeRecords]] with observer.
    2. If result is true:
      1. Set anyWorkDone to true.
  4. Return anyWorkDone.

Note: It is the intention that the embedder will call this internal algorithm when it is time to deliver the change records.

[[CreateChangeRecord]]

There is now an abstract operation [[CreateChangeRecord]]:

?.??.?? [[CreateChangeRecord]] (type, object, name, oldDesc, newDesc)

When the abstract operation CreateChangeRecord is called with the arguments: type, object, name and oldDesc, the following steps are taken:

  1. Let changeRecord be the result of the abstraction operation ObjectCreate (15.2).
  2. Call the [[DefineOwnProperty]] internal method of changeRecord with arguments “type”, Property Descriptor {[[Value]]: type, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false}, and false.
  3. Call the [[DefineOwnProperty]] internal method of changeRecord with arguments “object”, Property Descriptor {[[Value]]: object, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false}, and false.
  4. Call the [[DefineOwnProperty]] internal method of changeRecord with arguments “name”, Property Descriptor {[[Value]]: name, [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false}, and false.
  5. If IsDataDescriptor(oldDesc) is true:
    1. If IsDataDescritor(newDesc) is false or SameValue(oldDesc.[[Value]], newDesc.[[Value]]) is false
      1. Call the [[DefineOwnProperty]] internal method of changeRecord with arguments “oldValue”, Property Descriptor {[[Value]]: oldDesc.[Value]], [[Writable]]: false, [[Enumerable]]: true, [[Configurable]]: false}, and false.
  6. Set the [[Extensible]] internal property of changeRecord to false.
  7. Return changeRecord.

Modifications to existing internal algorithms

[[DefineOwnProperty]]

Modify the [[DefineOwnProperty]] algorithm as indicated:

8.12.9 [[DefineOwnProperty]] (P, Desc, Throw)

In the following algorithm, the term “Reject” means “If Throw is true, then throw a TypeError exception, otherwise return false”. The algorithm contains steps that test various fields of the Property Descriptor Desc for specific values. The fields that are tested in this manner need not actually exist in Desc. If a field is absent then its value is considered to be false.

When the [[DefineOwnProperty]] internal method of O is called with property name P, property descriptor Desc, and Boolean flag Throw, the following steps are taken:

  1. Let current be the result of calling the [[GetOwnProperty]] internal method of O with property name P.
  2. Let extensible be the value of the [[Extensible]] internal property of O.
  3. Let notifier be the result of calling [[GetNotifier]], passing O.
  4. Let changeObservers be [[ChangeObservers]] of notifier.
  5. Let changeType be a string, initially set to “reconfigured”.
  6. If current is undefined and extensible is false, then Reject.
  7. If current is undefined and extensible is true, then
    1. If IsGenericDescriptor(Desc) or IsDataDescriptor(Desc) is true, then
      1. Create an own data property named P of object O whose [[Value]], [[Writable]], [[Enumerable]] and [[Configurable]] attribute values are described by Desc. If the value of an attribute field of Desc is absent, the attribute of the newly created property is set to its default value.
    2. Else, Desc must be an accessor Property Descriptor so,
      1. Create an own accessor property named P of object O whose [[Get]], [[Set]], [[Enumerable]] and [[Configurable]] attribute values are described by Desc. If the value of an attribute field of Desc is absent, the attribute of the newly created property is set to its default value.
    3. Let R be the result of calling [[CreateChangeRecord]] with arguments: “new”, O, P, current and Desc.
    4. Call [[EnqueueChangeRecord]] passing R and changeObservers.
    5. Return true.
  8. Return true, if every field in Desc is absent.
  9. Return true, if every field in Desc also occurs in current and the value of every field in Desc is the same value as the corresponding field in current when compared using the SameValue algorithm (9.12).
  10. If the [[Configurable]] field of current is false then
    1. Reject, if the [[Configurable]] field of Desc is true.
    2. Reject, if the [[Enumerable]] field of Desc is present and the [[Enumerable]] fields of current and Desc are the Boolean negation of each other.
  11. If IsGenericDescriptor(Desc) is true, then no further validation is required.
  12. Else, if IsDataDescriptor(current) and IsDataDescriptor(Desc) have different results, then
    1. Reject, if the [[Configurable]] field of current is false.
    2. If IsDataDescriptor(current) is true, then
      1. Convert the property named P of object O from a data property to an accessor property. Preserve the existing values of the converted property’s [[Configurable]] and [[Enumerable]] attributes and set the rest of the property’s attributes to their default values.
    3. Else,
      1. Convert the property named P of object O from an accessor property to a data property. Preserve the existing values of the converted property’s [[Configurable]] and [[Enumerable]] attributes and set the rest of the property’s attributes to their default values.
  13. Else, if IsDataDescriptor(current) and IsDataDescriptor(Desc) are both true, then
    1. If the [[Configurable]] field of current is false, then
      1. Reject, if the [[Writable]] field of current is false and the [[Writable]] field of Desc is true.
      2. If the [[Writable]] field of current is false, then
        1. Reject, if the [[Value]] field of Desc is present and SameValue(Desc.[[Value]], current.[[Value]]) is false.
      3. else, the [[Configurable]] field of current is true, so any change is acceptable.
    2. If [[Configurable]] is not in Desc or SameValue(Desc.[[Configurable]], current.[[Configurable]]) is true and [[Enumerable]] is not in Desc or SameValue(Desc.[[Enumerable]], current.[[Enumerable]]) is true and [[Writable]] is not in Desc or SameValue(Desc.[[Writable]], current.[[Writable]]) is true, then
      1. Set changeType to be the string “updated”.
  14. Else, IsAccessorDescriptor(current) and IsAccessorDescriptor(Desc) are both true so,
    1. If the [[Configurable]] field of current is false, then
      1. Reject, if the [[Set]] field of Desc is present and SameValue(Desc.[[Set]], current.[[Set]]) is false.
      2. Reject, if the [[Get]] field of Desc is present and SameValue(Desc.[[Get]], current.[[Get]]) is false.
  15. For each attribute field of Desc that is present, set the correspondingly named attribute of the property named P of object O to the value of the field.
  16. Let R be the result of calling [[CreateChangeRecord]] with arguments: changeType, O, P, current and Desc.
  17. Call [[EnqueueChangeRecord]] passing R and changeObservers.
  18. Return true.

However, if O is an Array object, it has a more elaborate [[DefineOwnProperty]] internal method defined in 15.4.5.1. NOTE Step 10.b allows any field of Desc to be different from the corresponding field of current if current’s [[Configurable]] field is true. This even permits changing the [[Value]] of a property whose [[Writable]] attribute is false. This is allowed because a true [[Configurable]] attribute would permit an equivalent sequence of calls where [[Writable]] is first set to true, a new [[Value]] is set, and then [[Writable]] is set to false.

[[Delete]]

Modify the [[Delete]] algorithm as indicated:

8.12.7 [[Delete]] (P, Throw)

When the [[Delete]] internal method of O is called with property name P and the Boolean flag Throw, the following steps are taken:

  1. Let desc be the result of calling the [[GetOwnProperty]] internal method of O with property name P.
  2. If desc is undefined, then return true.
  3. Let notifier be the result of calling [[GetNotifier]], passing O.
  4. Let changeObservers be [[ChangeObservers]] of notifier.
  5. If desc.[[Configurable]] is true, then
    1. Remove the own property with name P from O.
    2. Let R be the result of calling [[CreateChangeRecord]] with arguments: “deleted”, O, P and desc.
    3. Call [[EnqueueChangeRecord]] on passing R and changeObservers.
    4. Return true.
  6. Else if Throw, then throw a TypeError exception.
  7. Return false.

Changing the [[Prototype]]

It has been agreed that we will support changing the [[Prototype]] by setting the __proto__ property. However, the spec draft for this is currently not done.

Whenever the [[Prototype]] is set on an object O, through any means, we need to do the following:

  1. Let notifier be the result of calling [[GetNotifier]], passing O.
  2. Let changeObservers be [[ChangeObservers]] of notifier.
  3. Let oldPrototype be the old value of [[Prototype]].
  4. Let changeRecord be the result of the abstraction operation ObjectCreate (15.2).
  5. Call the [[DefineOwnProperty]] internal method of changeRecord with arguments “type”, Property Descriptor {[[Value]]: “prototype”, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}, and false.
  6. Call the [[DefineOwnProperty]] internal method of changeRecord with arguments “object”, Property Descriptor {[[Value]]: O, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}, and false.
  7. Call the [[DefineOwnProperty]] internal method of changeRecord with arguments “oldValue”, Property Descriptor {[[Value]]: oldPrototype.[Value]], [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}, and false.
  8. Call [[EnqueueChangeRecord]] on passing changeRecord and changeObservers.
 
harmony/observe.txt · Last modified: 2013/01/30 21:41 by arv
gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.