From 88f10ec66b740a18c14a23a9d6876c5d8483b4c8 Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Fri, 19 Jun 2020 13:58:38 +0100 Subject: [PATCH 1/2] fix(watchEffect): prevent recursive calls when using `flush:sync` --- src/apis/watch.ts | 286 +++++++++-------- test/v3/runtime-core/apiWatch.spec.ts | 440 ++++++++++++++------------ 2 files changed, 392 insertions(+), 334 deletions(-) diff --git a/src/apis/watch.ts b/src/apis/watch.ts index 342597fc..cb5a73e2 100644 --- a/src/apis/watch.ts +++ b/src/apis/watch.ts @@ -1,75 +1,75 @@ -import { ComponentInstance } from '../component'; -import { Ref, isRef, isReactive } from '../reactivity'; -import { assert, logError, noopFn, warn, isFunction } from '../utils'; -import { defineComponentInstance } from '../helper'; -import { getCurrentVM, getCurrentVue } from '../runtimeContext'; -import { WatcherPreFlushQueueKey, WatcherPostFlushQueueKey } from '../symbols'; -import { ComputedRef } from './computed'; +import { ComponentInstance } from '../component' +import { Ref, isRef, isReactive } from '../reactivity' +import { assert, logError, noopFn, warn, isFunction } from '../utils' +import { defineComponentInstance } from '../helper' +import { getCurrentVM, getCurrentVue } from '../runtimeContext' +import { WatcherPreFlushQueueKey, WatcherPostFlushQueueKey } from '../symbols' +import { ComputedRef } from './computed' -export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void; +export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void -export type WatchSource = Ref | ComputedRef | (() => T); +export type WatchSource = Ref | ComputedRef | (() => T) export type WatchCallback = ( value: V, oldValue: OV, onInvalidate: InvalidateCbRegistrator -) => any; +) => any type MapSources = { - [K in keyof T]: T[K] extends WatchSource ? V : never; -}; + [K in keyof T]: T[K] extends WatchSource ? V : never +} type MapOldSources = { [K in keyof T]: T[K] extends WatchSource ? Immediate extends true ? V | undefined : V - : never; -}; + : never +} export interface WatchOptionsBase { - flush?: FlushMode; + flush?: FlushMode // onTrack?: ReactiveEffectOptions['onTrack']; // onTrigger?: ReactiveEffectOptions['onTrigger']; } -type InvalidateCbRegistrator = (cb: () => void) => void; +type InvalidateCbRegistrator = (cb: () => void) => void -export type FlushMode = 'pre' | 'post' | 'sync'; +export type FlushMode = 'pre' | 'post' | 'sync' export interface WatchOptions extends WatchOptionsBase { - immediate?: Immediate; - deep?: boolean; + immediate?: Immediate + deep?: boolean } export interface VueWatcher { - lazy: boolean; - get(): any; - teardown(): void; + lazy: boolean + get(): any + teardown(): void } -export type WatchStopHandle = () => void; +export type WatchStopHandle = () => void -let fallbackVM: ComponentInstance; +let fallbackVM: ComponentInstance function flushPreQueue(this: any) { - flushQueue(this, WatcherPreFlushQueueKey); + flushQueue(this, WatcherPreFlushQueueKey) } function flushPostQueue(this: any) { - flushQueue(this, WatcherPostFlushQueueKey); + flushQueue(this, WatcherPostFlushQueueKey) } function hasWatchEnv(vm: any) { - return vm[WatcherPreFlushQueueKey] !== undefined; + return vm[WatcherPreFlushQueueKey] !== undefined } function installWatchEnv(vm: any) { - vm[WatcherPreFlushQueueKey] = []; - vm[WatcherPostFlushQueueKey] = []; - vm.$on('hook:beforeUpdate', flushPreQueue); - vm.$on('hook:updated', flushPostQueue); + vm[WatcherPreFlushQueueKey] = [] + vm[WatcherPostFlushQueueKey] = [] + vm.$on('hook:beforeUpdate', flushPreQueue) + vm.$on('hook:updated', flushPostQueue) } function getWatcherOption(options?: Partial): WatchOptions { @@ -80,7 +80,7 @@ function getWatcherOption(options?: Partial): WatchOptions { flush: 'post', }, ...options, - }; + } } function getWatchEffectOption(options?: Partial): WatchOptions { @@ -91,55 +91,62 @@ function getWatchEffectOption(options?: Partial): WatchOptions { flush: 'post', }, ...options, - }; + } } function getWatcherVM() { - let vm = getCurrentVM(); + let vm = getCurrentVM() if (!vm) { if (!fallbackVM) { - fallbackVM = defineComponentInstance(getCurrentVue()); + fallbackVM = defineComponentInstance(getCurrentVue()) } - vm = fallbackVM; + vm = fallbackVM } else if (!hasWatchEnv(vm)) { - installWatchEnv(vm); + installWatchEnv(vm) } - return vm; + return vm } function flushQueue(vm: any, key: any) { - const queue = vm[key]; + const queue = vm[key] for (let index = 0; index < queue.length; index++) { - queue[index](); + queue[index]() } - queue.length = 0; + queue.length = 0 } -function queueFlushJob(vm: any, fn: () => void, mode: Exclude) { +function queueFlushJob( + vm: any, + fn: () => void, + mode: Exclude +) { // flush all when beforeUpdate and updated are not fired const fallbackFlush = () => { vm.$nextTick(() => { if (vm[WatcherPreFlushQueueKey].length) { - flushQueue(vm, WatcherPreFlushQueueKey); + flushQueue(vm, WatcherPreFlushQueueKey) } if (vm[WatcherPostFlushQueueKey].length) { - flushQueue(vm, WatcherPostFlushQueueKey); + flushQueue(vm, WatcherPostFlushQueueKey) } - }); - }; + }) + } switch (mode) { case 'pre': - fallbackFlush(); - vm[WatcherPreFlushQueueKey].push(fn); - break; + fallbackFlush() + vm[WatcherPreFlushQueueKey].push(fn) + break case 'post': - fallbackFlush(); - vm[WatcherPostFlushQueueKey].push(fn); - break; + fallbackFlush() + vm[WatcherPostFlushQueueKey].push(fn) + break default: - assert(false, `flush must be one of ["post", "pre", "sync"], but got ${mode}`); - break; + assert( + false, + `flush must be one of ["post", "pre", "sync"], but got ${mode}` + ) + break } } @@ -148,14 +155,14 @@ function createVueWatcher( getter: () => any, callback: (n: any, o: any) => any, options: { - deep: boolean; - sync: boolean; - immediateInvokeCallback?: boolean; - noRun?: boolean; - before?: () => void; + deep: boolean + sync: boolean + immediateInvokeCallback?: boolean + noRun?: boolean + before?: () => void } ): VueWatcher { - const index = vm._watchers.length; + const index = vm._watchers.length // @ts-ignore: use undocumented options vm.$watch(getter, callback, { immediate: options.immediateInvokeCallback, @@ -163,19 +170,19 @@ function createVueWatcher( lazy: options.noRun, sync: options.sync, before: options.before, - }); + }) - return vm._watchers[index]; + return vm._watchers[index] } // We have to monkeypatch the teardown function so Vue will run // runCleanup() when it tears down the watcher on unmmount. function patchWatcherTeardown(watcher: VueWatcher, runCleanup: () => void) { - const _teardown = watcher.teardown; + const _teardown = watcher.teardown watcher.teardown = function (...args) { - _teardown.apply(watcher, args); - runCleanup(); - }; + _teardown.apply(watcher, args) + runCleanup() + } } function createWatcher( @@ -184,100 +191,115 @@ function createWatcher( cb: WatchCallback | null, options: WatchOptions ): () => void { - const flushMode = options.flush; - const isSync = flushMode === 'sync'; - let cleanup: (() => void) | null; + const flushMode = options.flush + const isSync = flushMode === 'sync' + let cleanup: (() => void) | null const registerCleanup: InvalidateCbRegistrator = (fn: () => void) => { cleanup = () => { try { - fn(); + fn() } catch (error) { - logError(error, vm, 'onCleanup()'); + logError(error, vm, 'onCleanup()') } - }; - }; + } + } // cleanup before running getter again const runCleanup = () => { if (cleanup) { - cleanup(); - cleanup = null; + cleanup() + cleanup = null } - }; + } const createScheduler = (fn: T): T => { - if (isSync || /* without a current active instance, ignore pre|post mode */ vm === fallbackVM) { - return fn; + if ( + isSync || + /* without a current active instance, ignore pre|post mode */ vm === + fallbackVM + ) { + return fn } return (((...args: any[]) => queueFlushJob( vm, () => { - fn(...args); + fn(...args) }, flushMode as 'pre' | 'post' - )) as any) as T; - }; + )) as any) as T + } // effect watch if (cb === null) { - const getter = () => (source as WatchEffect)(registerCleanup); + let running = false + const getter = () => { + if (running) { + return // already running + } + try { + running = true + ;(source as WatchEffect)(registerCleanup) + } finally { + running = false + } + } const watcher = createVueWatcher(vm, getter, noopFn, { deep: options.deep || false, sync: isSync, before: runCleanup, - }); + }) - patchWatcherTeardown(watcher, runCleanup); + patchWatcherTeardown(watcher, runCleanup) // enable the watcher update - watcher.lazy = false; - const originGet = watcher.get.bind(watcher); + watcher.lazy = false + const originGet = watcher.get.bind(watcher) // always run watchEffect - watcher.get = createScheduler(originGet); + watcher.get = createScheduler(originGet) return () => { - watcher.teardown(); - }; + watcher.teardown() + } } - let deep = options.deep; + let deep = options.deep - let getter: () => any; + let getter: () => any if (Array.isArray(source)) { - getter = () => source.map((s) => (isRef(s) ? s.value : s())); + getter = () => source.map((s) => (isRef(s) ? s.value : s())) } else if (isRef(source)) { - getter = () => source.value; + getter = () => source.value } else if (isReactive(source)) { - getter = () => source; - deep = true; + getter = () => source + deep = true } else if (isFunction(source)) { - getter = source as () => any; + getter = source as () => any } else { - getter = noopFn; + getter = noopFn warn( `Invalid watch source: ${JSON.stringify(source)}. A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.`, vm - ); + ) } const applyCb = (n: any, o: any) => { // cleanup before running cb again - runCleanup(); - cb(n, o, registerCleanup); - }; - let callback = createScheduler(applyCb); + runCleanup() + cb(n, o, registerCleanup) + } + let callback = createScheduler(applyCb) if (options.immediate) { - const originalCallbck = callback; + const originalCallbck = callback // `shiftCallback` is used to handle the first sync effect run. // The subsequent callbacks will redirect to `callback`. let shiftCallback = (n: any, o: any) => { - shiftCallback = originalCallbck; - applyCb(n, o); - }; + shiftCallback = originalCallbck + applyCb(n, o) + } callback = (n: any, o: any) => { - shiftCallback(n, o); - }; + shiftCallback(n, o) + } } // @ts-ignore: use undocumented option "sync" @@ -285,21 +307,24 @@ function createWatcher( immediate: options.immediate, deep: deep, sync: isSync, - }); + }) // Once again, we have to hack the watcher for proper teardown - const watcher = vm._watchers[vm._watchers.length - 1]; - patchWatcherTeardown(watcher, runCleanup); + const watcher = vm._watchers[vm._watchers.length - 1] + patchWatcherTeardown(watcher, runCleanup) return () => { - stop(); - }; + stop() + } } -export function watchEffect(effect: WatchEffect, options?: WatchOptionsBase): WatchStopHandle { - const opts = getWatchEffectOption(options); - const vm = getWatcherVM(); - return createWatcher(vm, effect, null, opts); +export function watchEffect( + effect: WatchEffect, + options?: WatchOptionsBase +): WatchStopHandle { + const opts = getWatchEffectOption(options) + const vm = getWatcherVM() + return createWatcher(vm, effect, null, opts) } // overload #1: array of multiple sources + cb @@ -313,21 +338,24 @@ export function watch< sources: T, cb: WatchCallback, MapOldSources>, options?: WatchOptions -): WatchStopHandle; +): WatchStopHandle // overload #2: single source + cb export function watch = false>( source: WatchSource, cb: WatchCallback, options?: WatchOptions -): WatchStopHandle; +): WatchStopHandle // overload #3: watching reactive object w/ cb -export function watch = false>( +export function watch< + T extends object, + Immediate extends Readonly = false +>( source: T, cb: WatchCallback, options?: WatchOptions -): WatchStopHandle; +): WatchStopHandle // implementation export function watch( @@ -335,10 +363,10 @@ export function watch( cb: WatchCallback, options?: WatchOptions ): WatchStopHandle { - let callback: WatchCallback | null = null; + let callback: WatchCallback | null = null if (typeof cb === 'function') { // source watch - callback = cb as WatchCallback; + callback = cb as WatchCallback } else { // effect watch if (__DEV__) { @@ -346,14 +374,14 @@ export function watch( `\`watch(fn, options?)\` signature has been moved to a separate API. ` + `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` + `supports \`watch(source, cb, options?) signature.` - ); + ) } - options = cb as Partial; - callback = null; + options = cb as Partial + callback = null } - const opts = getWatcherOption(options); - const vm = getWatcherVM(); + const opts = getWatcherOption(options) + const vm = getWatcherVM() - return createWatcher(vm, source, callback, opts); + return createWatcher(vm, source, callback, opts) } diff --git a/test/v3/runtime-core/apiWatch.spec.ts b/test/v3/runtime-core/apiWatch.spec.ts index ec16a411..d766b40d 100644 --- a/test/v3/runtime-core/apiWatch.spec.ts +++ b/test/v3/runtime-core/apiWatch.spec.ts @@ -1,243 +1,268 @@ -import { watch, watchEffect, computed, reactive, ref, set, shallowReactive } from '../../../src'; -import { nextTick } from '../../helpers/utils'; -import Vue from 'vue'; +import { + watch, + watchEffect, + computed, + reactive, + ref, + set, + shallowReactive, +} from '../../../src' +import { nextTick } from '../../helpers/utils' +import Vue from 'vue' // reference: https://vue-composition-api-rfc.netlify.com/api.html#watch describe('api: watch', () => { // const warnSpy = jest.spyOn(console, 'warn'); - const warnSpy = jest.spyOn((Vue as any).util, 'warn'); + const warnSpy = jest.spyOn((Vue as any).util, 'warn') beforeEach(() => { - warnSpy.mockReset(); - }); + warnSpy.mockReset() + }) it('effect', async () => { - const state = reactive({ count: 0 }); - let dummy; + const state = reactive({ count: 0 }) + let dummy watchEffect(() => { - dummy = state.count; - }); - expect(dummy).toBe(0); + dummy = state.count + }) + expect(dummy).toBe(0) - state.count++; - await nextTick(); - expect(dummy).toBe(1); - }); + state.count++ + await nextTick() + expect(dummy).toBe(1) + }) it('watching single source: getter', async () => { - const state = reactive({ count: 0 }); - let dummy; + const state = reactive({ count: 0 }) + let dummy watch( () => state.count, (count, prevCount) => { - dummy = [count, prevCount]; + dummy = [count, prevCount] // assert types - count + 1; + count + 1 if (prevCount) { - prevCount + 1; + prevCount + 1 } } - ); - state.count++; - await nextTick(); - expect(dummy).toMatchObject([1, 0]); - }); + ) + state.count++ + await nextTick() + expect(dummy).toMatchObject([1, 0]) + }) it('watching single source: ref', async () => { - const count = ref(0); - let dummy; + const count = ref(0) + let dummy watch(count, (count, prevCount) => { - dummy = [count, prevCount]; + dummy = [count, prevCount] // assert types - count + 1; + count + 1 if (prevCount) { - prevCount + 1; + prevCount + 1 } - }); - count.value++; - await nextTick(); - expect(dummy).toMatchObject([1, 0]); - }); + }) + count.value++ + await nextTick() + expect(dummy).toMatchObject([1, 0]) + }) it('watching single source: computed ref', async () => { - const count = ref(0); - const plus = computed(() => count.value + 1); - let dummy; + const count = ref(0) + const plus = computed(() => count.value + 1) + let dummy watch(plus, (count, prevCount) => { - dummy = [count, prevCount]; + dummy = [count, prevCount] // assert types - count + 1; + count + 1 if (prevCount) { - prevCount + 1; + prevCount + 1 } - }); - count.value++; - await nextTick(); - expect(dummy).toMatchObject([2, 1]); - }); + }) + count.value++ + await nextTick() + expect(dummy).toMatchObject([2, 1]) + }) it('watching primitive with deep: true', async () => { - const count = ref(0); - let dummy; + const count = ref(0) + let dummy watch( count, (c, prevCount) => { - dummy = [c, prevCount]; + dummy = [c, prevCount] }, { deep: true, } - ); - count.value++; - await nextTick(); - expect(dummy).toMatchObject([1, 0]); - }); + ) + count.value++ + await nextTick() + expect(dummy).toMatchObject([1, 0]) + }) it('directly watching reactive object (with automatic deep: true)', async () => { const src = reactive({ count: 0, - }); - let dummy; + }) + let dummy watch(src, ({ count }) => { - dummy = count; - }); - src.count++; - await nextTick(); - expect(dummy).toBe(1); - }); + dummy = count + }) + src.count++ + await nextTick() + expect(dummy).toBe(1) + }) it('watching multiple sources', async () => { - const state = reactive({ count: 1 }); - const count = ref(1); - const plus = computed(() => count.value + 1); + const state = reactive({ count: 1 }) + const count = ref(1) + const plus = computed(() => count.value + 1) - let dummy; + let dummy watch([() => state.count, count, plus], (vals, oldVals) => { - dummy = [vals, oldVals]; + dummy = [vals, oldVals] // assert types - vals.concat(1); - oldVals.concat(1); - }); + vals.concat(1) + oldVals.concat(1) + }) - state.count++; - count.value++; - await nextTick(); + state.count++ + count.value++ + await nextTick() expect(dummy).toMatchObject([ [2, 2, 3], [1, 1, 2], - ]); - }); + ]) + }) it('watching multiple sources: readonly array', async () => { - const state = reactive({ count: 1 }); - const status = ref(false); + const state = reactive({ count: 1 }) + const status = ref(false) - let dummy; + let dummy watch([() => state.count, status] as const, (vals, oldVals) => { - dummy = [vals, oldVals]; - const [count] = vals; - const [, oldStatus] = oldVals; + dummy = [vals, oldVals] + const [count] = vals + const [, oldStatus] = oldVals // assert types - count + 1; - oldStatus === true; - }); + count + 1 + oldStatus === true + }) - state.count++; - status.value = true; - await nextTick(); + state.count++ + status.value = true + await nextTick() expect(dummy).toMatchObject([ [2, true], [1, false], - ]); - }); + ]) + }) it('warn invalid watch source', () => { // @ts-ignore - watch(1, () => {}); + watch(1, () => {}) expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining('Invalid watch source'), expect.anything() - ); - }); + ) + }) it('stopping the watcher (effect)', async () => { - const state = reactive({ count: 0 }); - let dummy; + const state = reactive({ count: 0 }) + let dummy const stop = watchEffect(() => { - dummy = state.count; - }); - expect(dummy).toBe(0); + dummy = state.count + }) + expect(dummy).toBe(0) - stop(); - state.count++; - await nextTick(); + stop() + state.count++ + await nextTick() // should not update - expect(dummy).toBe(0); - }); + expect(dummy).toBe(0) + }) + + // #388 + it('should not call the callback multiple times', () => { + const data = ref([1, 1, 1, 1, 1]) + const data2 = ref([]) + + watchEffect( + () => { + data2.value = data.value.slice(1, 2) + }, + { + flush: 'sync', + } + ) + + expect(data2.value).toMatchObject([1]) + }) it('stopping the watcher (with source)', async () => { - const state = reactive({ count: 0 }); - let dummy; + const state = reactive({ count: 0 }) + let dummy const stop = watch( () => state.count, (count) => { - dummy = count; + dummy = count } - ); + ) - state.count++; - await nextTick(); - expect(dummy).toBe(1); + state.count++ + await nextTick() + expect(dummy).toBe(1) - stop(); - state.count++; - await nextTick(); + stop() + state.count++ + await nextTick() // should not update - expect(dummy).toBe(1); - }); + expect(dummy).toBe(1) + }) it('cleanup registration (effect)', async () => { - const state = reactive({ count: 0 }); - const cleanup = jest.fn(); - let dummy; + const state = reactive({ count: 0 }) + const cleanup = jest.fn() + let dummy const stop = watchEffect((onCleanup) => { - onCleanup(cleanup); - dummy = state.count; - }); - expect(dummy).toBe(0); + onCleanup(cleanup) + dummy = state.count + }) + expect(dummy).toBe(0) - state.count++; - await nextTick(); - expect(cleanup).toHaveBeenCalledTimes(1); - expect(dummy).toBe(1); + state.count++ + await nextTick() + expect(cleanup).toHaveBeenCalledTimes(1) + expect(dummy).toBe(1) - stop(); - expect(cleanup).toHaveBeenCalledTimes(2); - }); + stop() + expect(cleanup).toHaveBeenCalledTimes(2) + }) it('cleanup registration (with source)', async () => { - const count = ref(0); - const cleanup = jest.fn(); - let dummy; + const count = ref(0) + const cleanup = jest.fn() + let dummy const stop = watch(count, (count, prevCount, onCleanup) => { - onCleanup(cleanup); - dummy = count; - }); + onCleanup(cleanup) + dummy = count + }) - count.value++; - await nextTick(); - expect(cleanup).toHaveBeenCalledTimes(0); - expect(dummy).toBe(1); + count.value++ + await nextTick() + expect(cleanup).toHaveBeenCalledTimes(0) + expect(dummy).toBe(1) - count.value++; - await nextTick(); - expect(cleanup).toHaveBeenCalledTimes(1); - expect(dummy).toBe(2); + count.value++ + await nextTick() + expect(cleanup).toHaveBeenCalledTimes(1) + expect(dummy).toBe(2) - stop(); - expect(cleanup).toHaveBeenCalledTimes(2); - }); + stop() + expect(cleanup).toHaveBeenCalledTimes(2) + }) // it('flush timing: post (default)', async () => { // const count = ref(0); @@ -360,25 +385,30 @@ describe('api: watch', () => { ['b', 2], ]), set: new Set([1, 2, 3]), - }); + }) - let dummy; + let dummy watch( () => state, (state) => { - dummy = [state.nested.count, state.array[0], state.map.get('a'), state.set.has(1)]; + dummy = [ + state.nested.count, + state.array[0], + state.map.get('a'), + state.set.has(1), + ] }, { deep: true } - ); + ) - state.nested.count++; - await nextTick(); - expect(dummy).toEqual([1, 1, 1, true]); + state.nested.count++ + await nextTick() + expect(dummy).toEqual([1, 1, 1, true]) // nested array mutation - set(state.array, '0', 2); - await nextTick(); - expect(dummy).toEqual([1, 2, 1, true]); + set(state.array, '0', 2) + await nextTick() + expect(dummy).toEqual([1, 2, 1, true]) // NOT supported by Vue.observe :( // // nested map mutation @@ -390,81 +420,81 @@ describe('api: watch', () => { // state.set.delete(1); // await nextTick(); // expect(dummy).toEqual([1, 2, 2, false]); - }); + }) it('immediate', async () => { - const count = ref(0); - const cb = jest.fn(); - watch(count, cb, { immediate: true }); - expect(cb).toHaveBeenCalledTimes(1); - count.value++; - await nextTick(); - expect(cb).toHaveBeenCalledTimes(2); - }); + const count = ref(0) + const cb = jest.fn() + watch(count, cb, { immediate: true }) + expect(cb).toHaveBeenCalledTimes(1) + count.value++ + await nextTick() + expect(cb).toHaveBeenCalledTimes(2) + }) it('immediate: triggers when initial value is null', async () => { - const state = ref(null); - const spy = jest.fn(); - watch(() => state.value, spy, { immediate: true }); - expect(spy).toHaveBeenCalled(); - }); + const state = ref(null) + const spy = jest.fn() + watch(() => state.value, spy, { immediate: true }) + expect(spy).toHaveBeenCalled() + }) it('immediate: triggers when initial value is undefined', async () => { - const state = ref(); - const spy = jest.fn(); - watch(() => state.value, spy, { immediate: true }); - expect(spy).toHaveBeenCalled(); - state.value = 3; - await nextTick(); - expect(spy).toHaveBeenCalledTimes(2); + const state = ref() + const spy = jest.fn() + watch(() => state.value, spy, { immediate: true }) + expect(spy).toHaveBeenCalled() + state.value = 3 + await nextTick() + expect(spy).toHaveBeenCalledTimes(2) // testing if undefined can trigger the watcher - state.value = undefined; - await nextTick(); - expect(spy).toHaveBeenCalledTimes(3); + state.value = undefined + await nextTick() + expect(spy).toHaveBeenCalledTimes(3) // it shouldn't trigger if the same value is set - state.value = undefined; - await nextTick(); - expect(spy).toHaveBeenCalledTimes(3); - }); + state.value = undefined + await nextTick() + expect(spy).toHaveBeenCalledTimes(3) + }) it('shallow reactive effect', async () => { - const state = shallowReactive({ count: 0 }); - let dummy; + const state = shallowReactive({ count: 0 }) + let dummy watch( () => state.count, () => { - dummy = state.count; + dummy = state.count }, { immediate: true } - ); - expect(dummy).toBe(0); + ) + expect(dummy).toBe(0) - state.count++; - await nextTick(); - expect(dummy).toBe(1); - }); + state.count++ + await nextTick() + expect(dummy).toBe(1) + }) it('shallow reactive object', async () => { - const state = shallowReactive({ a: { count: 0 } }); - let dummy; + const state = shallowReactive({ a: { count: 0 } }) + let dummy watch( () => state.a, () => { - dummy = state.a.count; + dummy = state.a.count }, { immediate: true } - ); - expect(dummy).toBe(0); + ) + expect(dummy).toBe(0) - state.a.count++; - await nextTick(); - expect(dummy).toBe(0); + state.a.count++ + await nextTick() + expect(dummy).toBe(0) - state.a = { count: 5 }; - await nextTick(); + state.a = { count: 5 } + await nextTick() - expect(dummy).toBe(5); - }); + expect(dummy).toBe(5) + }) // it('warn immediate option when using effect', async () => { // const count = ref(0); @@ -501,4 +531,4 @@ describe('api: watch', () => { // expect(spy).toHaveBeenCalledTimes(1); // expect(warnSpy).toHaveBeenCalledWith(`"deep" option is only respected`); // }); -}); +}) From 9b5b75a92552ce812ba8511bbc650d3265958b7f Mon Sep 17 00:00:00 2001 From: Carlos Rodrigues Date: Fri, 19 Jun 2020 14:41:21 +0100 Subject: [PATCH 2/2] chore: better comment --- src/apis/watch.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apis/watch.ts b/src/apis/watch.ts index cb5a73e2..a4b94633 100644 --- a/src/apis/watch.ts +++ b/src/apis/watch.ts @@ -232,8 +232,9 @@ function createWatcher( if (cb === null) { let running = false const getter = () => { + // preventing the watch callback being call in the same execution if (running) { - return // already running + return } try { running = true