test: tests for observer computed

This commit is contained in:
Evan You 2018-09-20 18:03:59 -04:00
parent 8a714a6c39
commit fa9b3df5ab
3 changed files with 160 additions and 7 deletions

View File

@ -1 +1,139 @@
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)
})
})

View File

@ -7,6 +7,7 @@ export interface Autorun {
active: boolean active: boolean
raw: Function raw: Function
deps: Array<Dep> deps: Array<Dep>
computed?: boolean
scheduler?: Scheduler scheduler?: Scheduler
onTrack?: Debugger onTrack?: Debugger
onTrigger?: Debugger onTrigger?: Debugger
@ -109,33 +110,45 @@ export function trigger(
) { ) {
const depsMap = targetMap.get(target) as KeyToDepMap const depsMap = targetMap.get(target) as KeyToDepMap
const runners = new Set() const runners = new Set()
const computedRunners = new Set()
if (type === OperationTypes.CLEAR) { if (type === OperationTypes.CLEAR) {
// collection being cleared, trigger all runners for target // collection being cleared, trigger all runners for target
depsMap.forEach(dep => { depsMap.forEach(dep => {
addRunners(runners, dep) addRunners(runners, computedRunners, dep)
}) })
} else { } else {
// schedule runs for SET | ADD | DELETE // schedule runs for SET | ADD | DELETE
if (key !== void 0) { 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 // also run for iteration key on ADD | DELETE
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) { if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY 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) 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( function addRunners(
runners: Set<Autorun>, runners: Set<Autorun>,
computedRunners: Set<Autorun>,
runnersToAdd: Set<Autorun> | undefined runnersToAdd: Set<Autorun> | undefined
) { ) {
if (runnersToAdd !== void 0) { if (runnersToAdd !== void 0) {
runnersToAdd.forEach(runners.add, runners) runnersToAdd.forEach(runner => {
if (runner.computed) {
computedRunners.add(runner)
} else {
runners.add(runner)
}
})
} }
} }

View File

@ -15,6 +15,8 @@ export function computed(getter: Function, context?: any): ComputedGetter {
dirty = true dirty = true
} }
}) })
// mark runner as computed so that it gets priority during trigger
runner.computed = true
const computedGetter = (() => { const computedGetter = (() => {
if (dirty) { if (dirty) {
value = runner() value = runner()