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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobx",
3
- "version": "7.0.0",
3
+ "version": "7.0.2",
4
4
  "description": "Simple, scalable state management.",
5
5
  "source": "src/mobx.ts",
6
6
  "type": "commonjs",
package/src/core/atom.ts CHANGED
@@ -30,7 +30,8 @@ const enum AtomFlags {
30
30
  export class Atom implements IAtom {
31
31
  private flags_ = 0b000
32
32
 
33
- observers_ = new Set<IDerivation>()
33
+ // Allocated lazily on first observer to save memory.
34
+ observers_: Set<IDerivation> | null = null
34
35
 
35
36
  lastAccessedBy_ = 0
36
37
  lowestObserverState_ = IDerivationState_.NOT_TRACKING_
@@ -14,6 +14,7 @@ import {
14
14
  globalState,
15
15
  isCaughtException,
16
16
  isSpyEnabled,
17
+ markObserved,
17
18
  propagateChangeConfirmed,
18
19
  propagateMaybeChanged,
19
20
  reportObserved,
@@ -84,7 +85,8 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
84
85
  dependenciesState_ = IDerivationState_.NOT_TRACKING_
85
86
  observing_: IObservable[] = [] // nodes we are looking at. Our value depends on these nodes
86
87
  newObserving_ = null // during tracking it's an array with new observed observers
87
- observers_ = new Set<IDerivation>()
88
+ // Lazily allocated on first observer - see Atom.observers_.
89
+ observers_: Set<IDerivation> | null = null
88
90
  runId_ = 0
89
91
  lastAccessedBy_ = 0
90
92
  lowestObserverState_ = IDerivationState_.UP_TO_DATE_
@@ -200,7 +202,7 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
200
202
  if (
201
203
  globalState.inBatch === 0 &&
202
204
  // !globalState.trackingDerivatpion &&
203
- this.observers_.size === 0 &&
205
+ (!this.observers_ || this.observers_.size === 0) &&
204
206
  !this.keepAlive_
205
207
  ) {
206
208
  if (shouldCompute(this)) {
@@ -210,6 +212,7 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
210
212
  endBatch()
211
213
  }
212
214
  } else {
215
+ const wasBeingObserved = this.isBeingObserved
213
216
  reportObserved(this)
214
217
  if (shouldCompute(this)) {
215
218
  let prevTrackingContext = globalState.trackingContext
@@ -220,6 +223,10 @@ export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDeriva
220
223
  propagateChangeConfirmed(this)
221
224
  }
222
225
  globalState.trackingContext = prevTrackingContext
226
+ } else if (!wasBeingObserved && this.isBeingObserved) {
227
+ // We just became observed while serving a cached value, so the getter
228
+ // won't run and won't re-report our dependencies. Cascade to them. #4547
229
+ this.observing_.forEach(markObserved)
223
230
  }
224
231
  }
225
232
  const result = this.value_!
@@ -134,7 +134,7 @@ export function checkIfStateModificationsAreAllowed(atom: IAtom) {
134
134
  if (!__DEV__) {
135
135
  return
136
136
  }
137
- const hasObservers = atom.observers_.size > 0
137
+ const hasObservers = !!atom.observers_ && atom.observers_.size > 0
138
138
  // Should not be possible to change observed state outside strict mode, except during initialization, see #563
139
139
  if (
140
140
  !globalState.allowStateChanges &&
@@ -82,6 +82,14 @@ export class MobXGlobals {
82
82
  */
83
83
  isRunningReactions = false
84
84
 
85
+ /**
86
+ * Are we currently draining pendingUnobservations in endBatch?
87
+ * An onBecomeUnobserved handler can dispose a Reaction, which calls
88
+ * startBatch/endBatch again; this guards against re-entering the same
89
+ * drain loop recursively (see endBatch in observable.ts).
90
+ */
91
+ isRunningUnobservations = false
92
+
85
93
  /**
86
94
  * Is it allowed to change observables at this point?
87
95
  * In general, MobX doesn't allow that when running computations and React.render.
@@ -26,7 +26,7 @@ export interface IObservable extends IDepTreeNode {
26
26
  lowestObserverState_: IDerivationState_ // Used to avoid redundant propagations
27
27
  isPendingUnobservation: boolean // Used to push itself to global.pendingUnobservations at most once per batch.
28
28
 
29
- observers_: Set<IDerivation>
29
+ observers_: Set<IDerivation> | null
30
30
 
31
31
  onBUO(): void
32
32
  onBO(): void
@@ -36,11 +36,11 @@ export interface IObservable extends IDepTreeNode {
36
36
  }
37
37
 
38
38
  export function hasObservers(observable: IObservable): boolean {
39
- return observable.observers_ && observable.observers_.size > 0
39
+ return !!observable.observers_ && observable.observers_.size > 0
40
40
  }
41
41
 
42
42
  export function getObservers(observable: IObservable): Set<IDerivation> {
43
- return observable.observers_
43
+ return observable.observers_ ?? new Set()
44
44
  }
45
45
 
46
46
  // function invariantObservers(observable: IObservable) {
@@ -65,7 +65,7 @@ export function addObserver(observable: IObservable, node: IDerivation) {
65
65
  // invariant(observable._observers.indexOf(node) === -1, "INTERNAL ERROR add already added node");
66
66
  // invariantObservers(observable);
67
67
 
68
- observable.observers_.add(node)
68
+ ;(observable.observers_ ??= new Set()).add(node)
69
69
  if (observable.lowestObserverState_ > node.dependenciesState_) {
70
70
  observable.lowestObserverState_ = node.dependenciesState_
71
71
  }
@@ -78,8 +78,12 @@ export function removeObserver(observable: IObservable, node: IDerivation) {
78
78
  // invariant(globalState.inBatch > 0, "INTERNAL ERROR, remove should be called only inside batch");
79
79
  // invariant(observable._observers.indexOf(node) !== -1, "INTERNAL ERROR remove already removed node");
80
80
  // invariantObservers(observable);
81
- observable.observers_.delete(node)
82
- if (observable.observers_.size === 0) {
81
+ const observers = observable.observers_
82
+ if (!observers) {
83
+ return
84
+ }
85
+ observers.delete(node)
86
+ if (observers.size === 0) {
83
87
  // deleting last observer
84
88
  queueForUnobservation(observable)
85
89
  }
@@ -108,27 +112,60 @@ export function endBatch() {
108
112
  if (--globalState.inBatch === 0) {
109
113
  runReactions()
110
114
  // the batch is actually about to finish, all unobserving should happen here.
111
- const list = globalState.pendingUnobservations
112
- for (let i = 0; i < list.length; i++) {
113
- const observable = list[i]
114
- observable.isPendingUnobservation = false
115
- if (observable.observers_.size === 0) {
116
- if (observable.isBeingObserved) {
117
- // if this observable had reactive observers, trigger the hooks
118
- observable.isBeingObserved = false
119
- observable.onBUO()
120
- }
121
- if (observable instanceof ComputedValue) {
122
- // computed values are automatically teared down when the last observer leaves
123
- // this process happens recursively, this computed might be the last observabe of another, etc..
124
- observable.suspend_()
115
+ // Guard against re-entering this loop: an onBUO handler can dispose a Reaction,
116
+ // which calls startBatch/endBatch again while we're still iterating. Bail out of
117
+ // the nested call instead of recursing; the outer loop re-reads list.length on
118
+ // every iteration, so it picks up anything the nested dispose() pushes onto the
119
+ // same pendingUnobservations array.
120
+ if (!globalState.isRunningUnobservations) {
121
+ globalState.isRunningUnobservations = true
122
+ try {
123
+ const list = globalState.pendingUnobservations
124
+ for (let i = 0; i < list.length; i++) {
125
+ const observable = list[i]
126
+ observable.isPendingUnobservation = false
127
+ if (!observable.observers_ || observable.observers_.size === 0) {
128
+ if (observable.isBeingObserved) {
129
+ // if this observable had reactive observers, trigger the hooks
130
+ observable.isBeingObserved = false
131
+ observable.onBUO()
132
+ }
133
+ if (observable instanceof ComputedValue) {
134
+ // computed values are automatically teared down when the last observer leaves
135
+ // this process happens recursively, this computed might be the last observabe of another, etc..
136
+ observable.suspend_()
137
+ }
138
+ }
125
139
  }
140
+ globalState.pendingUnobservations = []
141
+ } finally {
142
+ // Always release the guard, even if an onBUO handler (user code) threw,
143
+ // otherwise every future endBatch() would see isRunningUnobservations
144
+ // stuck true and silently stop draining pendingUnobservations forever.
145
+ globalState.isRunningUnobservations = false
126
146
  }
127
147
  }
128
- globalState.pendingUnobservations = []
129
148
  }
130
149
  }
131
150
 
151
+ /**
152
+ * Marks an observable as observed, cascading into the dependencies of a ComputedValue.
153
+ * Unobservation already cascades (`suspend_` -> `clearObserving`), observation normally
154
+ * only does so by accident: a newly observed computed usually recomputes and re-reports
155
+ * its dependencies. When it serves a cached value instead nothing re-reports them, so the
156
+ * transition has to be propagated by hand. See #4547.
157
+ */
158
+ export function markObserved(observable: IObservable) {
159
+ if (observable.isBeingObserved) {
160
+ return
161
+ }
162
+ observable.isBeingObserved = true
163
+ observable.onBO()
164
+ // No queueForUnobservation here: the observer links already exist, so the regular
165
+ // suspend_ -> clearObserving -> removeObserver teardown still delivers the onBUO.
166
+ observable.observing_?.forEach(markObserved)
167
+ }
168
+
132
169
  export function reportObserved(observable: IObservable): boolean {
133
170
  checkIfStateReadsAreAllowed(observable)
134
171
 
@@ -149,7 +186,10 @@ export function reportObserved(observable: IObservable): boolean {
149
186
  }
150
187
  }
151
188
  return observable.isBeingObserved
152
- } else if (observable.observers_.size === 0 && globalState.inBatch > 0) {
189
+ } else if (
190
+ (!observable.observers_ || observable.observers_.size === 0) &&
191
+ globalState.inBatch > 0
192
+ ) {
153
193
  queueForUnobservation(observable)
154
194
  }
155
195
 
@@ -187,7 +227,7 @@ export function propagateChanged(observable: IObservable) {
187
227
  observable.lowestObserverState_ = IDerivationState_.STALE_
188
228
 
189
229
  // Ideally we use for..of here, but the downcompiled version is really slow...
190
- observable.observers_.forEach(d => {
230
+ observable.observers_?.forEach(d => {
191
231
  if (d.dependenciesState_ === IDerivationState_.UP_TO_DATE_) {
192
232
  d.onBecomeStale_()
193
233
  }
@@ -204,7 +244,7 @@ export function propagateChangeConfirmed(observable: IObservable) {
204
244
  }
205
245
  observable.lowestObserverState_ = IDerivationState_.STALE_
206
246
 
207
- observable.observers_.forEach(d => {
247
+ observable.observers_?.forEach(d => {
208
248
  if (d.dependenciesState_ === IDerivationState_.POSSIBLY_STALE_) {
209
249
  d.dependenciesState_ = IDerivationState_.STALE_
210
250
  } else if (
@@ -224,7 +264,7 @@ export function propagateMaybeChanged(observable: IObservable) {
224
264
  }
225
265
  observable.lowestObserverState_ = IDerivationState_.POSSIBLY_STALE_
226
266
 
227
- observable.observers_.forEach(d => {
267
+ observable.observers_?.forEach(d => {
228
268
  if (d.dependenciesState_ === IDerivationState_.UP_TO_DATE_) {
229
269
  d.dependenciesState_ = IDerivationState_.POSSIBLY_STALE_
230
270
  d.onBecomeStale_()
@@ -22,6 +22,11 @@ export interface IEnhancer<T> {
22
22
  }
23
23
 
24
24
  export function deepEnhancer(v, _, name) {
25
+ // primitives can never be made observable; skip the type checks below
26
+ if (v === null || (typeof v !== "object" && typeof v !== "function")) {
27
+ return v
28
+ }
29
+
25
30
  // it is an observable already, done
26
31
  if (isObservable(v)) {
27
32
  return v
@@ -233,21 +233,11 @@ export class ObservableSet<T = any> implements Set<T>, IInterceptable<ISetWillCh
233
233
  }
234
234
 
235
235
  intersection<U>(otherSet: ReadonlySetLike<U> | Set<U>): Set<T & U> {
236
- if (isES6Set(otherSet) && !isObservableSet(otherSet)) {
237
- return otherSet.intersection(this)
238
- } else {
239
- const dehancedSet = new Set(this)
240
- return dehancedSet.intersection(otherSet)
241
- }
236
+ return new Set(this).intersection(otherSet)
242
237
  }
243
238
 
244
239
  union<U>(otherSet: ReadonlySetLike<U> | Set<U>): Set<T | U> {
245
- if (isES6Set(otherSet) && !isObservableSet(otherSet)) {
246
- return otherSet.union(this)
247
- } else {
248
- const dehancedSet = new Set(this)
249
- return dehancedSet.union(otherSet)
250
- }
240
+ return new Set(this).union(otherSet)
251
241
  }
252
242
 
253
243
  difference<U>(otherSet: ReadonlySetLike<U>): Set<T> {
@@ -255,12 +245,7 @@ export class ObservableSet<T = any> implements Set<T>, IInterceptable<ISetWillCh
255
245
  }
256
246
 
257
247
  symmetricDifference<U>(otherSet: ReadonlySetLike<U> | Set<U>): Set<T | U> {
258
- if (isES6Set(otherSet) && !isObservableSet(otherSet)) {
259
- return otherSet.symmetricDifference(this)
260
- } else {
261
- const dehancedSet = new Set(this)
262
- return dehancedSet.symmetricDifference(otherSet)
263
- }
248
+ return new Set(this).symmetricDifference(otherSet)
264
249
  }
265
250
 
266
251
  isSubsetOf(otherSet: ReadonlySetLike<unknown>): boolean {
@@ -272,12 +257,7 @@ export class ObservableSet<T = any> implements Set<T>, IInterceptable<ISetWillCh
272
257
  }
273
258
 
274
259
  isDisjointFrom(otherSet: ReadonlySetLike<unknown> | Set<unknown>): boolean {
275
- if (isES6Set(otherSet) && !isObservableSet(otherSet)) {
276
- return otherSet.isDisjointFrom(this)
277
- } else {
278
- const dehancedSet = new Set(this)
279
- return dehancedSet.isDisjointFrom(otherSet)
280
- }
260
+ return new Set(this).isDisjointFrom(otherSet)
281
261
  }
282
262
 
283
263
  replace(other: ObservableSet<T> | IObservableSetInitialValues<T>): ObservableSet<T> {
@@ -285,17 +265,42 @@ export class ObservableSet<T = any> implements Set<T>, IInterceptable<ISetWillCh
285
265
  other = new Set(other)
286
266
  }
287
267
 
288
- transaction(() => {
289
- if (Array.isArray(other)) {
290
- this.clear()
291
- other.forEach(value => this.add(value))
292
- } else if (isES6Set(other)) {
293
- this.clear()
294
- other.forEach(value => this.add(value))
295
- } else if (other !== null && other !== undefined) {
296
- die(41, other)
297
- }
298
- })
268
+ if (Array.isArray(other) || isES6Set(other)) {
269
+ // Only emit `delete`/`add` events (and `reportChanged`) for values that
270
+ // actually change, instead of clearing and re-adding everything. `add` and
271
+ // `delete` are already no-ops for values that are respectively already
272
+ // present or already absent, so we just need to avoid deleting values that
273
+ // are part of the replacement. See #3761.
274
+ transaction(() => {
275
+ // Collect the desired values for quick lookup. `other` is already a Set
276
+ // here when it was passed (or snapshotted from an observable set) as one,
277
+ // so reuse it rather than allocating another; arrays are wrapped (which
278
+ // also dedupes them).
279
+ const replacementValues: Set<T> = isES6Set(other)
280
+ ? other
281
+ : new Set<T>(other as Iterable<T>)
282
+ // Short-circuit the trivial cases: an empty replacement is just a clear,
283
+ // and replacing into an empty set only needs the adds.
284
+ if (replacementValues.size === 0) {
285
+ this.clear()
286
+ return
287
+ }
288
+ if (this.data_.size === 0) {
289
+ replacementValues.forEach(value => this.add(value))
290
+ return
291
+ }
292
+ // Delete values that are not part of the replacement.
293
+ for (const value of this.data_.values()) {
294
+ if (!replacementValues.has(this.dehanceValue_(value))) {
295
+ this.delete(value)
296
+ }
297
+ }
298
+ // Add new values; values that are already present are a no-op.
299
+ replacementValues.forEach(value => this.add(value))
300
+ })
301
+ } else if (other !== null && other !== undefined) {
302
+ die(41, other)
303
+ }
299
304
 
300
305
  return this
301
306
  }