mobx 7.0.0 → 7.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -265,7 +265,8 @@
265
265
  constructor(name_ = "Atom@" + getNextId() ) {
266
266
  this.name_ = void 0;
267
267
  this.flags_ = 0b000;
268
- this.observers_ = new Set();
268
+ // Allocated lazily on first observer to save memory.
269
+ this.observers_ = null;
269
270
  this.lastAccessedBy_ = 0;
270
271
  this.lowestObserverState_ = -1 /* IDerivationState_.NOT_TRACKING_ */;
271
272
  // onBecomeObservedListeners
@@ -347,6 +348,10 @@
347
348
  const compareDefault = Object.is;
348
349
 
349
350
  function deepEnhancer(v, _, name) {
351
+ // primitives can never be made observable; skip the type checks below
352
+ if (v === null || typeof v !== "object" && typeof v !== "function") {
353
+ return v;
354
+ }
350
355
  // it is an observable already, done
351
356
  if (isObservable(v)) {
352
357
  return v;
@@ -1287,7 +1292,8 @@
1287
1292
  // nodes we are looking at. Our value depends on these nodes
1288
1293
  this.newObserving_ = null;
1289
1294
  // during tracking it's an array with new observed observers
1290
- this.observers_ = new Set();
1295
+ // Lazily allocated on first observer - see Atom.observers_.
1296
+ this.observers_ = null;
1291
1297
  this.runId_ = 0;
1292
1298
  this.lastAccessedBy_ = 0;
1293
1299
  this.lowestObserverState_ = 0 /* IDerivationState_.UP_TO_DATE_ */;
@@ -1370,9 +1376,9 @@
1370
1376
  if (this.isComputing) {
1371
1377
  die(32, this.name_, this.derivation);
1372
1378
  }
1373
- if (globalState.inBatch === 0 &&
1379
+ if (globalState.inBatch === 0 && (
1374
1380
  // !globalState.trackingDerivatpion &&
1375
- this.observers_.size === 0 && !this.keepAlive_) {
1381
+ !this.observers_ || this.observers_.size === 0) && !this.keepAlive_) {
1376
1382
  if (shouldCompute(this)) {
1377
1383
  this.warnAboutUntrackedRead_();
1378
1384
  startBatch(); // See perf test 'computed memoization'
@@ -1380,6 +1386,7 @@
1380
1386
  endBatch();
1381
1387
  }
1382
1388
  } else {
1389
+ const wasBeingObserved = this.isBeingObserved;
1383
1390
  reportObserved(this);
1384
1391
  if (shouldCompute(this)) {
1385
1392
  let prevTrackingContext = globalState.trackingContext;
@@ -1390,6 +1397,10 @@
1390
1397
  propagateChangeConfirmed(this);
1391
1398
  }
1392
1399
  globalState.trackingContext = prevTrackingContext;
1400
+ } else if (!wasBeingObserved && this.isBeingObserved) {
1401
+ // We just became observed while serving a cached value, so the getter
1402
+ // won't run and won't re-report our dependencies. Cascade to them. #4547
1403
+ this.observing_.forEach(markObserved);
1393
1404
  }
1394
1405
  }
1395
1406
  const result = this.value_;
@@ -1558,7 +1569,7 @@
1558
1569
  return globalState.trackingDerivation !== null; // filter out actions inside computations
1559
1570
  }
1560
1571
  function checkIfStateModificationsAreAllowed(atom) {
1561
- const hasObservers = atom.observers_.size > 0;
1572
+ const hasObservers = !!atom.observers_ && atom.observers_.size > 0;
1562
1573
  // Should not be possible to change observed state outside strict mode, except during initialization, see #563
1563
1574
  if (!globalState.allowStateChanges && (hasObservers || globalState.enforceActions === "always")) {
1564
1575
  console.warn("[MobX] " + (globalState.enforceActions ? "Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed. Tried to modify: " : "Side effects like changing state are not allowed at this point. Are you trying to modify state from, for example, a computed value or the render function of a React component? You can wrap side effects in 'runInAction' (or decorate functions with 'action') if needed. Tried to modify: ") + atom.name_);
@@ -1781,6 +1792,13 @@
1781
1792
  * Are we currently processing reactions?
1782
1793
  */
1783
1794
  this.isRunningReactions = false;
1795
+ /**
1796
+ * Are we currently draining pendingUnobservations in endBatch?
1797
+ * An onBecomeUnobserved handler can dispose a Reaction, which calls
1798
+ * startBatch/endBatch again; this guards against re-entering the same
1799
+ * drain loop recursively (see endBatch in observable.ts).
1800
+ */
1801
+ this.isRunningUnobservations = false;
1784
1802
  /**
1785
1803
  * Is it allowed to change observables at this point?
1786
1804
  * In general, MobX doesn't allow that when running computations and React.render.
@@ -1897,10 +1915,11 @@
1897
1915
  }
1898
1916
 
1899
1917
  function hasObservers(observable) {
1900
- return observable.observers_ && observable.observers_.size > 0;
1918
+ return !!observable.observers_ && observable.observers_.size > 0;
1901
1919
  }
1902
1920
  function getObservers(observable) {
1903
- return observable.observers_;
1921
+ var _observable$observers;
1922
+ return (_observable$observers = observable.observers_) != null ? _observable$observers : new Set();
1904
1923
  }
1905
1924
  // function invariantObservers(observable: IObservable) {
1906
1925
  // const list = observable.observers
@@ -1920,10 +1939,8 @@
1920
1939
  // )
1921
1940
  // }
1922
1941
  function addObserver(observable, node) {
1923
- // invariant(node.dependenciesState !== -1, "INTERNAL ERROR, can add only dependenciesState !== -1");
1924
- // invariant(observable._observers.indexOf(node) === -1, "INTERNAL ERROR add already added node");
1925
- // invariantObservers(observable);
1926
- observable.observers_.add(node);
1942
+ var _observable$observers2;
1943
+ ((_observable$observers2 = observable.observers_) != null ? _observable$observers2 : observable.observers_ = new Set()).add(node);
1927
1944
  if (observable.lowestObserverState_ > node.dependenciesState_) {
1928
1945
  observable.lowestObserverState_ = node.dependenciesState_;
1929
1946
  }
@@ -1934,8 +1951,12 @@
1934
1951
  // invariant(globalState.inBatch > 0, "INTERNAL ERROR, remove should be called only inside batch");
1935
1952
  // invariant(observable._observers.indexOf(node) !== -1, "INTERNAL ERROR remove already removed node");
1936
1953
  // invariantObservers(observable);
1937
- observable.observers_.delete(node);
1938
- if (observable.observers_.size === 0) {
1954
+ const observers = observable.observers_;
1955
+ if (!observers) {
1956
+ return;
1957
+ }
1958
+ observers.delete(node);
1959
+ if (observers.size === 0) {
1939
1960
  // deleting last observer
1940
1961
  queueForUnobservation(observable);
1941
1962
  }
@@ -1961,26 +1982,59 @@
1961
1982
  if (--globalState.inBatch === 0) {
1962
1983
  runReactions();
1963
1984
  // the batch is actually about to finish, all unobserving should happen here.
1964
- const list = globalState.pendingUnobservations;
1965
- for (let i = 0; i < list.length; i++) {
1966
- const observable = list[i];
1967
- observable.isPendingUnobservation = false;
1968
- if (observable.observers_.size === 0) {
1969
- if (observable.isBeingObserved) {
1970
- // if this observable had reactive observers, trigger the hooks
1971
- observable.isBeingObserved = false;
1972
- observable.onBUO();
1973
- }
1974
- if (observable instanceof ComputedValue) {
1975
- // computed values are automatically teared down when the last observer leaves
1976
- // this process happens recursively, this computed might be the last observabe of another, etc..
1977
- observable.suspend_();
1985
+ // Guard against re-entering this loop: an onBUO handler can dispose a Reaction,
1986
+ // which calls startBatch/endBatch again while we're still iterating. Bail out of
1987
+ // the nested call instead of recursing; the outer loop re-reads list.length on
1988
+ // every iteration, so it picks up anything the nested dispose() pushes onto the
1989
+ // same pendingUnobservations array.
1990
+ if (!globalState.isRunningUnobservations) {
1991
+ globalState.isRunningUnobservations = true;
1992
+ try {
1993
+ const list = globalState.pendingUnobservations;
1994
+ for (let i = 0; i < list.length; i++) {
1995
+ const observable = list[i];
1996
+ observable.isPendingUnobservation = false;
1997
+ if (!observable.observers_ || observable.observers_.size === 0) {
1998
+ if (observable.isBeingObserved) {
1999
+ // if this observable had reactive observers, trigger the hooks
2000
+ observable.isBeingObserved = false;
2001
+ observable.onBUO();
2002
+ }
2003
+ if (observable instanceof ComputedValue) {
2004
+ // computed values are automatically teared down when the last observer leaves
2005
+ // this process happens recursively, this computed might be the last observabe of another, etc..
2006
+ observable.suspend_();
2007
+ }
2008
+ }
1978
2009
  }
2010
+ globalState.pendingUnobservations = [];
2011
+ } finally {
2012
+ // Always release the guard, even if an onBUO handler (user code) threw,
2013
+ // otherwise every future endBatch() would see isRunningUnobservations
2014
+ // stuck true and silently stop draining pendingUnobservations forever.
2015
+ globalState.isRunningUnobservations = false;
1979
2016
  }
1980
2017
  }
1981
- globalState.pendingUnobservations = [];
1982
2018
  }
1983
2019
  }
2020
+ /**
2021
+ * Marks an observable as observed, cascading into the dependencies of a ComputedValue.
2022
+ * Unobservation already cascades (`suspend_` -> `clearObserving`), observation normally
2023
+ * only does so by accident: a newly observed computed usually recomputes and re-reports
2024
+ * its dependencies. When it serves a cached value instead nothing re-reports them, so the
2025
+ * transition has to be propagated by hand. See #4547.
2026
+ */
2027
+ function markObserved(observable) {
2028
+ var _observable$observing;
2029
+ if (observable.isBeingObserved) {
2030
+ return;
2031
+ }
2032
+ observable.isBeingObserved = true;
2033
+ observable.onBO();
2034
+ // No queueForUnobservation here: the observer links already exist, so the regular
2035
+ // suspend_ -> clearObserving -> removeObserver teardown still delivers the onBUO.
2036
+ (_observable$observing = observable.observing_) == null || _observable$observing.forEach(markObserved);
2037
+ }
1984
2038
  function reportObserved(observable) {
1985
2039
  checkIfStateReadsAreAllowed(observable);
1986
2040
  const derivation = globalState.trackingDerivation;
@@ -2000,7 +2054,7 @@
2000
2054
  }
2001
2055
  }
2002
2056
  return observable.isBeingObserved;
2003
- } else if (observable.observers_.size === 0 && globalState.inBatch > 0) {
2057
+ } else if ((!observable.observers_ || observable.observers_.size === 0) && globalState.inBatch > 0) {
2004
2058
  queueForUnobservation(observable);
2005
2059
  }
2006
2060
  return false;
@@ -2027,13 +2081,14 @@
2027
2081
  */
2028
2082
  // Called by Atom when its value changes
2029
2083
  function propagateChanged(observable) {
2084
+ var _observable$observers3;
2030
2085
  // invariantLOS(observable, "changed start");
2031
2086
  if (observable.lowestObserverState_ === 2 /* IDerivationState_.STALE_ */) {
2032
2087
  return;
2033
2088
  }
2034
2089
  observable.lowestObserverState_ = 2 /* IDerivationState_.STALE_ */;
2035
2090
  // Ideally we use for..of here, but the downcompiled version is really slow...
2036
- observable.observers_.forEach(d => {
2091
+ (_observable$observers3 = observable.observers_) == null || _observable$observers3.forEach(d => {
2037
2092
  if (d.dependenciesState_ === 0 /* IDerivationState_.UP_TO_DATE_ */) {
2038
2093
  d.onBecomeStale_();
2039
2094
  }
@@ -2043,12 +2098,13 @@
2043
2098
  }
2044
2099
  // Called by ComputedValue when it recalculate and its value changed
2045
2100
  function propagateChangeConfirmed(observable) {
2101
+ var _observable$observers4;
2046
2102
  // invariantLOS(observable, "confirmed start");
2047
2103
  if (observable.lowestObserverState_ === 2 /* IDerivationState_.STALE_ */) {
2048
2104
  return;
2049
2105
  }
2050
2106
  observable.lowestObserverState_ = 2 /* IDerivationState_.STALE_ */;
2051
- observable.observers_.forEach(d => {
2107
+ (_observable$observers4 = observable.observers_) == null || _observable$observers4.forEach(d => {
2052
2108
  if (d.dependenciesState_ === 1 /* IDerivationState_.POSSIBLY_STALE_ */) {
2053
2109
  d.dependenciesState_ = 2 /* IDerivationState_.STALE_ */;
2054
2110
  } else if (d.dependenciesState_ === 0 /* IDerivationState_.UP_TO_DATE_ */ // this happens during computing of `d`, just keep lowestObserverState up to date.
@@ -2060,12 +2116,13 @@
2060
2116
  }
2061
2117
  // Used by computed when its dependency changed, but we don't wan't to immediately recompute.
2062
2118
  function propagateMaybeChanged(observable) {
2119
+ var _observable$observers5;
2063
2120
  // invariantLOS(observable, "maybe start");
2064
2121
  if (observable.lowestObserverState_ !== 0 /* IDerivationState_.UP_TO_DATE_ */) {
2065
2122
  return;
2066
2123
  }
2067
2124
  observable.lowestObserverState_ = 1 /* IDerivationState_.POSSIBLY_STALE_ */;
2068
- observable.observers_.forEach(d => {
2125
+ (_observable$observers5 = observable.observers_) == null || _observable$observers5.forEach(d => {
2069
2126
  if (d.dependenciesState_ === 0 /* IDerivationState_.UP_TO_DATE_ */) {
2070
2127
  d.dependenciesState_ = 1 /* IDerivationState_.POSSIBLY_STALE_ */;
2071
2128
  d.onBecomeStale_();
@@ -4320,31 +4377,16 @@
4320
4377
  });
4321
4378
  }
4322
4379
  intersection(otherSet) {
4323
- if (isES6Set(otherSet) && !isObservableSet(otherSet)) {
4324
- return otherSet.intersection(this);
4325
- } else {
4326
- const dehancedSet = new Set(this);
4327
- return dehancedSet.intersection(otherSet);
4328
- }
4380
+ return new Set(this).intersection(otherSet);
4329
4381
  }
4330
4382
  union(otherSet) {
4331
- if (isES6Set(otherSet) && !isObservableSet(otherSet)) {
4332
- return otherSet.union(this);
4333
- } else {
4334
- const dehancedSet = new Set(this);
4335
- return dehancedSet.union(otherSet);
4336
- }
4383
+ return new Set(this).union(otherSet);
4337
4384
  }
4338
4385
  difference(otherSet) {
4339
4386
  return new Set(this).difference(otherSet);
4340
4387
  }
4341
4388
  symmetricDifference(otherSet) {
4342
- if (isES6Set(otherSet) && !isObservableSet(otherSet)) {
4343
- return otherSet.symmetricDifference(this);
4344
- } else {
4345
- const dehancedSet = new Set(this);
4346
- return dehancedSet.symmetricDifference(otherSet);
4347
- }
4389
+ return new Set(this).symmetricDifference(otherSet);
4348
4390
  }
4349
4391
  isSubsetOf(otherSet) {
4350
4392
  return new Set(this).isSubsetOf(otherSet);
@@ -4353,28 +4395,46 @@
4353
4395
  return new Set(this).isSupersetOf(otherSet);
4354
4396
  }
4355
4397
  isDisjointFrom(otherSet) {
4356
- if (isES6Set(otherSet) && !isObservableSet(otherSet)) {
4357
- return otherSet.isDisjointFrom(this);
4358
- } else {
4359
- const dehancedSet = new Set(this);
4360
- return dehancedSet.isDisjointFrom(otherSet);
4361
- }
4398
+ return new Set(this).isDisjointFrom(otherSet);
4362
4399
  }
4363
4400
  replace(other) {
4364
4401
  if (isObservableSet(other)) {
4365
4402
  other = new Set(other);
4366
4403
  }
4367
- transaction(() => {
4368
- if (Array.isArray(other)) {
4369
- this.clear();
4370
- other.forEach(value => this.add(value));
4371
- } else if (isES6Set(other)) {
4372
- this.clear();
4373
- other.forEach(value => this.add(value));
4374
- } else if (other !== null && other !== undefined) {
4375
- die(41, other);
4376
- }
4377
- });
4404
+ if (Array.isArray(other) || isES6Set(other)) {
4405
+ // Only emit `delete`/`add` events (and `reportChanged`) for values that
4406
+ // actually change, instead of clearing and re-adding everything. `add` and
4407
+ // `delete` are already no-ops for values that are respectively already
4408
+ // present or already absent, so we just need to avoid deleting values that
4409
+ // are part of the replacement. See #3761.
4410
+ transaction(() => {
4411
+ // Collect the desired values for quick lookup. `other` is already a Set
4412
+ // here when it was passed (or snapshotted from an observable set) as one,
4413
+ // so reuse it rather than allocating another; arrays are wrapped (which
4414
+ // also dedupes them).
4415
+ const replacementValues = isES6Set(other) ? other : new Set(other);
4416
+ // Short-circuit the trivial cases: an empty replacement is just a clear,
4417
+ // and replacing into an empty set only needs the adds.
4418
+ if (replacementValues.size === 0) {
4419
+ this.clear();
4420
+ return;
4421
+ }
4422
+ if (this.data_.size === 0) {
4423
+ replacementValues.forEach(value => this.add(value));
4424
+ return;
4425
+ }
4426
+ // Delete values that are not part of the replacement.
4427
+ for (const value of this.data_.values()) {
4428
+ if (!replacementValues.has(this.dehanceValue_(value))) {
4429
+ this.delete(value);
4430
+ }
4431
+ }
4432
+ // Add new values; values that are already present are a no-op.
4433
+ replacementValues.forEach(value => this.add(value));
4434
+ });
4435
+ } else if (other !== null && other !== undefined) {
4436
+ die(41, other);
4437
+ }
4378
4438
  return this;
4379
4439
  }
4380
4440
  toJSON() {