-describe('observer/computed', () => {})
+import { computed, observable, autorun } from '../src'
+
+describe('observer/computed', () => {
+ it('should return updated value', () => {
+ const value: any = observable({})
+ const cValue = computed(() => value.foo)
+ expect(cValue()).toBe(undefined)
+ value.foo = 1
+ expect(cValue()).toBe(1)
+ })
+
+ it('should compute lazily', () => {
+ const value: any = observable({})
+ const getter = jest.fn(() => value.foo)
+ const cValue = computed(getter)
+
+ // lazy
+ expect(getter).not.toHaveBeenCalled()
+
+ expect(cValue()).toBe(undefined)
+ expect(getter).toHaveBeenCalledTimes(1)
+
+ // should not compute again
+ cValue()
+ expect(getter).toHaveBeenCalledTimes(1)
+
+ // should not compute until needed
+ value.foo = 1
+ expect(getter).toHaveBeenCalledTimes(1)
+
+ // now it should compute
+ expect(cValue()).toBe(1)
+ expect(getter).toHaveBeenCalledTimes(2)
+
+ // should not compute again
+ cValue()
+ expect(getter).toHaveBeenCalledTimes(2)
+ })
+
+ it('should accept context', () => {
+ const value: any = observable({})
+ let callCtx, callArg
+ const getter = function(arg: any) {
+ callCtx = this
+ callArg = arg
+ return value.foo
+ }
+ const ctx = {}
+ const cValue = computed(getter, ctx)
+ cValue()
+ expect(callCtx).toBe(ctx)
+ expect(callArg).toBe(ctx)
+ })
+
+ it('should trigger autorun', () => {
+ const value: any = observable({})
+ const cValue = computed(() => value.foo)
+ let dummy
+ autorun(() => {
+ dummy = cValue()
+ })
+ expect(dummy).toBe(undefined)
+ value.foo = 1
+ expect(dummy).toBe(1)
+ })
+
+ it('should work when chained', () => {
+ const value: any = observable({ foo: 0 })
+ const c1 = computed(() => value.foo)
+ const c2 = computed(() => c1() + 1)
+ expect(c2()).toBe(1)
+ expect(c1()).toBe(0)
+ value.foo++
+ expect(c2()).toBe(2)
+ expect(c1()).toBe(1)
+ })
+
+ it('should trigger autorun when chained', () => {
+ const value: any = observable({ foo: 0 })
+ const getter1 = jest.fn(() => value.foo)
+ const getter2 = jest.fn(() => {
+ return c1() + 1
+ })
+ const c1 = computed(getter1)
+ const c2 = computed(getter2)
+
+ let dummy
+ autorun(() => {
+ dummy = c2()
+ })
+ expect(dummy).toBe(1)
+ expect(getter1).toHaveBeenCalledTimes(1)
+ expect(getter2).toHaveBeenCalledTimes(1)
+ value.foo++
+ expect(dummy).toBe(2)
+ // should not result in duplicate calls
+ expect(getter1).toHaveBeenCalledTimes(2)
+ expect(getter2).toHaveBeenCalledTimes(2)
+ })
+
+ it('should trigger autorun when chained (mixed invocations)', () => {
+ const value: any = observable({ foo: 0 })
+ const getter1 = jest.fn(() => value.foo)
+ const getter2 = jest.fn(() => {
+ return c1() + 1
+ })
+ const c1 = computed(getter1)
+ const c2 = computed(getter2)
+
+ let dummy
+ autorun(() => {
+ dummy = c1() + c2()
+ })
+ expect(dummy).toBe(1)
+
+ expect(getter1).toHaveBeenCalledTimes(1)
+ expect(getter2).toHaveBeenCalledTimes(1)
+ value.foo++
+ expect(dummy).toBe(3)
+ // should not result in duplicate calls
+ expect(getter1).toHaveBeenCalledTimes(2)
+ expect(getter2).toHaveBeenCalledTimes(2)
+ })
+
+ it('should no longer update when stopped', () => {
+ const value: any = observable({})
+ const cValue = computed(() => value.foo)
+ let dummy
+ autorun(() => {
+ dummy = cValue()
+ })
+ expect(dummy).toBe(undefined)
+ value.foo = 1
+ expect(dummy).toBe(1)
+ cValue.stop()
+ value.foo = 2
+ expect(dummy).toBe(1)
+ })
+})
active: boolean
raw: Function
deps: Array<Dep>
+ computed?: boolean
scheduler?: Scheduler
onTrack?: Debugger
onTrigger?: Debugger
) {
const depsMap = targetMap.get(target) as KeyToDepMap
const runners = new Set()
+ const computedRunners = new Set()
if (type === OperationTypes.CLEAR) {
// collection being cleared, trigger all runners for target
depsMap.forEach(dep => {
- addRunners(runners, dep)
+ addRunners(runners, computedRunners, dep)
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
- addRunners(runners, depsMap.get(key as string | symbol))
+ addRunners(runners, computedRunners, depsMap.get(key as string | symbol))
}
// also run for iteration key on ADD | DELETE
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
- addRunners(runners, depsMap.get(iterationKey))
+ addRunners(runners, computedRunners, depsMap.get(iterationKey))
}
}
- runners.forEach(runner => {
+ const run = (runner: Autorun) => {
scheduleRun(runner, target, type, key, extraInfo)
- })
+ }
+ // Important: computed runners must be run first so that computed getters
+ // can be invalidated before any normal runners that depend on them are run.
+ computedRunners.forEach(run)
+ runners.forEach(run)
}
function addRunners(
runners: Set<Autorun>,
+ computedRunners: Set<Autorun>,
runnersToAdd: Set<Autorun> | undefined
) {
if (runnersToAdd !== void 0) {
- runnersToAdd.forEach(runners.add, runners)
+ runnersToAdd.forEach(runner => {
+ if (runner.computed) {
+ computedRunners.add(runner)
+ } else {
+ runners.add(runner)
+ }
+ })
}
}