From fa9b3df5abf199273710a9ab87e3fb68e0222a4d Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 20 Sep 2018 18:03:59 -0400 Subject: [PATCH] test: tests for observer computed --- packages/observer/__tests__/computed.spec.ts | 140 ++++++++++++++++++- packages/observer/src/autorun.ts | 25 +++- packages/observer/src/computed.ts | 2 + 3 files changed, 160 insertions(+), 7 deletions(-) diff --git a/packages/observer/__tests__/computed.spec.ts b/packages/observer/__tests__/computed.spec.ts index 04114631..f969b65d 100644 --- a/packages/observer/__tests__/computed.spec.ts +++ b/packages/observer/__tests__/computed.spec.ts @@ -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) + }) +}) diff --git a/packages/observer/src/autorun.ts b/packages/observer/src/autorun.ts index c0c9aa82..f47eb591 100644 --- a/packages/observer/src/autorun.ts +++ b/packages/observer/src/autorun.ts @@ -7,6 +7,7 @@ export interface Autorun { active: boolean raw: Function deps: Array + computed?: boolean scheduler?: Scheduler onTrack?: Debugger onTrigger?: Debugger @@ -109,33 +110,45 @@ export function trigger( ) { 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, + computedRunners: Set, runnersToAdd: Set | undefined ) { if (runnersToAdd !== void 0) { - runnersToAdd.forEach(runners.add, runners) + runnersToAdd.forEach(runner => { + if (runner.computed) { + computedRunners.add(runner) + } else { + runners.add(runner) + } + }) } } diff --git a/packages/observer/src/computed.ts b/packages/observer/src/computed.ts index 01a4f7e7..c4eb6e94 100644 --- a/packages/observer/src/computed.ts +++ b/packages/observer/src/computed.ts @@ -15,6 +15,8 @@ export function computed(getter: Function, context?: any): ComputedGetter { dirty = true } }) + // mark runner as computed so that it gets priority during trigger + runner.computed = true const computedGetter = (() => { if (dirty) { value = runner()