UNPKG

10.5 kBPlain TextView Raw
1import {
2 CaughtException,
3 IDerivation,
4 IDerivationState_,
5 IEqualsComparer,
6 IObservable,
7 Lambda,
8 TraceMode,
9 autorun,
10 clearObserving,
11 comparer,
12 createAction,
13 createInstanceofPredicate,
14 endBatch,
15 getNextId,
16 globalState,
17 isCaughtException,
18 isSpyEnabled,
19 propagateChangeConfirmed,
20 propagateMaybeChanged,
21 reportObserved,
22 shouldCompute,
23 spyReport,
24 startBatch,
25 toPrimitive,
26 trackDerivedFunction,
27 untrackedEnd,
28 untrackedStart,
29 UPDATE,
30 die,
31 allowStateChangesStart,
32 allowStateChangesEnd
33} from "../internal"
34
35export interface IComputedValue<T> {
36 get(): T
37 set(value: T): void
38 observe_(listener: (change: IComputedDidChange<T>) => void, fireImmediately?: boolean): Lambda
39}
40
41export interface IComputedValueOptions<T> {
42 get?: () => T
43 set?: (value: T) => void
44 name?: string
45 equals?: IEqualsComparer<T>
46 context?: any
47 requiresReaction?: boolean
48 keepAlive?: boolean
49}
50
51export type IComputedDidChange<T = any> = {
52 type: "update"
53 observableKind: "computed"
54 object: unknown
55 debugObjectName: string
56 newValue: T
57 oldValue: T | undefined
58}
59
60/**
61 * A node in the state dependency root that observes other nodes, and can be observed itself.
62 *
63 * ComputedValue will remember the result of the computation for the duration of the batch, or
64 * while being observed.
65 *
66 * During this time it will recompute only when one of its direct dependencies changed,
67 * but only when it is being accessed with `ComputedValue.get()`.
68 *
69 * Implementation description:
70 * 1. First time it's being accessed it will compute and remember result
71 * give back remembered result until 2. happens
72 * 2. First time any deep dependency change, propagate POSSIBLY_STALE to all observers, wait for 3.
73 * 3. When it's being accessed, recompute if any shallow dependency changed.
74 * if result changed: propagate STALE to all observers, that were POSSIBLY_STALE from the last step.
75 * go to step 2. either way
76 *
77 * If at any point it's outside batch and it isn't observed: reset everything and go to 1.
78 */
79export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDerivation {
80 dependenciesState_ = IDerivationState_.NOT_TRACKING_
81 observing_: IObservable[] = [] // nodes we are looking at. Our value depends on these nodes
82 newObserving_ = null // during tracking it's an array with new observed observers
83 isBeingObserved_ = false
84 isPendingUnobservation_: boolean = false
85 observers_ = new Set<IDerivation>()
86 diffValue_ = 0
87 runId_ = 0
88 lastAccessedBy_ = 0
89 lowestObserverState_ = IDerivationState_.UP_TO_DATE_
90 unboundDepsCount_ = 0
91 protected value_: T | undefined | CaughtException = new CaughtException(null)
92 name_: string
93 triggeredBy_?: string
94 isComputing_: boolean = false // to check for cycles
95 isRunningSetter_: boolean = false
96 derivation: () => T // N.B: unminified as it is used by MST
97 setter_?: (value: T) => void
98 isTracing_: TraceMode = TraceMode.NONE
99 scope_: Object | undefined
100 private equals_: IEqualsComparer<any>
101 private requiresReaction_: boolean
102 keepAlive_: boolean
103
104 /**
105 * Create a new computed value based on a function expression.
106 *
107 * The `name` property is for debug purposes only.
108 *
109 * The `equals` property specifies the comparer function to use to determine if a newly produced
110 * value differs from the previous value. Two comparers are provided in the library; `defaultComparer`
111 * compares based on identity comparison (===), and `structuralComparer` deeply compares the structure.
112 * Structural comparison can be convenient if you always produce a new aggregated object and
113 * don't want to notify observers if it is structurally the same.
114 * This is useful for working with vectors, mouse coordinates etc.
115 */
116 constructor(options: IComputedValueOptions<T>) {
117 if (!options.get) die(31)
118 this.derivation = options.get!
119 this.name_ = options.name || (__DEV__ ? "ComputedValue@" + getNextId() : "ComputedValue")
120 if (options.set) {
121 this.setter_ = createAction(
122 __DEV__ ? this.name_ + "-setter" : "ComputedValue-setter",
123 options.set
124 ) as any
125 }
126 this.equals_ =
127 options.equals ||
128 ((options as any).compareStructural || (options as any).struct
129 ? comparer.structural
130 : comparer.default)
131 this.scope_ = options.context
132 this.requiresReaction_ = !!options.requiresReaction
133 this.keepAlive_ = !!options.keepAlive
134 }
135
136 onBecomeStale_() {
137 propagateMaybeChanged(this)
138 }
139
140 public onBOL: Set<Lambda> | undefined
141 public onBUOL: Set<Lambda> | undefined
142
143 public onBO() {
144 if (this.onBOL) {
145 this.onBOL.forEach(listener => listener())
146 }
147 }
148
149 public onBUO() {
150 if (this.onBUOL) {
151 this.onBUOL.forEach(listener => listener())
152 }
153 }
154
155 /**
156 * Returns the current value of this computed value.
157 * Will evaluate its computation first if needed.
158 */
159 public get(): T {
160 if (this.isComputing_) die(32, this.name_, this.derivation)
161 if (
162 globalState.inBatch === 0 &&
163 // !globalState.trackingDerivatpion &&
164 this.observers_.size === 0 &&
165 !this.keepAlive_
166 ) {
167 if (shouldCompute(this)) {
168 this.warnAboutUntrackedRead_()
169 startBatch() // See perf test 'computed memoization'
170 this.value_ = this.computeValue_(false)
171 endBatch()
172 }
173 } else {
174 reportObserved(this)
175 if (shouldCompute(this)) {
176 let prevTrackingContext = globalState.trackingContext
177 if (this.keepAlive_ && !prevTrackingContext) globalState.trackingContext = this
178 if (this.trackAndCompute()) propagateChangeConfirmed(this)
179 globalState.trackingContext = prevTrackingContext
180 }
181 }
182 const result = this.value_!
183
184 if (isCaughtException(result)) throw result.cause
185 return result
186 }
187
188 public set(value: T) {
189 if (this.setter_) {
190 if (this.isRunningSetter_) die(33, this.name_)
191 this.isRunningSetter_ = true
192 try {
193 this.setter_.call(this.scope_, value)
194 } finally {
195 this.isRunningSetter_ = false
196 }
197 } else die(34, this.name_)
198 }
199
200 trackAndCompute(): boolean {
201 // N.B: unminified as it is used by MST
202 const oldValue = this.value_
203 const wasSuspended =
204 /* see #1208 */ this.dependenciesState_ === IDerivationState_.NOT_TRACKING_
205 const newValue = this.computeValue_(true)
206
207 const changed =
208 wasSuspended ||
209 isCaughtException(oldValue) ||
210 isCaughtException(newValue) ||
211 !this.equals_(oldValue, newValue)
212
213 if (changed) {
214 this.value_ = newValue
215
216 if (__DEV__ && isSpyEnabled()) {
217 spyReport({
218 observableKind: "computed",
219 debugObjectName: this.name_,
220 object: this.scope_,
221 type: "update",
222 oldValue,
223 newValue
224 } as IComputedDidChange)
225 }
226 }
227
228 return changed
229 }
230
231 computeValue_(track: boolean) {
232 this.isComputing_ = true
233 // don't allow state changes during computation
234 const prev = allowStateChangesStart(false)
235 let res: T | CaughtException
236 if (track) {
237 res = trackDerivedFunction(this, this.derivation, this.scope_)
238 } else {
239 if (globalState.disableErrorBoundaries === true) {
240 res = this.derivation.call(this.scope_)
241 } else {
242 try {
243 res = this.derivation.call(this.scope_)
244 } catch (e) {
245 res = new CaughtException(e)
246 }
247 }
248 }
249 allowStateChangesEnd(prev)
250 this.isComputing_ = false
251 return res
252 }
253
254 suspend_() {
255 if (!this.keepAlive_) {
256 clearObserving(this)
257 this.value_ = undefined // don't hold on to computed value!
258 if (__DEV__ && this.isTracing_ !== TraceMode.NONE) {
259 console.log(
260 `[mobx.trace] Computed value '${this.name_}' was suspended and it will recompute on the next access.`
261 )
262 }
263 }
264 }
265
266 observe_(listener: (change: IComputedDidChange<T>) => void, fireImmediately?: boolean): Lambda {
267 let firstTime = true
268 let prevValue: T | undefined = undefined
269 return autorun(() => {
270 // TODO: why is this in a different place than the spyReport() function? in all other observables it's called in the same place
271 let newValue = this.get()
272 if (!firstTime || fireImmediately) {
273 const prevU = untrackedStart()
274 listener({
275 observableKind: "computed",
276 debugObjectName: this.name_,
277 type: UPDATE,
278 object: this,
279 newValue,
280 oldValue: prevValue
281 })
282 untrackedEnd(prevU)
283 }
284 firstTime = false
285 prevValue = newValue
286 })
287 }
288
289 warnAboutUntrackedRead_() {
290 if (!__DEV__) return
291 if (this.isTracing_ !== TraceMode.NONE) {
292 console.log(
293 `[mobx.trace] Computed value '${this.name_}' is being read outside a reactive context. Doing a full recompute.`
294 )
295 }
296 if (globalState.computedRequiresReaction || this.requiresReaction_) {
297 console.warn(
298 `[mobx] Computed value '${this.name_}' is being read outside a reactive context. Doing a full recompute.`
299 )
300 }
301 }
302
303 toString() {
304 return `${this.name_}[${this.derivation.toString()}]`
305 }
306
307 valueOf(): T {
308 return toPrimitive(this.get())
309 }
310
311 [Symbol.toPrimitive]() {
312 return this.valueOf()
313 }
314}
315
316export const isComputedValue = createInstanceofPredicate("ComputedValue", ComputedValue)