1 | import {
|
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 |
|
35 | export interface IComputedValue<T> {
|
36 | get(): T
|
37 | set(value: T): void
|
38 | observe_(listener: (change: IComputedDidChange<T>) => void, fireImmediately?: boolean): Lambda
|
39 | }
|
40 |
|
41 | export 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 |
|
51 | export 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 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDerivation {
|
80 | dependenciesState_ = IDerivationState_.NOT_TRACKING_
|
81 | observing_: IObservable[] = []
|
82 | newObserving_ = null
|
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
|
95 | isRunningSetter_: boolean = false
|
96 | derivation: () => T
|
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 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
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 |
|
157 |
|
158 |
|
159 | public get(): T {
|
160 | if (this.isComputing_) die(32, this.name_, this.derivation)
|
161 | if (
|
162 | globalState.inBatch === 0 &&
|
163 |
|
164 | this.observers_.size === 0 &&
|
165 | !this.keepAlive_
|
166 | ) {
|
167 | if (shouldCompute(this)) {
|
168 | this.warnAboutUntrackedRead_()
|
169 | startBatch()
|
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 |
|
202 | const oldValue = this.value_
|
203 | const wasSuspended =
|
204 | 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 |
|
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
|
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 |
|
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 |
|
316 | export const isComputedValue = createInstanceofPredicate("ComputedValue", ComputedValue)
|