Skip to content

Commit

Permalink
refactor(watch): adjsut watch API behavior
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `watch` behavior has been adjusted.

    - When using the `watch(source, callback, options?)` signature, the
      callback now fires lazily by default (consistent with 2.x
      behavior).

      Note that the `watch(effect, options?)` signature is still eager,
      since it must invoke the `effect` immediately to collect
      dependencies.

    - The `lazy` option has been replaced by the opposite `immediate`
      option, which defaults to `false`. (It's ignored when using the
      effect signature)

    - Due to the above changes, the `watch` option in Options API now
      behaves exactly the same as 2.x.

    - When using the effect signature or `{ immediate: true }`, the
      intital execution is now performed synchronously instead of
      deferred until the component is mounted. This is necessary for
      certain use cases to work properly with `async setup()` and
      Suspense.

      The side effect of this is the immediate watcher invocation will
      no longer have access to the mounted DOM. However, the watcher can
      be initiated inside `onMounted` to retain previous behavior.
  • Loading branch information
yyx990803 committed Feb 18, 2020
1 parent d9d63f2 commit 9571ede
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 143 deletions.
40 changes: 14 additions & 26 deletions packages/runtime-core/__tests__/apiOptions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,30 +149,24 @@ describe('api: options', () => {

function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
expect(spy).toHaveReturnedWith(ctx)
}

assertCall(spyA, 0, [1, undefined])
assertCall(spyB, 0, [2, undefined])
assertCall(spyC, 0, [{ qux: 3 }, undefined])
expect(spyA).toHaveReturnedWith(ctx)
expect(spyB).toHaveReturnedWith(ctx)
expect(spyC).toHaveReturnedWith(ctx)

ctx.foo++
await nextTick()
expect(spyA).toHaveBeenCalledTimes(2)
assertCall(spyA, 1, [2, 1])
expect(spyA).toHaveBeenCalledTimes(1)
assertCall(spyA, 0, [2, 1])

ctx.bar++
await nextTick()
expect(spyB).toHaveBeenCalledTimes(2)
assertCall(spyB, 1, [3, 2])
expect(spyB).toHaveBeenCalledTimes(1)
assertCall(spyB, 0, [3, 2])

ctx.baz.qux++
await nextTick()
expect(spyC).toHaveBeenCalledTimes(2)
expect(spyC).toHaveBeenCalledTimes(1)
// new and old objects have same identity
assertCall(spyC, 1, [{ qux: 4 }, { qux: 4 }])
assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
})

test('watch array', async () => {
Expand Down Expand Up @@ -218,30 +212,24 @@ describe('api: options', () => {

function assertCall(spy: jest.Mock, callIndex: number, args: any[]) {
expect(spy.mock.calls[callIndex].slice(0, 2)).toMatchObject(args)
expect(spy).toHaveReturnedWith(ctx)
}

assertCall(spyA, 0, [1, undefined])
assertCall(spyB, 0, [2, undefined])
assertCall(spyC, 0, [{ qux: 3 }, undefined])
expect(spyA).toHaveReturnedWith(ctx)
expect(spyB).toHaveReturnedWith(ctx)
expect(spyC).toHaveReturnedWith(ctx)

ctx.foo++
await nextTick()
expect(spyA).toHaveBeenCalledTimes(2)
assertCall(spyA, 1, [2, 1])
expect(spyA).toHaveBeenCalledTimes(1)
assertCall(spyA, 0, [2, 1])

ctx.bar++
await nextTick()
expect(spyB).toHaveBeenCalledTimes(2)
assertCall(spyB, 1, [3, 2])
expect(spyB).toHaveBeenCalledTimes(1)
assertCall(spyB, 0, [3, 2])

ctx.baz.qux++
await nextTick()
expect(spyC).toHaveBeenCalledTimes(2)
expect(spyC).toHaveBeenCalledTimes(1)
// new and old objects have same identity
assertCall(spyC, 1, [{ qux: 4 }, { qux: 4 }])
assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }])
})

