UNPKG

mobx

Version:

Simple, scalable state management.

387 lines (355 loc) 12 kB
import { $mobx, Atom, ComputedValue, IAtom, IComputedValueOptions, IEnhancer, IInterceptable, IListenable, Lambda, ObservableValue, addHiddenProp, assertPropertyConfigurable, createInstanceofPredicate, deepEnhancer, endBatch, getNextId, hasInterceptors, hasListeners, interceptChange, isObject, isPlainObject, isSpyEnabled, notifyListeners, referenceEnhancer, registerInterceptor, registerListener, spyReportEnd, spyReportStart, startBatch, stringifyKey, globalState, ADD, UPDATE, die, defineProperty, hasProp } from "../internal" export type IObjectDidChange<T = any> = { observableKind: "object" name: PropertyKey object: T debugObjectName: string } & ( | { type: "add" newValue: any } | { type: "update" oldValue: any newValue: any } | { type: "remove" oldValue: any } ) export type IObjectWillChange<T = any> = | { object: T type: "update" | "add" name: PropertyKey newValue: any } | { object: T type: "remove" name: PropertyKey } const REMOVE = "remove" export class ObservableObjectAdministration implements IInterceptable<IObjectWillChange>, IListenable { keysAtom_: IAtom changeListeners_ interceptors_ proxy_: any private pendingKeys_: undefined | Map<PropertyKey, ObservableValue<boolean>> private keysValue_: PropertyKey[] = [] private isStaledKeysValue_ = true constructor( public target_: any, public values_ = new Map<PropertyKey, ObservableValue<any> | ComputedValue<any>>(), public name_: string, public defaultEnhancer_: IEnhancer<any> ) { this.keysAtom_ = new Atom(name_ + ".keys") } read_(key: PropertyKey) { return this.values_.get(key)!.get() } write_(key: PropertyKey, newValue) { const instance = this.target_ const observable = this.values_.get(key) if (observable instanceof ComputedValue) { observable.set(newValue) return } // intercept if (hasInterceptors(this)) { const change = interceptChange<IObjectWillChange>(this, { type: UPDATE, object: this.proxy_ || instance, name: key, newValue }) if (!change) return newValue = (change as any).newValue } newValue = (observable as any).prepareNewValue_(newValue) // notify spy & observers if (newValue !== globalState.UNCHANGED) { const notify = hasListeners(this) const notifySpy = __DEV__ && isSpyEnabled() const change: IObjectDidChange | null = notify || notifySpy ? { type: UPDATE, observableKind: "object", debugObjectName: this.name_, object: this.proxy_ || instance, oldValue: (observable as any).value_, name: key, newValue } : null if (__DEV__ && notifySpy) spyReportStart(change!) ;(observable as ObservableValue<any>).setNewValue_(newValue) if (notify) notifyListeners(this, change) if (__DEV__ && notifySpy) spyReportEnd() } } has_(key: PropertyKey) { const map = this.pendingKeys_ || (this.pendingKeys_ = new Map()) let entry = map.get(key) if (entry) return entry.get() else { const exists = !!this.values_.get(key) // Possible optimization: Don't have a separate map for non existing keys, // but store them in the values map instead, using a special symbol to denote "not existing" entry = new ObservableValue( exists, referenceEnhancer, `${this.name_}.${stringifyKey(key)}?`, false ) map.set(key, entry) return entry.get() // read to subscribe } } addObservableProp_( propName: PropertyKey, newValue, enhancer: IEnhancer<any> = this.defaultEnhancer_ ) { const { target_: target } = this if (__DEV__) assertPropertyConfigurable(target, propName) if (hasInterceptors(this)) { const change = interceptChange<IObjectWillChange>(this, { object: this.proxy_ || target, name: propName, type: ADD, newValue }) if (!change) return newValue = (change as any).newValue } const observable = new ObservableValue( newValue, enhancer, `${this.name_}.${stringifyKey(propName)}`, false ) this.values_.set(propName, observable) newValue = (observable as any).value_ // observableValue might have changed it defineProperty(target, propName, generateObservablePropConfig(propName)) this.notifyPropertyAddition_(propName, newValue) } addComputedProp_( propertyOwner: any, // where is the property declared? propName: PropertyKey, options: IComputedValueOptions<any> ) { const { target_: target } = this options.name = options.name || `${this.name_}.${stringifyKey(propName)}` options.context = this.proxy_ || target this.values_.set(propName, new ComputedValue(options)) // Doesn't seem we need this condition: // if (propertyOwner === target || isPropertyConfigurable(propertyOwner, propName)) defineProperty(propertyOwner, propName, generateComputedPropConfig(propName)) } remove_(key: PropertyKey) { if (!this.values_.has(key)) return const { target_: target } = this if (hasInterceptors(this)) { const change = interceptChange<IObjectWillChange>(this, { object: this.proxy_ || target, name: key, type: REMOVE }) if (!change) return } try { startBatch() const notify = hasListeners(this) const notifySpy = __DEV__ && isSpyEnabled() const oldObservable = this.values_.get(key) const oldValue = oldObservable && oldObservable.get() oldObservable && oldObservable.set(undefined) // notify key and keyset listeners this.reportKeysChanged() this.values_.delete(key) if (this.pendingKeys_) { const entry = this.pendingKeys_.get(key) if (entry) entry.set(false) } // delete the prop delete this.target_[key] const change: IObjectDidChange | null = notify || notifySpy ? ({ type: REMOVE, observableKind: "object", object: this.proxy_ || target, debugObjectName: this.name_, oldValue: oldValue, name: key } as const) : null if (__DEV__ && notifySpy) spyReportStart(change!) if (notify) notifyListeners(this, change) if (__DEV__ && notifySpy) spyReportEnd() } finally { endBatch() } } /** * Observes this object. Triggers for the events 'add', 'update' and 'delete'. * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe * for callback details */ observe_(callback: (changes: IObjectDidChange) => void, fireImmediately?: boolean): Lambda { if (__DEV__ && fireImmediately === true) die("`observe` doesn't support the fire immediately property for observable objects.") return registerListener(this, callback) } intercept_(handler): Lambda { return registerInterceptor(this, handler) } notifyPropertyAddition_(key: PropertyKey, newValue) { const notify = hasListeners(this) const notifySpy = __DEV__ && isSpyEnabled() const change: IObjectDidChange | null = notify || notifySpy ? ({ type: ADD, observableKind: "object", debugObjectName: this.name_, object: this.proxy_ || this.target_, name: key, newValue } as const) : null if (__DEV__ && notifySpy) spyReportStart(change!) if (notify) notifyListeners(this, change) if (__DEV__ && notifySpy) spyReportEnd() if (this.pendingKeys_) { const entry = this.pendingKeys_.get(key) if (entry) entry.set(true) } this.reportKeysChanged() } getKeys_(): PropertyKey[] { this.keysAtom_.reportObserved() if (!this.isStaledKeysValue_) { return this.keysValue_ } // return Reflect.ownKeys(this.values) as any this.keysValue_ = [] for (const [key, value] of this.values_) if (value instanceof ObservableValue) this.keysValue_.push(key) if (__DEV__) Object.freeze(this.keysValue_) this.isStaledKeysValue_ = false return this.keysValue_ } private reportKeysChanged() { this.isStaledKeysValue_ = true this.keysAtom_.reportChanged() } } export interface IIsObservableObject { $mobx: ObservableObjectAdministration } export function asObservableObject( target: any, name: PropertyKey = "", defaultEnhancer: IEnhancer<any> = deepEnhancer ): ObservableObjectAdministration { if (hasProp(target, $mobx)) return target[$mobx] if (__DEV__ && !Object.isExtensible(target)) die("Cannot make the designated object observable; it is not extensible") if (!isPlainObject(target)) name = (target.constructor.name || "ObservableObject") + "@" + getNextId() if (!name) name = "ObservableObject@" + getNextId() const adm = new ObservableObjectAdministration( target, new Map(), stringifyKey(name), defaultEnhancer ) addHiddenProp(target, $mobx, adm) return adm } const observablePropertyConfigs = Object.create(null) const computedPropertyConfigs = Object.create(null) export function generateObservablePropConfig(propName) { return ( observablePropertyConfigs[propName] || (observablePropertyConfigs[propName] = { configurable: true, enumerable: true, get() { return this[$mobx].read_(propName) }, set(v) { this[$mobx].write_(propName, v) } }) ) } export function generateComputedPropConfig(propName) { return ( computedPropertyConfigs[propName] || (computedPropertyConfigs[propName] = { configurable: true, enumerable: false, get() { return this[$mobx].read_(propName) }, set(v) { this[$mobx].write_(propName, v) } }) ) } const isObservableObjectAdministration = createInstanceofPredicate( "ObservableObjectAdministration", ObservableObjectAdministration ) export function isObservableObject(thing: any): boolean { if (isObject(thing)) { return isObservableObjectAdministration((thing as any)[$mobx]) } return false }