Index: src/object-observe.js |
diff --git a/src/object-observe.js b/src/object-observe.js |
index 81385c3f9a6a11cb651241a53ec157f8051e0e53..f5e0d9d563a08e0507d01b03cc519370e49d0215 100644 |
--- a/src/object-observe.js |
+++ b/src/object-observe.js |
@@ -27,41 +27,12 @@ |
"use strict"; |
-// Overview: |
-// |
-// This file contains all of the routing and accounting for Object.observe. |
-// User code will interact with these mechanisms via the Object.observe APIs |
-// and, as a side effect of mutation objects which are observed. The V8 runtime |
-// (both C++ and JS) will interact with these mechanisms primarily by enqueuing |
-// proper change records for objects which were mutated. The Object.observe |
-// routing and accounting consists primarily of three participants |
-// |
-// 1) ObjectInfo. This represents the observed state of a given object. It |
-// records what callbacks are observing the object, with what options, and |
-// what "change types" are in progress on the object (i.e. via |
-// notifier.performChange). |
-// |
-// 2) CallbackInfo. This represents a callback used for observation. It holds |
-// the records which must be delivered to the callback, as well as the global |
-// priority of the callback (which determines delivery order between |
-// callbacks). |
-// |
-// 3) observationState.pendingObservers. This is the set of observers which |
-// have change records which must be delivered. During "normal" delivery |
-// (i.e. not Object.deliverChangeRecords), this is the mechanism by which |
-// callbacks are invoked in the proper order until there are no more |
-// change records pending to a callback. |
-// |
-// Note that in order to reduce allocation and processing costs, the |
-// implementation of (1) and (2) have "optimized" states which represent |
-// common cases which can be handled more efficiently. |
- |
var observationState = %GetObservationState(); |
if (IS_UNDEFINED(observationState.callbackInfoMap)) { |
observationState.callbackInfoMap = %ObservationWeakMapCreate(); |
observationState.objectInfoMap = %ObservationWeakMapCreate(); |
- observationState.notifierObjectInfoMap = %ObservationWeakMapCreate(); |
- observationState.pendingObservers = null; |
+ observationState.notifierTargetMap = %ObservationWeakMapCreate(); |
+ observationState.pendingObservers = new InternalArray; |
observationState.nextCallbackPriority = 0; |
} |
@@ -88,191 +59,126 @@ ObservationWeakMap.prototype = { |
var callbackInfoMap = |
new ObservationWeakMap(observationState.callbackInfoMap); |
var objectInfoMap = new ObservationWeakMap(observationState.objectInfoMap); |
-var notifierObjectInfoMap = |
- new ObservationWeakMap(observationState.notifierObjectInfoMap); |
- |
-function TypeMapCreate() { |
- return { __proto__: null }; |
-} |
- |
-function TypeMapAddType(typeMap, type, ignoreDuplicate) { |
- typeMap[type] = ignoreDuplicate ? 1 : (typeMap[type] || 0) + 1; |
-} |
- |
-function TypeMapRemoveType(typeMap, type) { |
- typeMap[type]--; |
-} |
- |
-function TypeMapCreateFromList(typeList) { |
- var typeMap = TypeMapCreate(); |
- for (var i = 0; i < typeList.length; i++) { |
- TypeMapAddType(typeMap, typeList[i], true); |
- } |
- return typeMap; |
-} |
- |
-function TypeMapHasType(typeMap, type) { |
- return !!typeMap[type]; |
+var notifierTargetMap = |
+ new ObservationWeakMap(observationState.notifierTargetMap); |
+ |
+function CreateObjectInfo(object) { |
+ var info = { |
+ changeObservers: new InternalArray, |
+ notifier: null, |
+ inactiveObservers: new InternalArray, |
+ performing: { __proto__: null }, |
+ performingCount: 0, |
+ }; |
+ objectInfoMap.set(object, info); |
+ return info; |
} |
-function TypeMapIsDisjointFrom(typeMap1, typeMap2) { |
- if (!typeMap1 || !typeMap2) |
- return true; |
- |
- for (var type in typeMap1) { |
- if (TypeMapHasType(typeMap1, type) && TypeMapHasType(typeMap2, type)) |
- return false; |
- } |
- |
- return true; |
-} |
+var defaultAcceptTypes = { |
+ __proto__: null, |
+ 'new': true, |
+ 'updated': true, |
+ 'deleted': true, |
+ 'prototype': true, |
+ 'reconfigured': true |
+}; |
-var defaultAcceptTypes = TypeMapCreateFromList([ |
- 'new', |
- 'updated', |
- 'deleted', |
- 'prototype', |
- 'reconfigured' |
-]); |
- |
-// An Observer is a registration to observe an object by a callback with |
-// a given set of accept types. If the set of accept types is the default |
-// set for Object.observe, the observer is represented as a direct reference |
-// to the callback. An observer never changes its accept types and thus never |
-// needs to "normalize". |
-function ObserverCreate(callback, acceptList) { |
- return IS_UNDEFINED(acceptList) ? callback : { |
+function CreateObserver(callback, accept) { |
+ var observer = { |
__proto__: null, |
callback: callback, |
- accept: TypeMapCreateFromList(acceptList) |
+ accept: defaultAcceptTypes |
}; |
-} |
-function ObserverGetCallback(observer) { |
- return IS_SPEC_FUNCTION(observer) ? observer : observer.callback; |
-} |
+ if (IS_UNDEFINED(accept)) |
+ return observer; |
-function ObserverGetAcceptTypes(observer) { |
- return IS_SPEC_FUNCTION(observer) ? defaultAcceptTypes : observer.accept; |
-} |
+ var acceptMap = { __proto__: null }; |
+ for (var i = 0; i < accept.length; i++) |
+ acceptMap[accept[i]] = true; |
-function ObserverIsActive(observer, objectInfo) { |
- return TypeMapIsDisjointFrom(ObjectInfoGetPerformingTypes(objectInfo), |
- ObserverGetAcceptTypes(observer)); |
+ observer.accept = acceptMap; |
+ return observer; |
} |
-function ObjectInfoGet(object) { |
- var objectInfo = objectInfoMap.get(object); |
- if (IS_UNDEFINED(objectInfo)) { |
- if (!%IsJSProxy(object)) |
- %SetIsObserved(object); |
- |
- objectInfo = { |
- object: object, |
- changeObservers: null, |
- notifier: null, |
- performing: null, |
- performingCount: 0, |
- }; |
- objectInfoMap.set(object, objectInfo); |
- } |
- return objectInfo; |
-} |
- |
-function ObjectInfoGetFromNotifier(notifier) { |
- return notifierObjectInfoMap.get(notifier); |
-} |
+function ObserverIsActive(observer, objectInfo) { |
+ if (objectInfo.performingCount === 0) |
+ return true; |
-function ObjectInfoGetNotifier(objectInfo) { |
- if (IS_NULL(objectInfo.notifier)) { |
- objectInfo.notifier = { __proto__: notifierPrototype }; |
- notifierObjectInfoMap.set(objectInfo.notifier, objectInfo); |
+ var performing = objectInfo.performing; |
+ for (var type in performing) { |
+ if (performing[type] > 0 && observer.accept[type]) |
+ return false; |
} |
- return objectInfo.notifier; |
-} |
- |
-function ObjectInfoGetObject(objectInfo) { |
- return objectInfo.object; |
-} |
- |
-function ChangeObserversIsOptimized(changeObservers) { |
- return typeof changeObservers === 'function' || |
- typeof changeObservers.callback === 'function'; |
+ return true; |
} |
-// The set of observers on an object is called 'changeObservers'. The first |
-// observer is referenced directly via objectInfo.changeObservers. When a second |
-// is added, changeObservers "normalizes" to become a mapping of callback |
-// priority -> observer and is then stored on objectInfo.changeObservers. |
-function ObjectInfoNormalizeChangeObservers(objectInfo) { |
- if (ChangeObserversIsOptimized(objectInfo.changeObservers)) { |
- var observer = objectInfo.changeObservers; |
- var callback = ObserverGetCallback(observer); |
- var callbackInfo = CallbackInfoGet(callback); |
- var priority = CallbackInfoGetPriority(callbackInfo); |
- objectInfo.changeObservers = { __proto__: null }; |
- objectInfo.changeObservers[priority] = observer; |
- } |
+function ObserverIsInactive(observer, objectInfo) { |
+ return !ObserverIsActive(observer, objectInfo); |
} |
-function ObjectInfoAddObserver(objectInfo, callback, acceptList) { |
- var callbackInfo = CallbackInfoGetOrCreate(callback); |
- var observer = ObserverCreate(callback, acceptList); |
- |
- if (!objectInfo.changeObservers) { |
- objectInfo.changeObservers = observer; |
- return; |
+function RemoveNullElements(from) { |
+ var i = 0; |
+ var j = 0; |
+ for (; i < from.length; i++) { |
+ if (from[i] === null) |
+ continue; |
+ if (j < i) |
+ from[j] = from[i]; |
+ j++; |
} |
- ObjectInfoNormalizeChangeObservers(objectInfo); |
- var priority = CallbackInfoGetPriority(callbackInfo); |
- objectInfo.changeObservers[priority] = observer; |
+ if (i !== j) |
+ from.length = from.length - (i - j); |
} |
-function ObjectInfoRemoveObserver(objectInfo, callback) { |
- if (!objectInfo.changeObservers) |
- return; |
- |
- if (ChangeObserversIsOptimized(objectInfo.changeObservers)) { |
- if (callback === ObserverGetCallback(objectInfo.changeObservers)) |
- objectInfo.changeObservers = null; |
- return; |
- } |
- |
- var callbackInfo = CallbackInfoGet(callback); |
- var priority = CallbackInfoGetPriority(callbackInfo); |
- delete objectInfo.changeObservers[priority]; |
-} |
- |
-function ObjectInfoHasActiveObservers(objectInfo) { |
- if (IS_UNDEFINED(objectInfo) || !objectInfo.changeObservers) |
- return false; |
- |
- if (ChangeObserversIsOptimized(objectInfo.changeObservers)) |
- return ObserverIsActive(objectInfo.changeObservers, objectInfo); |
- |
- for (var priority in objectInfo.changeObservers) { |
- if (ObserverIsActive(objectInfo.changeObservers[priority], objectInfo)) |
- return true; |
+function RepartitionObservers(conditionFn, from, to, objectInfo) { |
+ var anyRemoved = false; |
+ for (var i = 0; i < from.length; i++) { |
+ var observer = from[i]; |
+ if (conditionFn(observer, objectInfo)) { |
+ anyRemoved = true; |
+ from[i] = null; |
+ to.push(observer); |
+ } |
} |
- return false; |
+ if (anyRemoved) |
+ RemoveNullElements(from); |
} |
-function ObjectInfoAddPerformingType(objectInfo, type) { |
- objectInfo.performing = objectInfo.performing || TypeMapCreate(); |
- TypeMapAddType(objectInfo.performing, type); |
+function BeginPerformChange(objectInfo, type) { |
+ objectInfo.performing[type] = (objectInfo.performing[type] || 0) + 1; |
objectInfo.performingCount++; |
+ RepartitionObservers(ObserverIsInactive, |
+ objectInfo.changeObservers, |
+ objectInfo.inactiveObservers, |
+ objectInfo); |
} |
-function ObjectInfoRemovePerformingType(objectInfo, type) { |
+function EndPerformChange(objectInfo, type) { |
+ objectInfo.performing[type]--; |
objectInfo.performingCount--; |
- TypeMapRemoveType(objectInfo.performing, type); |
-} |
+ RepartitionObservers(ObserverIsActive, |
+ objectInfo.inactiveObservers, |
+ objectInfo.changeObservers, |
+ objectInfo); |
+} |
+ |
+function EnsureObserverRemoved(objectInfo, callback) { |
+ function remove(observerList) { |
+ for (var i = 0; i < observerList.length; i++) { |
+ if (observerList[i].callback === callback) { |
+ observerList.splice(i, 1); |
+ return true; |
+ } |
+ } |
+ return false; |
+ } |
-function ObjectInfoGetPerformingTypes(objectInfo) { |
- return objectInfo.performingCount > 0 ? objectInfo.performing : null; |
+ if (!remove(objectInfo.changeObservers)) |
+ remove(objectInfo.inactiveObservers); |
} |
function AcceptArgIsValid(arg) { |
@@ -292,31 +198,12 @@ function AcceptArgIsValid(arg) { |
return true; |
} |
-// CallbackInfo's optimized state is just a number which represents its global |
-// priority. When a change record must be enqueued for the callback, it |
-// normalizes. When delivery clears any pending change records, it re-optimizes. |
-function CallbackInfoGet(callback) { |
- return callbackInfoMap.get(callback); |
+function EnsureCallbackPriority(callback) { |
+ if (!callbackInfoMap.has(callback)) |
+ callbackInfoMap.set(callback, observationState.nextCallbackPriority++); |
} |
-function CallbackInfoGetOrCreate(callback) { |
- var callbackInfo = callbackInfoMap.get(callback); |
- if (!IS_UNDEFINED(callbackInfo)) |
- return callbackInfo; |
- |
- var priority = observationState.nextCallbackPriority++ |
- callbackInfoMap.set(callback, priority); |
- return priority; |
-} |
- |
-function CallbackInfoGetPriority(callbackInfo) { |
- if (IS_NUMBER(callbackInfo)) |
- return callbackInfo; |
- else |
- return callbackInfo.priority; |
-} |
- |
-function CallbackInfoNormalize(callback) { |
+function NormalizeCallbackInfo(callback) { |
var callbackInfo = callbackInfoMap.get(callback); |
if (IS_NUMBER(callbackInfo)) { |
var priority = callbackInfo; |
@@ -327,18 +214,32 @@ function CallbackInfoNormalize(callback) { |
return callbackInfo; |
} |
-function ObjectObserve(object, callback, acceptList) { |
+function ObjectObserve(object, callback, accept) { |
if (!IS_SPEC_OBJECT(object)) |
throw MakeTypeError("observe_non_object", ["observe"]); |
if (!IS_SPEC_FUNCTION(callback)) |
throw MakeTypeError("observe_non_function", ["observe"]); |
if (ObjectIsFrozen(callback)) |
throw MakeTypeError("observe_callback_frozen"); |
- if (!AcceptArgIsValid(acceptList)) |
+ if (!AcceptArgIsValid(accept)) |
throw MakeTypeError("observe_accept_invalid"); |
- var objectInfo = ObjectInfoGet(object); |
- ObjectInfoAddObserver(objectInfo, callback, acceptList); |
+ EnsureCallbackPriority(callback); |
+ |
+ var objectInfo = objectInfoMap.get(object); |
+ if (IS_UNDEFINED(objectInfo)) { |
+ objectInfo = CreateObjectInfo(object); |
+ %SetIsObserved(object); |
+ } |
+ |
+ EnsureObserverRemoved(objectInfo, callback); |
+ |
+ var observer = CreateObserver(callback, accept); |
+ if (ObserverIsActive(observer, objectInfo)) |
+ objectInfo.changeObservers.push(observer); |
+ else |
+ objectInfo.inactiveObservers.push(observer); |
+ |
return object; |
} |
@@ -352,7 +253,7 @@ function ObjectUnobserve(object, callback) { |
if (IS_UNDEFINED(objectInfo)) |
return object; |
- ObjectInfoRemoveObserver(objectInfo, callback); |
+ EnsureObserverRemoved(objectInfo, callback); |
return object; |
} |
@@ -367,52 +268,41 @@ function ArrayUnobserve(object, callback) { |
return ObjectUnobserve(object, callback); |
} |
-function ObserverEnqueueIfActive(observer, objectInfo, changeRecord) { |
- if (!ObserverIsActive(observer, objectInfo) || |
- !TypeMapHasType(ObserverGetAcceptTypes(observer), changeRecord.type)) { |
- return; |
- } |
- |
- var callback = ObserverGetCallback(observer); |
- var callbackInfo = CallbackInfoNormalize(callback); |
- if (!observationState.pendingObservers) |
- observationState.pendingObservers = { __proto__: null }; |
+function EnqueueToCallback(callback, changeRecord) { |
+ var callbackInfo = NormalizeCallbackInfo(callback); |
observationState.pendingObservers[callbackInfo.priority] = callback; |
callbackInfo.push(changeRecord); |
%SetObserverDeliveryPending(); |
} |
-function ObjectInfoEnqueueChangeRecord(objectInfo, changeRecord) { |
+function EnqueueChangeRecord(changeRecord, observers) { |
// TODO(rossberg): adjust once there is a story for symbols vs proxies. |
if (IS_SYMBOL(changeRecord.name)) return; |
- if (ChangeObserversIsOptimized(objectInfo.changeObservers)) { |
- var observer = objectInfo.changeObservers; |
- ObserverEnqueueIfActive(observer, objectInfo, changeRecord); |
- return; |
- } |
+ for (var i = 0; i < observers.length; i++) { |
+ var observer = observers[i]; |
+ if (IS_UNDEFINED(observer.accept[changeRecord.type])) |
+ continue; |
- for (var priority in objectInfo.changeObservers) { |
- var observer = objectInfo.changeObservers[priority]; |
- ObserverEnqueueIfActive(observer, objectInfo, changeRecord); |
+ EnqueueToCallback(observer.callback, changeRecord); |
} |
} |
function BeginPerformSplice(array) { |
var objectInfo = objectInfoMap.get(array); |
if (!IS_UNDEFINED(objectInfo)) |
- ObjectInfoAddPerformingType(objectInfo, 'splice'); |
+ BeginPerformChange(objectInfo, 'splice'); |
} |
function EndPerformSplice(array) { |
var objectInfo = objectInfoMap.get(array); |
if (!IS_UNDEFINED(objectInfo)) |
- ObjectInfoRemovePerformingType(objectInfo, 'splice'); |
+ EndPerformChange(objectInfo, 'splice'); |
} |
function EnqueueSpliceRecord(array, index, removed, addedCount) { |
var objectInfo = objectInfoMap.get(array); |
- if (!ObjectInfoHasActiveObservers(objectInfo)) |
+ if (IS_UNDEFINED(objectInfo) || objectInfo.changeObservers.length === 0) |
return; |
var changeRecord = { |
@@ -425,19 +315,19 @@ function EnqueueSpliceRecord(array, index, removed, addedCount) { |
ObjectFreeze(changeRecord); |
ObjectFreeze(changeRecord.removed); |
- ObjectInfoEnqueueChangeRecord(objectInfo, changeRecord); |
+ EnqueueChangeRecord(changeRecord, objectInfo.changeObservers); |
} |
function NotifyChange(type, object, name, oldValue) { |
var objectInfo = objectInfoMap.get(object); |
- if (!ObjectInfoHasActiveObservers(objectInfo)) |
+ if (objectInfo.changeObservers.length === 0) |
return; |
var changeRecord = (arguments.length < 4) ? |
{ type: type, object: object, name: name } : |
{ type: type, object: object, name: name, oldValue: oldValue }; |
ObjectFreeze(changeRecord); |
- ObjectInfoEnqueueChangeRecord(objectInfo, changeRecord); |
+ EnqueueChangeRecord(changeRecord, objectInfo.changeObservers); |
} |
var notifierPrototype = {}; |
@@ -446,16 +336,17 @@ function ObjectNotifierNotify(changeRecord) { |
if (!IS_SPEC_OBJECT(this)) |
throw MakeTypeError("called_on_non_object", ["notify"]); |
- var objectInfo = ObjectInfoGetFromNotifier(this); |
- if (IS_UNDEFINED(objectInfo)) |
+ var target = notifierTargetMap.get(this); |
+ if (IS_UNDEFINED(target)) |
throw MakeTypeError("observe_notify_non_notifier"); |
if (!IS_STRING(changeRecord.type)) |
throw MakeTypeError("observe_type_non_string"); |
- if (!ObjectInfoHasActiveObservers(objectInfo)) |
+ var objectInfo = objectInfoMap.get(target); |
+ if (IS_UNDEFINED(objectInfo) || objectInfo.changeObservers.length === 0) |
return; |
- var newRecord = { object: ObjectInfoGetObject(objectInfo) }; |
+ var newRecord = { object: target }; |
for (var prop in changeRecord) { |
if (prop === 'object') continue; |
%DefineOrRedefineDataProperty(newRecord, prop, changeRecord[prop], |
@@ -463,16 +354,15 @@ function ObjectNotifierNotify(changeRecord) { |
} |
ObjectFreeze(newRecord); |
- ObjectInfoEnqueueChangeRecord(objectInfo, newRecord); |
+ EnqueueChangeRecord(newRecord, objectInfo.changeObservers); |
} |
function ObjectNotifierPerformChange(changeType, changeFn, receiver) { |
if (!IS_SPEC_OBJECT(this)) |
throw MakeTypeError("called_on_non_object", ["performChange"]); |
- var objectInfo = ObjectInfoGetFromNotifier(this); |
- |
- if (IS_UNDEFINED(objectInfo)) |
+ var target = notifierTargetMap.get(this); |
+ if (IS_UNDEFINED(target)) |
throw MakeTypeError("observe_notify_non_notifier"); |
if (!IS_STRING(changeType)) |
throw MakeTypeError("observe_perform_non_string"); |
@@ -485,11 +375,15 @@ function ObjectNotifierPerformChange(changeType, changeFn, receiver) { |
receiver = ToObject(receiver); |
} |
- ObjectInfoAddPerformingType(objectInfo, changeType); |
+ var objectInfo = objectInfoMap.get(target); |
+ if (IS_UNDEFINED(objectInfo)) |
+ return; |
+ |
+ BeginPerformChange(objectInfo, changeType); |
try { |
%_CallFunction(receiver, changeFn); |
} finally { |
- ObjectInfoRemovePerformingType(objectInfo, changeType); |
+ EndPerformChange(objectInfo, changeType); |
} |
} |
@@ -499,8 +393,18 @@ function ObjectGetNotifier(object) { |
if (ObjectIsFrozen(object)) return null; |
- var objectInfo = ObjectInfoGet(object); |
- return ObjectInfoGetNotifier(objectInfo); |
+ var objectInfo = objectInfoMap.get(object); |
+ if (IS_UNDEFINED(objectInfo)) { |
+ objectInfo = CreateObjectInfo(object); |
+ %SetIsObserved(object); |
+ } |
+ |
+ if (IS_NULL(objectInfo.notifier)) { |
+ objectInfo.notifier = { __proto__: notifierPrototype }; |
+ notifierTargetMap.set(objectInfo.notifier, object); |
+ } |
+ |
+ return objectInfo.notifier; |
} |
function CallbackDeliverPending(callback) { |
@@ -513,9 +417,7 @@ function CallbackDeliverPending(callback) { |
var priority = callbackInfo.priority; |
callbackInfoMap.set(callback, priority); |
- if (observationState.pendingObservers) |
- delete observationState.pendingObservers[priority]; |
- |
+ delete observationState.pendingObservers[priority]; |
var delivered = []; |
%MoveArrayContents(callbackInfo, delivered); |
@@ -533,9 +435,9 @@ function ObjectDeliverChangeRecords(callback) { |
} |
function DeliverChangeRecords() { |
- while (observationState.pendingObservers) { |
+ while (observationState.pendingObservers.length) { |
var pendingObservers = observationState.pendingObservers; |
- observationState.pendingObservers = null; |
+ observationState.pendingObservers = new InternalArray; |
for (var i in pendingObservers) { |
CallbackDeliverPending(pendingObservers[i]); |
} |