test('provide/inject', () => {
Expand Down
112 changes: 46 additions & 66 deletions packages/runtime-core/__tests__/apiWatch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,19 @@ import { mockWarn } from '@vue/shared'
describe('api: watch', () => {
mockWarn()

it('basic usage', async () => {
it('watch(effect)', async () => {
const state = reactive({ count: 0 })
let dummy
watch(() => {
dummy = state.count
})
await nextTick()
expect(dummy).toBe(0)

state.count++
await nextTick()
expect(dummy).toBe(1)
})

it('triggers when initial value is null', async () => {
const state = ref(null)
const spy = jest.fn()
watch(() => state.value, spy)
await nextTick()
expect(spy).toHaveBeenCalled()
})

it('triggers when initial value is undefined', async () => {
const state = ref()
const spy = jest.fn()
watch(() => state.value, spy)
await nextTick()
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)
// it shouldn't trigger if the same value is set
state.value = undefined
await nextTick()
expect(spy).toHaveBeenCalledTimes(3)
})

it('watching single source: getter', async () => {
const state = reactive({ count: 0 })
let dummy
Expand All @@ -68,9 +40,6 @@ describe('api: watch', () => {
}
}
)
await nextTick()
expect(dummy).toMatchObject([0, undefined])

state.count++
await nextTick()
expect(dummy).toMatchObject([1, 0])
Expand All @@ -87,9 +56,6 @@ describe('api: watch', () => {
prevCount + 1
}
})
await nextTick()
expect(dummy).toMatchObject([0, undefined])

count.value++
await nextTick()
expect(dummy).toMatchObject([1, 0])
Expand All @@ -107,9 +73,6 @@ describe('api: watch', () => {
prevCount + 1
}
})
await nextTick()
expect(dummy).toMatchObject([1, undefined])

count.value++
await nextTick()
expect(dummy).toMatchObject([2, 1])
Expand All @@ -127,8 +90,6 @@ describe('api: watch', () => {
vals.concat(1)
oldVals.concat(1)
})
await nextTick()
expect(dummy).toMatchObject([[1, 1, 2], []])

state.count++
count.value++
Expand All @@ -149,8 +110,6 @@ describe('api: watch', () => {
count + 1
oldStatus === true
})
await nextTick()
expect(dummy).toMatchObject([[1, false], []])

state.count++
status.value = true
Expand All @@ -164,7 +123,6 @@ describe('api: watch', () => {
const stop = watch(() => {
dummy = state.count
})
await nextTick()
expect(dummy).toBe(0)

stop()
Expand All @@ -174,15 +132,14 @@ describe('api: watch', () => {
expect(dummy).toBe(0)
})

it('cleanup registration (basic)', async () => {
it('cleanup registration (effect)', async () => {
const state = reactive({ count: 0 })
const cleanup = jest.fn()
let dummy
const stop = watch(onCleanup => {
onCleanup(cleanup)
dummy = state.count
})
await nextTick()
expect(dummy).toBe(0)

state.count++
Expand All @@ -202,22 +159,30 @@ describe('api: watch', () => {
onCleanup(cleanup)
dummy = count
})

count.value++
await nextTick()
expect(dummy).toBe(0)
expect(cleanup).toHaveBeenCalledTimes(0)
expect(dummy).toBe(1)

count.value++
await nextTick()
expect(cleanup).toHaveBeenCalledTimes(1)
expect(dummy).toBe(1)
expect(dummy).toBe(2)

stop()
expect(cleanup).toHaveBeenCalledTimes(2)
})

it('flush timing: post', async () => {
it('flush timing: post (default)', async () => {
const count = ref(0)
let callCount = 0
const assertion = jest.fn(count => {
expect(serializeInner(root)).toBe(`${count}`)
callCount++
// on mount, the watcher callback should be called before DOM render
// on update, should be called after the count is updated
const expectedDOM = callCount === 1 ? `` : `${count}`
expect(serializeInner(root)).toBe(expectedDOM)
})

const Comp = {
Expand All @@ -230,7 +195,6 @@ describe('api: watch', () => {
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
await nextTick()
expect(assertion).toHaveBeenCalledTimes(1)

count.value++
Expand Down Expand Up @@ -270,7 +234,6 @@ describe('api: watch', () => {
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
await nextTick()
expect(assertion).toHaveBeenCalledTimes(1)

count.value++
Expand Down Expand Up @@ -313,7 +276,6 @@ describe('api: watch', () => {
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
await nextTick()
expect(assertion).toHaveBeenCalledTimes(1)

count.value++
Expand Down Expand Up @@ -346,9 +308,6 @@ describe('api: watch', () => {
{ deep: true }
)

await nextTick()
expect(dummy).toEqual([0, 1, 1, true])

state.nested.count++
await nextTick()
expect(dummy).toEqual([1, 1, 1, true])
Expand All @@ -369,32 +328,53 @@ describe('api: watch', () => {
expect(dummy).toEqual([1, 2, 2, false])
})

it('lazy', async () => {
it('immediate', async () => {
const count = ref(0)
const cb = jest.fn()
watch(count, cb, { lazy: true })
await nextTick()
expect(cb).not.toHaveBeenCalled()
watch(count, cb, { immediate: true })
expect(cb).toHaveBeenCalledTimes(1)
count.value++
await nextTick()
expect(cb).toHaveBeenCalled()
expect(cb).toHaveBeenCalledTimes(2)
})

it('ignore lazy option when using simple callback', async () => {
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()
})

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)
// testing if undefined can trigger the watcher
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)
})

it('warn immediate option when using effect signature', async () => {
const count = ref(0)
let dummy
// @ts-ignore
watch(
() => {
dummy = count.value
},
{ lazy: true }
{ immediate: false }
)
expect(dummy).toBeUndefined()
expect(`lazy option is only respected`).toHaveBeenWarned()

await nextTick()
expect(dummy).toBe(0)
expect(`"immediate" option is only respected`).toHaveBeenWarned()

count.value++
await nextTick()
Expand Down
Loading

0 comments on commit 9571ede

Please sign in to comment.