can-observe
Version:
Like can.Map, but without the .attr method.
318 lines (284 loc) • 9.5 kB
JavaScript
// # -make-array.js
// 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");
var computedHelpers = require("./-computed-helpers");
var isSymbolLike = canReflect.isSymbolLike;
// 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;
}
// ## Rewrite array methods
// 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:
// - Avoid calling `ObservationRecorder.add` on every property.
// - Make the returned result observable.
//
// ### Rewrite mutating methods
// 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:
// - prevent other events from being fired off (index events and length events.)
// - dispatch patches events.
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
var dispatchArgs = {
type: "length",
patches: patches
};
//!steal-remove-start
if(process.env.NODE_ENV !== 'production') {
dispatchArgs.reasonLog = reasonLog;
}
//!steal-remove-end
mapBindings.dispatch.call( meta.proxy, dispatchArgs , [meta.target.length, oldLength]);
}
meta.preventSideEffects--;
return ret;
};
//!steal-remove-start
if(process.env.NODE_ENV !== 'production') {
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);
});
// ### Rewrite non-mutating methods
// 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
if(process.env.NODE_ENV !== 'production') {
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()),
computedKeys: Object.create(null),
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);
}
// If it has a defined property definiition
var computedValue = computedHelpers.set(receiver, key, value);
if(computedValue === true ) {
return true;
}
// 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 = !isSymbolLike(key) && +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
var dispatchArgs = {
type: key,
patches: patches,
keyChanged: !hadOwn ? key : undefined
};
//!steal-remove-start
if(process.env.NODE_ENV !== 'production') {
dispatchArgs.reasonLog = reasonLog;
}
//!steal-remove-end
mapBindings.dispatch.call( meta.proxy, dispatchArgs, [value, old]);
});
return true;
}
};
module.exports = makeArray;
;