This module’s .observable method proxies an Array to make it observable.
The other exports are not used elsewhere.
.set is the only proxy method that differs from make-object‘s.
var ObservationRecorder = require("can-observation-recorder"); var mapBindings = require("can-event-queue/map/map"); var canReflect = require("can-reflect"); var makeObject = require("./-make-object"); var symbols = require("./-symbols"); var observableStore = require("./-observable-store"); var helpers = require("./-helpers");
Returns if prop is an integer
var isInteger = Number.isInteger || function(value) { return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; };
Returns true if the length was set and it deleted indexed
properties.
function didLengthChangeCauseDeletions(key, value, old) { return key === "length" && value < old; }
The following rewrites array methods to generate events and for performance reasons.
Array’s methods that mutate are rewritten to generate patch events. Other methods on array are rewritten to:
ObservationRecorder.add on every property.The following defines a relationship between an array mutation method and the patch events that should be dispatched for that mutation.
var mutateMethods = { "push": function(arr, args) { return [{ index: arr.length - args.length, deleteCount: 0, insert: args, type: "splice" }]; }, "pop": function(arr) { return [{ index: arr.length, deleteCount: 1, insert: [], type: "splice" }]; }, "shift": function() { return [{ index: 0, deleteCount: 1, insert: [], type: "splice" }]; }, "unshift": function(arr, args) { return [{ index: 0, deleteCount: 0, insert: args, type: "splice" }]; }, "splice": function(arr, args) { return [{ index: args[0], deleteCount: args[1], insert: args.slice(2), type: "splice" }]; }, "sort": function(arr) {
The array replaced everything.
return [{ index: 0, deleteCount: arr.length, insert: arr, type: "splice" }]; }, "reverse": function(arr, args, old) {
The array replaced everything.
return [{ index: 0, deleteCount: arr.length, insert: arr, type: "splice" }]; } };
Overwrite Array’s methods that mutate to:
canReflect.eachKey(mutateMethods, function(makePatches, prop){ var protoFn = Array.prototype[prop]; var mutateMethod = function() { var meta = this[symbols.metaSymbol],
Capture if this function should be making sideEffects
makeSideEffects = meta.preventSideEffects === 0,
oldLength = meta.target.length;Prevent proxy from calling ObservationRecorder and sending events.
meta.preventSideEffects++;
Call the function – note that this is the Proxy here, so
accesses in the function still go through get() and set().
var ret = protoFn.apply(meta.target, arguments); var patches = makePatches(meta.target, Array.from(arguments), oldLength); if (makeSideEffects === true) {
!steal-remove-start
var reasonLog = [canReflect.getName(meta.proxy)+"."+prop+" called with", arguments];
!steal-remove-end
mapBindings.dispatch.call( meta.proxy, {
type: "length",!steal-remove-start
reasonLog: reasonLog,
!steal-remove-end
patches: patches
}, [meta.target.length, oldLength]);
}
meta.preventSideEffects--;
return ret;
};!steal-remove-start
Object.defineProperty(mutateMethod, "name", { value: prop });
!steal-remove-end
Store the proxied method so it will be used instead of the prototype method.
observableStore.proxiedObjects.set(protoFn, mutateMethod); observableStore.proxies.add(mutateMethod); });
The following rewrites the Array methods to signal
to ObservationRecorder to bind on patches events.
It also prevents the proxy handlers calling ObservationRecorder
themselves.
Object.getOwnPropertyNames(Array.prototype).forEach(function(prop) { var protoFn = Array.prototype[prop]; if (observableStore.proxiedObjects.has(protoFn)) { return; } if (prop !== "constructor" && typeof protoFn === "function") { var arrayMethod = function() { ObservationRecorder.add(this, symbols.patchesSymbol); var meta = this[symbols.metaSymbol]; meta.preventSideEffects++; var ret = protoFn.apply(this, arguments); meta.preventSideEffects--; return meta.options.observe(ret); };
!steal-remove-start
Object.defineProperty(arrayMethod, "name", { value: prop });
!steal-remove-end
observableStore.proxiedObjects.set(protoFn, arrayMethod); observableStore.proxies.add(arrayMethod); } });
Array’s have the same proxy keys as objects.
var proxyKeys = helpers.assignEverything(Object.create(null), makeObject.proxyKeys()); var makeArray = {
Returns a proxied version of the array.
array - An array to proxy.options - Configurable behaviors.proxyKeys - Keys that will override any keys on array. Defaults to makeObject.proxyKeys.observe(nonObservable) - A function that converts a nested value to an observable.shouldRecordObservation(keyInfo, meta) - Returns if ObservationRecorder
should be called. Defaults to makeObject.shouldRecordObservationOnOwnAndMissingKeys.observable: function(array, options) { if(options.shouldRecordObservation === undefined) { options.shouldRecordObservation = makeObject.shouldRecordObservationOnOwnAndMissingKeys; } var meta = { target: array, proxyKeys: options.proxyKeys !== undefined ? options.proxyKeys : Object.create(makeArray.proxyKeys()), options: options,
preventSideEffects is a counter used to "turn off" the proxy. This is incremented when some
function (like Array.splice) wants to handle event dispatching and/or calling
ObservationRecorder itself for performance reasons.
preventSideEffects: 0 }; meta.proxyKeys[symbols.metaSymbol] = meta; meta.proxy = new Proxy(array, { get: makeObject.get.bind(meta), set: makeArray.set.bind(meta), ownKeys: makeObject.ownKeys.bind(meta), deleteProperty: makeObject.deleteProperty.bind(meta), meta: meta }); mapBindings.addHandlers(meta.proxy, meta); return meta.proxy; }, proxyKeys: function() { return proxyKeys; },
set is called when a property is set on the proxy or an object
that has the proxy on its prototype.
set: function(target, key, value, receiver) {If the receiver is not this observable (the observable might be on the proto chain), set the key on the reciever.
if (receiver !== this.proxy) { return makeObject.setKey(receiver, key, value, this); }
Gets the observable value to set.
value = makeObject.getValueToSet(key, value, this); var startingLength = target.length;
Sets the value on the target. If there is a change, calls the callback.
makeObject.setValueAndOnChange(key, value, this, function(key, value, meta, hadOwn, old) {
Determine the patches this change should dispatch
var patches = [{ key: key, type: hadOwn ? "set" : "add", value: value }]; var numberKey = +key;
If we are adding an indexed value like arr[5] =value ...
if ( isInteger(numberKey) ) {If we set an enumerable property after the length ...
if (!hadOwn && numberKey > startingLength) {... add patches for those values.
patches.push({
index: startingLength,
deleteCount: 0,
insert: target.slice(startingLength),
type: "splice"
});
} else {Otherwise, splice the value into the array.
patches.push.apply(patches, mutateMethods.splice(target, [numberKey, 1, value]));
}
}In the case of deleting items by setting the length of the array, add patches that splice the items removed. (deleting individual items from an array doesn’t change the length; it just creates holes)
if (didLengthChangeCauseDeletions(key, value, old, meta)) { patches.push({ index: value, deleteCount: old - value, insert: [], type: "splice" }); }
!steal-remove-start
var reasonLog = [canReflect.getName(meta.proxy)+" set", key,"to", value];
!steal-remove-end
mapBindings.dispatch.call( meta.proxy, {
type: key,!steal-remove-start
/* jshint laxcomma: true */ reasonLog: reasonLog, /* jshint laxcomma: false */
!steal-remove-end
patches: patches, keyChanged: !hadOwn ? key : undefined },[value, old]); }); return true; } }; module.exports = makeArray;