mobx
Version:
Simple, scalable state management.
1,317 lines (1,305 loc) • 159 kB
JavaScript
/** MobX - (c) Michel Weststrate 2015 - 2019 - MIT Licensed */
const OBFUSCATED_ERROR = "An invariant failed, however the error is obfuscated because this is an production build.";
const EMPTY_ARRAY = [];
Object.freeze(EMPTY_ARRAY);
const EMPTY_OBJECT = {};
Object.freeze(EMPTY_OBJECT);
function getNextId() {
return ++globalState.mobxGuid;
}
function fail(message) {
invariant(false, message);
throw "X"; // unreachable
}
function invariant(check, message) {
if (!check)
throw new Error("[mobx] " + (message || OBFUSCATED_ERROR));
}
/**
* Prints a deprecation message, but only one time.
* Returns false if the deprecated message was already printed before
*/
const deprecatedMessages = [];
function deprecated(msg, thing) {
if (process.env.NODE_ENV === "production")
return false;
if (thing) {
return deprecated(`'${msg}', use '${thing}' instead.`);
}
if (deprecatedMessages.indexOf(msg) !== -1)
return false;
deprecatedMessages.push(msg);
console.error("[mobx] Deprecated: " + msg);
return true;
}
/**
* Makes sure that the provided function is invoked at most once.
*/
function once(func) {
let invoked = false;
return function () {
if (invoked)
return;
invoked = true;
return func.apply(this, arguments);
};
}
const noop = () => { };
function unique(list) {
const res = [];
list.forEach(item => {
if (res.indexOf(item) === -1)
res.push(item);
});
return res;
}
function isObject(value) {
return value !== null && typeof value === "object";
}
function isPlainObject(value) {
if (value === null || typeof value !== "object")
return false;
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}
function addHiddenProp(object, propName, value) {
Object.defineProperty(object, propName, {
enumerable: false,
writable: true,
configurable: true,
value
});
}
function addHiddenFinalProp(object, propName, value) {
Object.defineProperty(object, propName, {
enumerable: false,
writable: false,
configurable: true,
value
});
}
function isPropertyConfigurable(object, prop) {
const descriptor = Object.getOwnPropertyDescriptor(object, prop);
return !descriptor || (descriptor.configurable !== false && descriptor.writable !== false);
}
function assertPropertyConfigurable(object, prop) {
if (process.env.NODE_ENV !== "production" && !isPropertyConfigurable(object, prop))
fail(`Cannot make property '${prop.toString()}' observable, it is not configurable and writable in the target object`);
}
function createInstanceofPredicate(name, clazz) {
const propName = "isMobX" + name;
clazz.prototype[propName] = true;
return function (x) {
return isObject(x) && x[propName] === true;
};
}
/**
* Returns whether the argument is an array, disregarding observability.
*/
function isArrayLike(x) {
return Array.isArray(x) || isObservableArray(x);
}
function isES6Map(thing) {
return thing instanceof Map;
}
function isES6Set(thing) {
return thing instanceof Set;
}
/**
* Returns the following: own keys, prototype keys & own symbol keys, if they are enumerable.
*/
function getPlainObjectKeys(object) {
const enumerables = new Set();
for (let key in object)
enumerables.add(key); // *all* enumerables
Object.getOwnPropertySymbols(object).forEach(k => {
if (Object.getOwnPropertyDescriptor(object, k).enumerable)
enumerables.add(k);
}); // *own* symbols
// Note: this implementation is missing enumerable, inherited, symbolic property names! That would however pretty expensive to add,
// as there is no efficient iterator that returns *all* properties
return Array.from(enumerables);
}
function stringifyKey(key) {
if (key && key.toString)
return key.toString();
else
return new String(key).toString();
}
function getMapLikeKeys(map) {
if (isPlainObject(map))
return Object.keys(map);
if (Array.isArray(map))
return map.map(([key]) => key);
if (isES6Map(map) || isObservableMap(map))
return Array.from(map.keys());
return fail(`Cannot get keys from '${map}'`);
}
function toPrimitive(value) {
return value === null ? null : typeof value === "object" ? "" + value : value;
}
const $mobx = Symbol("mobx administration");
class Atom {
/**
* Create a new atom. For debugging purposes it is recommended to give it a name.
* The onBecomeObserved and onBecomeUnobserved callbacks can be used for resource management.
*/
constructor(name = "Atom@" + getNextId()) {
this.name = name;
this.isPendingUnobservation = false; // for effective unobserving. BaseAtom has true, for extra optimization, so its onBecomeUnobserved never gets called, because it's not needed
this.isBeingObserved = false;
this.observers = new Set();
this.diffValue = 0;
this.lastAccessedBy = 0;
this.lowestObserverState = IDerivationState.NOT_TRACKING;
}
onBecomeObserved() {
if (this.onBecomeObservedListeners) {
this.onBecomeObservedListeners.forEach(listener => listener());
}
}
onBecomeUnobserved() {
if (this.onBecomeUnobservedListeners) {
this.onBecomeUnobservedListeners.forEach(listener => listener());
}
}
/**
* Invoke this method to notify mobx that your atom has been used somehow.
* Returns true if there is currently a reactive context.
*/
reportObserved() {
return reportObserved(this);
}
/**
* Invoke this method _after_ this method has changed to signal mobx that all its observers should invalidate.
*/
reportChanged() {
startBatch();
propagateChanged(this);
endBatch();
}
toString() {
return this.name;
}
}
const isAtom = createInstanceofPredicate("Atom", Atom);
function createAtom(name, onBecomeObservedHandler = noop, onBecomeUnobservedHandler = noop) {
const atom = new Atom(name);
// default `noop` listener will not initialize the hook Set
if (onBecomeObservedHandler !== noop) {
onBecomeObserved(atom, onBecomeObservedHandler);
}
if (onBecomeUnobservedHandler !== noop) {
onBecomeUnobserved(atom, onBecomeUnobservedHandler);
}
return atom;
}
function identityComparer(a, b) {
return a === b;
}
function structuralComparer(a, b) {
return deepEqual(a, b);
}
function defaultComparer(a, b) {
return Object.is(a, b);
}
const comparer = {
identity: identityComparer,
structural: structuralComparer,
default: defaultComparer
};
const mobxDidRunLazyInitializersSymbol = Symbol("mobx did run lazy initializers");
const mobxPendingDecorators = Symbol("mobx pending decorators");
const enumerableDescriptorCache = {};
const nonEnumerableDescriptorCache = {};
function createPropertyInitializerDescriptor(prop, enumerable) {
const cache = enumerable ? enumerableDescriptorCache : nonEnumerableDescriptorCache;
return (cache[prop] ||
(cache[prop] = {
configurable: true,
enumerable: enumerable,
get() {
initializeInstance(this);
return this[prop];
},
set(value) {
initializeInstance(this);
this[prop] = value;
}
}));
}
function initializeInstance(target) {
if (target[mobxDidRunLazyInitializersSymbol] === true)
return;
const decorators = target[mobxPendingDecorators];
if (decorators) {
addHiddenProp(target, mobxDidRunLazyInitializersSymbol, true);
for (let key in decorators) {
const d = decorators[key];
d.propertyCreator(target, d.prop, d.descriptor, d.decoratorTarget, d.decoratorArguments);
}
}
}
function createPropDecorator(propertyInitiallyEnumerable, propertyCreator) {
return function decoratorFactory() {
let decoratorArguments;
const decorator = function decorate(target, prop, descriptor, applyImmediately
// This is a special parameter to signal the direct application of a decorator, allow extendObservable to skip the entire type decoration part,
// as the instance to apply the decorator to equals the target
) {
if (applyImmediately === true) {
propertyCreator(target, prop, descriptor, target, decoratorArguments);
return null;
}
if (process.env.NODE_ENV !== "production" && !quacksLikeADecorator(arguments))
fail("This function is a decorator, but it wasn't invoked like a decorator");
if (!Object.prototype.hasOwnProperty.call(target, mobxPendingDecorators)) {
const inheritedDecorators = target[mobxPendingDecorators];
addHiddenProp(target, mobxPendingDecorators, Object.assign({}, inheritedDecorators));
}
target[mobxPendingDecorators][prop] = {
prop,
propertyCreator,
descriptor,
decoratorTarget: target,
decoratorArguments
};
return createPropertyInitializerDescriptor(prop, propertyInitiallyEnumerable);
};
if (quacksLikeADecorator(arguments)) {
// @decorator
decoratorArguments = EMPTY_ARRAY;
return decorator.apply(null, arguments);
}
else {
// @decorator(args)
decoratorArguments = Array.prototype.slice.call(arguments);
return decorator;
}
};
}
function quacksLikeADecorator(args) {
return (((args.length === 2 || args.length === 3) && typeof args[1] === "string") ||
(args.length === 4 && args[3] === true));
}
function deepEnhancer(v, _, name) {
// it is an observable already, done
if (isObservable(v))
return v;
// something that can be converted and mutated?
if (Array.isArray(v))
return observable.array(v, { name });
if (isPlainObject(v))
return observable.object(v, undefined, { name });
if (isES6Map(v))
return observable.map(v, { name });
if (isES6Set(v))
return observable.set(v, { name });
return v;
}
function shallowEnhancer(v, _, name) {
if (v === undefined || v === null)
return v;
if (isObservableObject(v) || isObservableArray(v) || isObservableMap(v) || isObservableSet(v))
return v;
if (Array.isArray(v))
return observable.array(v, { name, deep: false });
if (isPlainObject(v))
return observable.object(v, undefined, { name, deep: false });
if (isES6Map(v))
return observable.map(v, { name, deep: false });
if (isES6Set(v))
return observable.set(v, { name, deep: false });
return fail(process.env.NODE_ENV !== "production" &&
"The shallow modifier / decorator can only used in combination with arrays, objects, maps and sets");
}
function referenceEnhancer(newValue) {
// never turn into an observable
return newValue;
}
function refStructEnhancer(v, oldValue, name) {
if (process.env.NODE_ENV !== "production" && isObservable(v))
throw `observable.struct should not be used with observable values`;
if (deepEqual(v, oldValue))
return oldValue;
return v;
}
function createDecoratorForEnhancer(enhancer) {
invariant(enhancer);
const decorator = createPropDecorator(true, (target, propertyName, descriptor, _decoratorTarget, decoratorArgs) => {
if (process.env.NODE_ENV !== "production") {
invariant(!descriptor || !descriptor.get, `@observable cannot be used on getter (property "${stringifyKey(propertyName)}"), use @computed instead.`);
}
const initialValue = descriptor
? descriptor.initializer
? descriptor.initializer.call(target)
: descriptor.value
: undefined;
asObservableObject(target).addObservableProp(propertyName, initialValue, enhancer);
});
const res =
// Extra process checks, as this happens during module initialization
typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production"
? function observableDecorator() {
// This wrapper function is just to detect illegal decorator invocations, deprecate in a next version
// and simply return the created prop decorator
if (arguments.length < 2)
return fail("Incorrect decorator invocation. @observable decorator doesn't expect any arguments");
return decorator.apply(null, arguments);
}
: decorator;
res.enhancer = enhancer;
return res;
}
// Predefined bags of create observable options, to avoid allocating temporarily option objects
// in the majority of cases
const defaultCreateObservableOptions = {
deep: true,
name: undefined,
defaultDecorator: undefined,
proxy: true
};
Object.freeze(defaultCreateObservableOptions);
function assertValidOption(key) {
if (!/^(deep|name|equals|defaultDecorator|proxy)$/.test(key))
fail(`invalid option for (extend)observable: ${key}`);
}
function asCreateObservableOptions(thing) {
if (thing === null || thing === undefined)
return defaultCreateObservableOptions;
if (typeof thing === "string")
return { name: thing, deep: true, proxy: true };
if (process.env.NODE_ENV !== "production") {
if (typeof thing !== "object")
return fail("expected options object");
Object.keys(thing).forEach(assertValidOption);
}
return thing;
}
const deepDecorator = createDecoratorForEnhancer(deepEnhancer);
const shallowDecorator = createDecoratorForEnhancer(shallowEnhancer);
const refDecorator = createDecoratorForEnhancer(referenceEnhancer);
const refStructDecorator = createDecoratorForEnhancer(refStructEnhancer);
function getEnhancerFromOptions(options) {
return options.defaultDecorator
? options.defaultDecorator.enhancer
: options.deep === false
? referenceEnhancer
: deepEnhancer;
}
/**
* Turns an object, array or function into a reactive structure.
* @param v the value which should become observable.
*/
function createObservable(v, arg2, arg3) {
// @observable someProp;
if (typeof arguments[1] === "string") {
return deepDecorator.apply(null, arguments);
}
// it is an observable already, done
if (isObservable(v))
return v;
// something that can be converted and mutated?
const res = isPlainObject(v)
? observable.object(v, arg2, arg3)
: Array.isArray(v)
? observable.array(v, arg2)
: isES6Map(v)
? observable.map(v, arg2)
: isES6Set(v)
? observable.set(v, arg2)
: v;
// this value could be converted to a new observable data structure, return it
if (res !== v)
return res;
// otherwise, just box it
fail(process.env.NODE_ENV !== "production" &&
`The provided value could not be converted into an observable. If you want just create an observable reference to the object use 'observable.box(value)'`);
}
const observableFactories = {
box(value, options) {
if (arguments.length > 2)
incorrectlyUsedAsDecorator("box");
const o = asCreateObservableOptions(options);
return new ObservableValue(value, getEnhancerFromOptions(o), o.name, true, o.equals);
},
array(initialValues, options) {
if (arguments.length > 2)
incorrectlyUsedAsDecorator("array");
const o = asCreateObservableOptions(options);
return createObservableArray(initialValues, getEnhancerFromOptions(o), o.name);
},
map(initialValues, options) {
if (arguments.length > 2)
incorrectlyUsedAsDecorator("map");
const o = asCreateObservableOptions(options);
return new ObservableMap(initialValues, getEnhancerFromOptions(o), o.name);
},
set(initialValues, options) {
if (arguments.length > 2)
incorrectlyUsedAsDecorator("set");
const o = asCreateObservableOptions(options);
return new ObservableSet(initialValues, getEnhancerFromOptions(o), o.name);
},
object(props, decorators, options) {
if (typeof arguments[1] === "string")
incorrectlyUsedAsDecorator("object");
const o = asCreateObservableOptions(options);
if (o.proxy === false) {
return extendObservable({}, props, decorators, o);
}
else {
const defaultDecorator = getDefaultDecoratorFromObjectOptions(o);
const base = extendObservable({}, undefined, undefined, o);
const proxy = createDynamicObservableObject(base);
extendObservableObjectWithProperties(proxy, props, decorators, defaultDecorator);
return proxy;
}
},
ref: refDecorator,
shallow: shallowDecorator,
deep: deepDecorator,
struct: refStructDecorator
};
const observable = createObservable;
// weird trick to keep our typings nicely with our funcs, and still extend the observable function
Object.keys(observableFactories).forEach(name => (observable[name] = observableFactories[name]));
function incorrectlyUsedAsDecorator(methodName) {
fail(
// process.env.NODE_ENV !== "production" &&
`Expected one or two arguments to observable.${methodName}. Did you accidentally try to use observable.${methodName} as decorator?`);
}
const computedDecorator = createPropDecorator(false, (instance, propertyName, descriptor, decoratorTarget, decoratorArgs) => {
const { get, set } = descriptor; // initialValue is the descriptor for get / set props
// Optimization: faster on decorator target or instance? Assuming target
// Optimization: find out if declaring on instance isn't just faster. (also makes the property descriptor simpler). But, more memory usage..
// Forcing instance now, fixes hot reloadig issues on React Native:
const options = decoratorArgs[0] || {};
asObservableObject(instance).addComputedProp(instance, propertyName, Object.assign({ get,
set, context: instance }, options));
});
const computedStructDecorator = computedDecorator({ equals: comparer.structural });
/**
* Decorator for class properties: @computed get value() { return expr; }.
* For legacy purposes also invokable as ES5 observable created: `computed(() => expr)`;
*/
const computed = function computed(arg1, arg2, arg3) {
if (typeof arg2 === "string") {
// @computed
return computedDecorator.apply(null, arguments);
}
if (arg1 !== null && typeof arg1 === "object" && arguments.length === 1) {
// @computed({ options })
return computedDecorator.apply(null, arguments);
}
// computed(expr, options?)
if (process.env.NODE_ENV !== "production") {
invariant(typeof arg1 === "function", "First argument to `computed` should be an expression.");
invariant(arguments.length < 3, "Computed takes one or two arguments if used as function");
}
const opts = typeof arg2 === "object" ? arg2 : {};
opts.get = arg1;
opts.set = typeof arg2 === "function" ? arg2 : opts.set;
opts.name = opts.name || arg1.name || ""; /* for generated name */
return new ComputedValue(opts);
};
computed.struct = computedStructDecorator;
function createAction(actionName, fn, ref) {
if (process.env.NODE_ENV !== "production") {
invariant(typeof fn === "function", "`action` can only be invoked on functions");
if (typeof actionName !== "string" || !actionName)
fail(`actions should have valid names, got: '${actionName}'`);
}
const res = function () {
return executeAction(actionName, fn, ref || this, arguments);
};
res.isMobxAction = true;
return res;
}
function executeAction(actionName, fn, scope, args) {
const runInfo = startAction(actionName, fn, scope, args);
let shouldSupressReactionError = true;
try {
const res = fn.apply(scope, args);
shouldSupressReactionError = false;
return res;
}
finally {
if (shouldSupressReactionError) {
globalState.suppressReactionErrors = shouldSupressReactionError;
endAction(runInfo);
globalState.suppressReactionErrors = false;
}
else {
endAction(runInfo);
}
}
}
function startAction(actionName, fn, scope, args) {
const notifySpy = isSpyEnabled() && !!actionName;
let startTime = 0;
if (notifySpy && process.env.NODE_ENV !== "production") {
startTime = Date.now();
const l = (args && args.length) || 0;
const flattendArgs = new Array(l);
if (l > 0)
for (let i = 0; i < l; i++)
flattendArgs[i] = args[i];
spyReportStart({
type: "action",
name: actionName,
object: scope,
arguments: flattendArgs
});
}
const prevDerivation = untrackedStart();
startBatch();
const prevAllowStateChanges = allowStateChangesStart(true);
return {
prevDerivation,
prevAllowStateChanges,
notifySpy,
startTime
};
}
function endAction(runInfo) {
allowStateChangesEnd(runInfo.prevAllowStateChanges);
endBatch();
untrackedEnd(runInfo.prevDerivation);
if (runInfo.notifySpy && process.env.NODE_ENV !== "production")
spyReportEnd({ time: Date.now() - runInfo.startTime });
}
function allowStateChanges(allowStateChanges, func) {
const prev = allowStateChangesStart(allowStateChanges);
let res;
try {
res = func();
}
finally {
allowStateChangesEnd(prev);
}
return res;
}
function allowStateChangesStart(allowStateChanges) {
const prev = globalState.allowStateChanges;
globalState.allowStateChanges = allowStateChanges;
return prev;
}
function allowStateChangesEnd(prev) {
globalState.allowStateChanges = prev;
}
function allowStateChangesInsideComputed(func) {
const prev = globalState.computationDepth;
globalState.computationDepth = 0;
let res;
try {
res = func();
}
finally {
globalState.computationDepth = prev;
}
return res;
}
class ObservableValue extends Atom {
constructor(value, enhancer, name = "ObservableValue@" + getNextId(), notifySpy = true, equals = comparer.default) {
super(name);
this.enhancer = enhancer;
this.name = name;
this.equals = equals;
this.hasUnreportedChange = false;
this.value = enhancer(value, undefined, name);
if (notifySpy && isSpyEnabled() && process.env.NODE_ENV !== "production") {
// only notify spy if this is a stand-alone observable
spyReport({ type: "create", name: this.name, newValue: "" + this.value });
}
}
dehanceValue(value) {
if (this.dehancer !== undefined)
return this.dehancer(value);
return value;
}
set(newValue) {
const oldValue = this.value;
newValue = this.prepareNewValue(newValue);
if (newValue !== globalState.UNCHANGED) {
const notifySpy = isSpyEnabled();
if (notifySpy && process.env.NODE_ENV !== "production") {
spyReportStart({
type: "update",
name: this.name,
newValue,
oldValue
});
}
this.setNewValue(newValue);
if (notifySpy && process.env.NODE_ENV !== "production")
spyReportEnd();
}
}
prepareNewValue(newValue) {
checkIfStateModificationsAreAllowed(this);
if (hasInterceptors(this)) {
const change = interceptChange(this, {
object: this,
type: "update",
newValue
});
if (!change)
return globalState.UNCHANGED;
newValue = change.newValue;
}
// apply modifier
newValue = this.enhancer(newValue, this.value, this.name);
return this.equals(this.value, newValue) ? globalState.UNCHANGED : newValue;
}
setNewValue(newValue) {
const oldValue = this.value;
this.value = newValue;
this.reportChanged();
if (hasListeners(this)) {
notifyListeners(this, {
type: "update",
object: this,
newValue,
oldValue
});
}
}
get() {
this.reportObserved();
return this.dehanceValue(this.value);
}
intercept(handler) {
return registerInterceptor(this, handler);
}
observe(listener, fireImmediately) {
if (fireImmediately)
listener({
object: this,
type: "update",
newValue: this.value,
oldValue: undefined
});
return registerListener(this, listener);
}
toJSON() {
return this.get();
}
toString() {
return `${this.name}[${this.value}]`;
}
valueOf() {
return toPrimitive(this.get());
}
[Symbol.toPrimitive]() {
return this.valueOf();
}
}
const isObservableValue = createInstanceofPredicate("ObservableValue", ObservableValue);
/**
* A node in the state dependency root that observes other nodes, and can be observed itself.
*
* ComputedValue will remember the result of the computation for the duration of the batch, or
* while being observed.
*
* During this time it will recompute only when one of its direct dependencies changed,
* but only when it is being accessed with `ComputedValue.get()`.
*
* Implementation description:
* 1. First time it's being accessed it will compute and remember result
* give back remembered result until 2. happens
* 2. First time any deep dependency change, propagate POSSIBLY_STALE to all observers, wait for 3.
* 3. When it's being accessed, recompute if any shallow dependency changed.
* if result changed: propagate STALE to all observers, that were POSSIBLY_STALE from the last step.
* go to step 2. either way
*
* If at any point it's outside batch and it isn't observed: reset everything and go to 1.
*/
class ComputedValue {
/**
* Create a new computed value based on a function expression.
*
* The `name` property is for debug purposes only.
*
* The `equals` property specifies the comparer function to use to determine if a newly produced
* value differs from the previous value. Two comparers are provided in the library; `defaultComparer`
* compares based on identity comparison (===), and `structualComparer` deeply compares the structure.
* Structural comparison can be convenient if you always produce a new aggregated object and
* don't want to notify observers if it is structurally the same.
* This is useful for working with vectors, mouse coordinates etc.
*/
constructor(options) {
this.dependenciesState = IDerivationState.NOT_TRACKING;
this.observing = []; // nodes we are looking at. Our value depends on these nodes
this.newObserving = null; // during tracking it's an array with new observed observers
this.isBeingObserved = false;
this.isPendingUnobservation = false;
this.observers = new Set();
this.diffValue = 0;
this.runId = 0;
this.lastAccessedBy = 0;
this.lowestObserverState = IDerivationState.UP_TO_DATE;
this.unboundDepsCount = 0;
this.__mapid = "#" + getNextId();
this.value = new CaughtException(null);
this.isComputing = false; // to check for cycles
this.isRunningSetter = false;
this.isTracing = TraceMode.NONE;
if (process.env.NODE_ENV !== "production" && !options.get)
throw "[mobx] missing option for computed: get";
this.derivation = options.get;
this.name = options.name || "ComputedValue@" + getNextId();
if (options.set)
this.setter = createAction(this.name + "-setter", options.set);
this.equals =
options.equals ||
(options.compareStructural || options.struct
? comparer.structural
: comparer.default);
this.scope = options.context;
this.requiresReaction = !!options.requiresReaction;
this.keepAlive = !!options.keepAlive;
}
onBecomeStale() {
propagateMaybeChanged(this);
}
onBecomeObserved() {
if (this.onBecomeObservedListeners) {
this.onBecomeObservedListeners.forEach(listener => listener());
}
}
onBecomeUnobserved() {
if (this.onBecomeUnobservedListeners) {
this.onBecomeUnobservedListeners.forEach(listener => listener());
}
}
/**
* Returns the current value of this computed value.
* Will evaluate its computation first if needed.
*/
get() {
if (this.isComputing)
fail(`Cycle detected in computation ${this.name}: ${this.derivation}`);
if (globalState.inBatch === 0 && this.observers.size === 0 && !this.keepAlive) {
if (shouldCompute(this)) {
this.warnAboutUntrackedRead();
startBatch(); // See perf test 'computed memoization'
this.value = this.computeValue(false);
endBatch();
}
}
else {
reportObserved(this);
if (shouldCompute(this))
if (this.trackAndCompute())
propagateChangeConfirmed(this);
}
const result = this.value;
if (isCaughtException(result))
throw result.cause;
return result;
}
peek() {
const res = this.computeValue(false);
if (isCaughtException(res))
throw res.cause;
return res;
}
set(value) {
if (this.setter) {
invariant(!this.isRunningSetter, `The setter of computed value '${this.name}' is trying to update itself. Did you intend to update an _observable_ value, instead of the computed property?`);
this.isRunningSetter = true;
try {
this.setter.call(this.scope, value);
}
finally {
this.isRunningSetter = false;
}
}
else
invariant(false, process.env.NODE_ENV !== "production" &&
`[ComputedValue '${this.name}'] It is not possible to assign a new value to a computed value.`);
}
trackAndCompute() {
if (isSpyEnabled() && process.env.NODE_ENV !== "production") {
spyReport({
object: this.scope,
type: "compute",
name: this.name
});
}
const oldValue = this.value;
const wasSuspended =
/* see #1208 */ this.dependenciesState === IDerivationState.NOT_TRACKING;
const newValue = this.computeValue(true);
const changed = wasSuspended ||
isCaughtException(oldValue) ||
isCaughtException(newValue) ||
!this.equals(oldValue, newValue);
if (changed) {
this.value = newValue;
}
return changed;
}
computeValue(track) {
this.isComputing = true;
globalState.computationDepth++;
let res;
if (track) {
res = trackDerivedFunction(this, this.derivation, this.scope);
}
else {
if (globalState.disableErrorBoundaries === true) {
res = this.derivation.call(this.scope);
}
else {
try {
res = this.derivation.call(this.scope);
}
catch (e) {
res = new CaughtException(e);
}
}
}
globalState.computationDepth--;
this.isComputing = false;
return res;
}
suspend() {
if (!this.keepAlive) {
clearObserving(this);
this.value = undefined; // don't hold on to computed value!
}
}
observe(listener, fireImmediately) {
let firstTime = true;
let prevValue = undefined;
return autorun(() => {
let newValue = this.get();
if (!firstTime || fireImmediately) {
const prevU = untrackedStart();
listener({
type: "update",
object: this,
newValue,
oldValue: prevValue
});
untrackedEnd(prevU);
}
firstTime = false;
prevValue = newValue;
});
}
warnAboutUntrackedRead() {
if (process.env.NODE_ENV === "production")
return;
if (this.requiresReaction === true) {
fail(`[mobx] Computed value ${this.name} is read outside a reactive context`);
}
if (this.isTracing !== TraceMode.NONE) {
console.log(`[mobx.trace] '${this.name}' is being read outside a reactive context. Doing a full recompute`);
}
if (globalState.computedRequiresReaction) {
console.warn(`[mobx] Computed value ${this.name} is being read outside a reactive context. Doing a full recompute`);
}
}
toJSON() {
return this.get();
}
toString() {
return `${this.name}[${this.derivation.toString()}]`;
}
valueOf() {
return toPrimitive(this.get());
}
[Symbol.toPrimitive]() {
return this.valueOf();
}
}
const isComputedValue = createInstanceofPredicate("ComputedValue", ComputedValue);
var IDerivationState;
(function (IDerivationState) {
// before being run or (outside batch and not being observed)
// at this point derivation is not holding any data about dependency tree
IDerivationState[IDerivationState["NOT_TRACKING"] = -1] = "NOT_TRACKING";
// no shallow dependency changed since last computation
// won't recalculate derivation
// this is what makes mobx fast
IDerivationState[IDerivationState["UP_TO_DATE"] = 0] = "UP_TO_DATE";
// some deep dependency changed, but don't know if shallow dependency changed
// will require to check first if UP_TO_DATE or POSSIBLY_STALE
// currently only ComputedValue will propagate POSSIBLY_STALE
//
// having this state is second big optimization:
// don't have to recompute on every dependency change, but only when it's needed
IDerivationState[IDerivationState["POSSIBLY_STALE"] = 1] = "POSSIBLY_STALE";
// A shallow dependency has changed since last computation and the derivation
// will need to recompute when it's needed next.
IDerivationState[IDerivationState["STALE"] = 2] = "STALE";
})(IDerivationState || (IDerivationState = {}));
var TraceMode;
(function (TraceMode) {
TraceMode[TraceMode["NONE"] = 0] = "NONE";
TraceMode[TraceMode["LOG"] = 1] = "LOG";
TraceMode[TraceMode["BREAK"] = 2] = "BREAK";
})(TraceMode || (TraceMode = {}));
class CaughtException {
constructor(cause) {
this.cause = cause;
// Empty
}
}
function isCaughtException(e) {
return e instanceof CaughtException;
}
/**
* Finds out whether any dependency of the derivation has actually changed.
* If dependenciesState is 1 then it will recalculate dependencies,
* if any dependency changed it will propagate it by changing dependenciesState to 2.
*
* By iterating over the dependencies in the same order that they were reported and
* stopping on the first change, all the recalculations are only called for ComputedValues
* that will be tracked by derivation. That is because we assume that if the first x
* dependencies of the derivation doesn't change then the derivation should run the same way
* up until accessing x-th dependency.
*/
function shouldCompute(derivation) {
switch (derivation.dependenciesState) {
case IDerivationState.UP_TO_DATE:
return false;
case IDerivationState.NOT_TRACKING:
case IDerivationState.STALE:
return true;
case IDerivationState.POSSIBLY_STALE: {
const prevUntracked = untrackedStart(); // no need for those computeds to be reported, they will be picked up in trackDerivedFunction.
const obs = derivation.observing, l = obs.length;
for (let i = 0; i < l; i++) {
const obj = obs[i];
if (isComputedValue(obj)) {
if (globalState.disableErrorBoundaries) {
obj.get();
}
else {
try {
obj.get();
}
catch (e) {
// we are not interested in the value *or* exception at this moment, but if there is one, notify all
untrackedEnd(prevUntracked);
return true;
}
}
// if ComputedValue `obj` actually changed it will be computed and propagated to its observers.
// and `derivation` is an observer of `obj`
// invariantShouldCompute(derivation)
if (derivation.dependenciesState === IDerivationState.STALE) {
untrackedEnd(prevUntracked);
return true;
}
}
}
changeDependenciesStateTo0(derivation);
untrackedEnd(prevUntracked);
return false;
}
}
}
// function invariantShouldCompute(derivation: IDerivation) {
// const newDepState = (derivation as any).dependenciesState
// if (
// process.env.NODE_ENV === "production" &&
// (newDepState === IDerivationState.POSSIBLY_STALE ||
// newDepState === IDerivationState.NOT_TRACKING)
// )
// fail("Illegal dependency state")
// }
function isComputingDerivation() {
return globalState.trackingDerivation !== null; // filter out actions inside computations
}
function checkIfStateModificationsAreAllowed(atom) {
const hasObservers = atom.observers.size > 0;
// Should never be possible to change an observed observable from inside computed, see #798
if (globalState.computationDepth > 0 && hasObservers)
fail(process.env.NODE_ENV !== "production" &&
`Computed values are not allowed to cause side effects by changing observables that are already being observed. Tried to modify: ${atom.name}`);
// Should not be possible to change observed state outside strict mode, except during initialization, see #563
if (!globalState.allowStateChanges && (hasObservers || globalState.enforceActions === "strict"))
fail(process.env.NODE_ENV !== "production" &&
(globalState.enforceActions
? "Since strict-mode is enabled, changing observed observable values outside actions is not allowed. Please wrap the code in an `action` if this change is intended. Tried to modify: "
: "Side effects like changing state are not allowed at this point. Are you trying to modify state from, for example, the render function of a React component? Tried to modify: ") +
atom.name);
}
/**
* Executes the provided function `f` and tracks which observables are being accessed.
* The tracking information is stored on the `derivation` object and the derivation is registered
* as observer of any of the accessed observables.
*/
function trackDerivedFunction(derivation, f, context) {
// pre allocate array allocation + room for variation in deps
// array will be trimmed by bindDependencies
changeDependenciesStateTo0(derivation);
derivation.newObserving = new Array(derivation.observing.length + 100);
derivation.unboundDepsCount = 0;
derivation.runId = ++globalState.runId;
const prevTracking = globalState.trackingDerivation;
globalState.trackingDerivation = derivation;
let result;
if (globalState.disableErrorBoundaries === true) {
result = f.call(context);
}
else {
try {
result = f.call(context);
}
catch (e) {
result = new CaughtException(e);
}
}
globalState.trackingDerivation = prevTracking;
bindDependencies(derivation);
return result;
}
/**
* diffs newObserving with observing.
* update observing to be newObserving with unique observables
* notify observers that become observed/unobserved
*/
function bindDependencies(derivation) {
// invariant(derivation.dependenciesState !== IDerivationState.NOT_TRACKING, "INTERNAL ERROR bindDependencies expects derivation.dependenciesState !== -1");
const prevObserving = derivation.observing;
const observing = (derivation.observing = derivation.newObserving);
let lowestNewObservingDerivationState = IDerivationState.UP_TO_DATE;
// Go through all new observables and check diffValue: (this list can contain duplicates):
// 0: first occurrence, change to 1 and keep it
// 1: extra occurrence, drop it
let i0 = 0, l = derivation.unboundDepsCount;
for (let i = 0; i < l; i++) {
const dep = observing[i];
if (dep.diffValue === 0) {
dep.diffValue = 1;
if (i0 !== i)
observing[i0] = dep;
i0++;
}
// Upcast is 'safe' here, because if dep is IObservable, `dependenciesState` will be undefined,
// not hitting the condition
if (dep.dependenciesState > lowestNewObservingDerivationState) {
lowestNewObservingDerivationState = dep.dependenciesState;
}
}
observing.length = i0;
derivation.newObserving = null; // newObserving shouldn't be needed outside tracking (statement moved down to work around FF bug, see #614)
// Go through all old observables and check diffValue: (it is unique after last bindDependencies)
// 0: it's not in new observables, unobserve it
// 1: it keeps being observed, don't want to notify it. change to 0
l = prevObserving.length;
while (l--) {
const dep = prevObserving[l];
if (dep.diffValue === 0) {
removeObserver(dep, derivation);
}
dep.diffValue = 0;
}
// Go through all new observables and check diffValue: (now it should be unique)
// 0: it was set to 0 in last loop. don't need to do anything.
// 1: it wasn't observed, let's observe it. set back to 0
while (i0--) {
const dep = observing[i0];
if (dep.diffValue === 1) {
dep.diffValue = 0;
addObserver(dep, derivation);
}
}
// Some new observed derivations may become stale during this derivation computation
// so they have had no chance to propagate staleness (#916)
if (lowestNewObservingDerivationState !== IDerivationState.UP_TO_DATE) {
derivation.dependenciesState = lowestNewObservingDerivationState;
derivation.onBecomeStale();
}
}
function clearObserving(derivation) {
// invariant(globalState.inBatch > 0, "INTERNAL ERROR clearObserving should be called only inside batch");
const obs = derivation.observing;
derivation.observing = [];
let i = obs.length;
while (i--)
removeObserver(obs[i], derivation);
derivation.dependenciesState = IDerivationState.NOT_TRACKING;
}
function untracked(action) {
const prev = untrackedStart();
try {
return action();
}
finally {
untrackedEnd(prev);
}
}
function untrackedStart() {
const prev = globalState.trackingDerivation;
globalState.trackingDerivation = null;
return prev;
}
function untrackedEnd(prev) {
globalState.trackingDerivation = prev;
}
/**
* needed to keep `lowestObserverState` correct. when changing from (2 or 1) to 0
*
*/
function changeDependenciesStateTo0(derivation) {
if (derivation.dependenciesState === IDerivationState.UP_TO_DATE)
return;
derivation.dependenciesState = IDerivationState.UP_TO_DATE;
const obs = derivation.observing;
let i = obs.length;
while (i--)
obs[i].lowestObserverState = IDerivationState.UP_TO_DATE;
}
/**
* These values will persist if global state is reset
*/
const persistentKeys = [
"mobxGuid",
"spyListeners",
"enforceActions",
"computedRequiresReaction",
"disableErrorBoundaries",
"runId",
"UNCHANGED"
];
class MobXGlobals {
constructor() {
/**
* MobXGlobals version.
* MobX compatiblity with other versions loaded in memory as long as this version matches.
* It indicates that the global state still stores similar information
*
* N.B: this version is unrelated to the package version of MobX, and is only the version of the
* internal state storage of MobX, and can be the same across many different package versions
*/
this.version = 5;
/**
* globally unique token to signal unchanged
*/
this.UNCHANGED = {};
/**
* Currently running derivation
*/
this.trackingDerivation = null;
/**
* Are we running a computation currently? (not a reaction)
*/
this.computationDepth = 0;
/**
* Each time a derivation is tracked, it is assigned a unique run-id
*/
this.runId = 0;
/**
* 'guid' for general purpose. Will be persisted amongst resets.
*/
this.mobxGuid = 0;
/**
* Are we in a batch block? (and how many of them)
*/
this.inBatch = 0;
/**
* Observables that don't have observers anymore, and are about to be
* suspended, unless somebody else accesses it in the same batch
*
* @type {IObservable[]}
*/
this.pendingUnobservations = [];
/**
* List of scheduled, not yet executed, reactions.
*/
this.pendingReactions = [];
/**
* Are we currently processing reactions?
*/
this.isRunningReactions = false;
/**
* Is it allowed to change observables at this point?
* In general, MobX doesn't allow that when running computations and React.render.
* To ensure that those functions stay pure.
*/
this.allowStateChanges = true;
/**
* If strict mode is enabled, state changes are by default not allowed
*/
this.enforceActions = false;
/**
* Spy callbacks
*/
this.spyListeners = [];
/**
* Globally attached error handlers that react specifically to errors in reactions
*/
this.globalReactionErrorHandlers = [];
/**
* Warn if computed values are accessed outside a reactive context
*/
this.computedRequiresReaction = false;
/**
* Allows overwriting of computed properties, useful in tests but not prod as it can cause
* memory leaks. See https://github.com/mobxjs/mobx/issues/1867
*/
this.computedConfigurable = false;
/*
* Don't catch and rethrow exceptions. This is useful for inspecting the state of
* the stack when an exception occurs while debugging.
*/
this.disableErrorBoundaries = false;
/*
* If true, we are already handling an exception in an action. Any errors in reactions should be supressed, as
* they are not the cause, see: https://github.com/mobxjs/mobx/issues/1836
*/
this.suppressReactionErrors = false;
}
}
let canMergeGlobalState = true;
let isolateCalled = false;
let globalState = (function () {
const global = getGlobal();
if (global.__mobxInstanceCount > 0 && !global.__mobxGlobals)
canMergeGlobalState = false;
if (global.__mobxGlobals && global.__mobxGlobals.version !== new MobXGlobals().version)
canMergeGlobalState = false;
if (!canMergeGlobalState) {
setTimeout(() => {
if (!isolateCalled) {
fail("There are multiple, different versions of MobX active. Make sure MobX is loaded only once or use `configure({ isolateGlobalState: true })`");
}
}, 1);
return new MobXGlobals();
}
else if (global.__mobxGlobals) {
global.__mobxInstanceCount += 1;
if (!global.__mobxGlobals.UNCHANGED)
global.__mobxGlobals.UNCHANGED = {}; // make merge backward compatible
return global.__mobxGlobals;
}
else {
global.__mobxInstanceCount = 1;
return (global.__mobxGlobals = new MobXGlobals());
}
})();
function isolateGlobalState() {
if (globalState.pendingReactions.length ||
globalState.inBatch ||
globalState.isRunningReactions)
fail("isolateGlobalState should be called before MobX is running any reactions");
isolateCalled = true;
if (canMergeGlobalState) {
if (--getGlobal().__mobxInstanceCount === 0)
getGlobal().__mobxGlobals = undefined;
globalState = new MobXGlobals();
}
}
function getGlobalState() {
return globalState;
}
/**
* For testing purposes only; this will break the internal state of existing observables,
* but can be used to get back at a stabl