refactor(watch): adjsut watch API behavior
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.
			
			
This commit is contained in:
		
							parent
							
								
									d9d63f21b1
								
							
						
					
					
						commit
						9571ede84b
					
				@ -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 () => {
 | 
			
		||||
@ -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', () => {
 | 
			
		||||
 | 
			
		||||
@ -13,13 +13,12 @@ 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++
 | 
			
		||||
@ -27,33 +26,6 @@ describe('api: watch', () => {
 | 
			
		||||
    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
 | 
			
		||||
@ -68,9 +40,6 @@ describe('api: watch', () => {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(dummy).toMatchObject([0, undefined])
 | 
			
		||||
 | 
			
		||||
    state.count++
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(dummy).toMatchObject([1, 0])
 | 
			
		||||
@ -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])
 | 
			
		||||
@ -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])
 | 
			
		||||
@ -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++
 | 
			
		||||
@ -149,8 +110,6 @@ describe('api: watch', () => {
 | 
			
		||||
      count + 1
 | 
			
		||||
      oldStatus === true
 | 
			
		||||
    })
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(dummy).toMatchObject([[1, false], []])
 | 
			
		||||
 | 
			
		||||
    state.count++
 | 
			
		||||
    status.value = true
 | 
			
		||||
@ -164,7 +123,6 @@ describe('api: watch', () => {
 | 
			
		||||
    const stop = watch(() => {
 | 
			
		||||
      dummy = state.count
 | 
			
		||||
    })
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(dummy).toBe(0)
 | 
			
		||||
 | 
			
		||||
    stop()
 | 
			
		||||
@ -174,7 +132,7 @@ 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
 | 
			
		||||
@ -182,7 +140,6 @@ describe('api: watch', () => {
 | 
			
		||||
      onCleanup(cleanup)
 | 
			
		||||
      dummy = state.count
 | 
			
		||||
    })
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(dummy).toBe(0)
 | 
			
		||||
 | 
			
		||||
    state.count++
 | 
			
		||||
@ -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 = {
 | 
			
		||||
@ -230,7 +195,6 @@ describe('api: watch', () => {
 | 
			
		||||
    }
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    render(h(Comp), root)
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(assertion).toHaveBeenCalledTimes(1)
 | 
			
		||||
 | 
			
		||||
    count.value++
 | 
			
		||||
@ -270,7 +234,6 @@ describe('api: watch', () => {
 | 
			
		||||
    }
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    render(h(Comp), root)
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(assertion).toHaveBeenCalledTimes(1)
 | 
			
		||||
 | 
			
		||||
    count.value++
 | 
			
		||||
@ -313,7 +276,6 @@ describe('api: watch', () => {
 | 
			
		||||
    }
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    render(h(Comp), root)
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(assertion).toHaveBeenCalledTimes(1)
 | 
			
		||||
 | 
			
		||||
    count.value++
 | 
			
		||||
@ -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])
 | 
			
		||||
@ -369,18 +328,42 @@ 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
 | 
			
		||||
@ -388,13 +371,10 @@ describe('api: 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()
 | 
			
		||||
 | 
			
		||||
@ -164,8 +164,14 @@ describe('Suspense', () => {
 | 
			
		||||
        deps.push(p.then(() => Promise.resolve()))
 | 
			
		||||
 | 
			
		||||
        watch(() => {
 | 
			
		||||
          calls.push('immediate effect')
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        const count = ref(0)
 | 
			
		||||
        watch(count, v => {
 | 
			
		||||
          calls.push('watch callback')
 | 
			
		||||
        })
 | 
			
		||||
        count.value++ // trigger the watcher now
 | 
			
		||||
 | 
			
		||||
        onMounted(() => {
 | 
			
		||||
          calls.push('mounted')
 | 
			
		||||
@ -193,18 +199,24 @@ describe('Suspense', () => {
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    render(h(Comp), root)
 | 
			
		||||
    expect(serializeInner(root)).toBe(`<div>fallback</div>`)
 | 
			
		||||
    expect(calls).toEqual([])
 | 
			
		||||
    expect(calls).toEqual([`immediate effect`])
 | 
			
		||||
 | 
			
		||||
    await Promise.all(deps)
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe(`<div>async</div>`)
 | 
			
		||||
    expect(calls).toEqual([`watch callback`, `mounted`])
 | 
			
		||||
    expect(calls).toEqual([`immediate effect`, `watch callback`, `mounted`])
 | 
			
		||||
 | 
			
		||||
    // effects inside an already resolved suspense should happen at normal timing
 | 
			
		||||
    toggle.value = false
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe(`<!---->`)
 | 
			
		||||
    expect(calls).toEqual([`watch callback`, `mounted`, 'unmounted'])
 | 
			
		||||
    expect(calls).toEqual([
 | 
			
		||||
      `immediate effect`,
 | 
			
		||||
      `watch callback`,
 | 
			
		||||
      `mounted`,
 | 
			
		||||
      'unmounted'
 | 
			
		||||
    ])
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('content update before suspense resolve', async () => {
 | 
			
		||||
@ -254,8 +266,14 @@ describe('Suspense', () => {
 | 
			
		||||
        deps.push(p)
 | 
			
		||||
 | 
			
		||||
        watch(() => {
 | 
			
		||||
          calls.push('immediate effect')
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        const count = ref(0)
 | 
			
		||||
        watch(count, () => {
 | 
			
		||||
          calls.push('watch callback')
 | 
			
		||||
        })
 | 
			
		||||
        count.value++ // trigger the watcher now
 | 
			
		||||
 | 
			
		||||
        onMounted(() => {
 | 
			
		||||
          calls.push('mounted')
 | 
			
		||||
@ -283,7 +301,7 @@ describe('Suspense', () => {
 | 
			
		||||
    const root = nodeOps.createElement('div')
 | 
			
		||||
    render(h(Comp), root)
 | 
			
		||||
    expect(serializeInner(root)).toBe(`<div>fallback</div>`)
 | 
			
		||||
    expect(calls).toEqual([])
 | 
			
		||||
    expect(calls).toEqual(['immediate effect'])
 | 
			
		||||
 | 
			
		||||
    // remove the async dep before it's resolved
 | 
			
		||||
    toggle.value = false
 | 
			
		||||
@ -294,8 +312,8 @@ describe('Suspense', () => {
 | 
			
		||||
    await Promise.all(deps)
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(serializeInner(root)).toBe(`<!---->`)
 | 
			
		||||
    // should discard effects
 | 
			
		||||
    expect(calls).toEqual([])
 | 
			
		||||
    // should discard effects (except for immediate ones)
 | 
			
		||||
    expect(calls).toEqual(['immediate effect'])
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('unmount suspense after resolve', async () => {
 | 
			
		||||
 | 
			
		||||
@ -241,7 +241,7 @@ describe('error handling', () => {
 | 
			
		||||
    expect(fn).toHaveBeenCalledWith(err, 'ref function')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('in watch (simple usage)', () => {
 | 
			
		||||
  test('in watch (effect)', () => {
 | 
			
		||||
    const err = new Error('foo')
 | 
			
		||||
    const fn = jest.fn()
 | 
			
		||||
 | 
			
		||||
@ -268,7 +268,7 @@ describe('error handling', () => {
 | 
			
		||||
    expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('in watch getter', () => {
 | 
			
		||||
  test('in watch (getter)', () => {
 | 
			
		||||
    const err = new Error('foo')
 | 
			
		||||
    const fn = jest.fn()
 | 
			
		||||
 | 
			
		||||
@ -298,7 +298,7 @@ describe('error handling', () => {
 | 
			
		||||
    expect(fn).toHaveBeenCalledWith(err, 'watcher getter')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('in watch callback', () => {
 | 
			
		||||
  test('in watch (callback)', async () => {
 | 
			
		||||
    const err = new Error('foo')
 | 
			
		||||
    const fn = jest.fn()
 | 
			
		||||
 | 
			
		||||
@ -312,10 +312,11 @@ describe('error handling', () => {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const count = ref(0)
 | 
			
		||||
    const Child = {
 | 
			
		||||
      setup() {
 | 
			
		||||
        watch(
 | 
			
		||||
          () => 1,
 | 
			
		||||
          () => count.value,
 | 
			
		||||
          () => {
 | 
			
		||||
            throw err
 | 
			
		||||
          }
 | 
			
		||||
@ -325,6 +326,9 @@ describe('error handling', () => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(h(Comp), nodeOps.createElement('div'))
 | 
			
		||||
 | 
			
		||||
    count.value++
 | 
			
		||||
    await nextTick()
 | 
			
		||||
    expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -47,22 +47,25 @@ type MapSources<T> = {
 | 
			
		||||
  [K in keyof T]: T[K] extends WatchSource<infer V> ? V : never
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type MapOldSources<T, Lazy> = {
 | 
			
		||||
type MapOldSources<T, Immediate> = {
 | 
			
		||||
  [K in keyof T]: T[K] extends WatchSource<infer V>
 | 
			
		||||
    ? Lazy extends true ? V : (V | undefined)
 | 
			
		||||
    ? Immediate extends true ? (V | undefined) : V
 | 
			
		||||
    : never
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CleanupRegistrator = (invalidate: () => void) => void
 | 
			
		||||
 | 
			
		||||
export interface WatchOptions<Lazy = boolean> {
 | 
			
		||||
  lazy?: Lazy
 | 
			
		||||
export interface BaseWatchOptions {
 | 
			
		||||
  flush?: 'pre' | 'post' | 'sync'
 | 
			
		||||
  deep?: boolean
 | 
			
		||||
  onTrack?: ReactiveEffectOptions['onTrack']
 | 
			
		||||
  onTrigger?: ReactiveEffectOptions['onTrigger']
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface WatchOptions<Immediate = boolean> extends BaseWatchOptions {
 | 
			
		||||
  immediate?: Immediate
 | 
			
		||||
  deep?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type StopHandle = () => void
 | 
			
		||||
 | 
			
		||||
const invoke = (fn: Function) => fn()
 | 
			
		||||
@ -73,14 +76,14 @@ const INITIAL_WATCHER_VALUE = {}
 | 
			
		||||
// overload #1: simple effect
 | 
			
		||||
export function watch(
 | 
			
		||||
  effect: WatchEffect,
 | 
			
		||||
  options?: WatchOptions<false>
 | 
			
		||||
  options?: BaseWatchOptions
 | 
			
		||||
): StopHandle
 | 
			
		||||
 | 
			
		||||
// overload #2: single source + cb
 | 
			
		||||
export function watch<T, Lazy extends Readonly<boolean> = false>(
 | 
			
		||||
export function watch<T, Immediate extends Readonly<boolean> = false>(
 | 
			
		||||
  source: WatchSource<T>,
 | 
			
		||||
  cb: WatchCallback<T, Lazy extends true ? T : (T | undefined)>,
 | 
			
		||||
  options?: WatchOptions<Lazy>
 | 
			
		||||
  cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
 | 
			
		||||
  options?: WatchOptions<Immediate>
 | 
			
		||||
): StopHandle
 | 
			
		||||
 | 
			
		||||
// overload #3: array of multiple sources + cb
 | 
			
		||||
@ -89,11 +92,11 @@ export function watch<T, Lazy extends Readonly<boolean> = false>(
 | 
			
		||||
// of all possible value types.
 | 
			
		||||
export function watch<
 | 
			
		||||
  T extends Readonly<WatchSource<unknown>[]>,
 | 
			
		||||
  Lazy extends Readonly<boolean> = false
 | 
			
		||||
  Immediate extends Readonly<boolean> = false
 | 
			
		||||
>(
 | 
			
		||||
  sources: T,
 | 
			
		||||
  cb: WatchCallback<MapSources<T>, MapOldSources<T, Lazy>>,
 | 
			
		||||
  options?: WatchOptions<Lazy>
 | 
			
		||||
  cb: WatchCallback<MapSources<T>, MapOldSources<T, Immediate>>,
 | 
			
		||||
  options?: WatchOptions<Immediate>
 | 
			
		||||
): StopHandle
 | 
			
		||||
 | 
			
		||||
// implementation
 | 
			
		||||
@ -102,15 +105,11 @@ export function watch<T = any>(
 | 
			
		||||
  cbOrOptions?: WatchCallback<T> | WatchOptions,
 | 
			
		||||
  options?: WatchOptions
 | 
			
		||||
): StopHandle {
 | 
			
		||||
  if (isInSSRComponentSetup && !(options && options.flush === 'sync')) {
 | 
			
		||||
    // component watchers during SSR are no-op
 | 
			
		||||
    return NOOP
 | 
			
		||||
  } else if (isFunction(cbOrOptions)) {
 | 
			
		||||
    // effect callback as 2nd argument - this is a source watcher
 | 
			
		||||
  if (isFunction(cbOrOptions)) {
 | 
			
		||||
    // watch(source, cb)
 | 
			
		||||
    return doWatch(effectOrSource, cbOrOptions, options)
 | 
			
		||||
  } else {
 | 
			
		||||
    // 2nd argument is either missing or an options object
 | 
			
		||||
    // - this is a simple effect watcher
 | 
			
		||||
    // watch(effect)
 | 
			
		||||
    return doWatch(effectOrSource, null, cbOrOptions)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -118,8 +117,23 @@ export function watch<T = any>(
 | 
			
		||||
function doWatch(
 | 
			
		||||
  source: WatchSource | WatchSource[] | WatchEffect,
 | 
			
		||||
  cb: WatchCallback | null,
 | 
			
		||||
  { lazy, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
 | 
			
		||||
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
 | 
			
		||||
): StopHandle {
 | 
			
		||||
  if (__DEV__ && !cb) {
 | 
			
		||||
    if (immediate !== undefined) {
 | 
			
		||||
      warn(
 | 
			
		||||
        `watch() "immediate" option is only respected when using the ` +
 | 
			
		||||
          `watch(source, callback) signature.`
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    if (deep !== undefined) {
 | 
			
		||||
      warn(
 | 
			
		||||
        `watch() "deep" option is only respected when using the ` +
 | 
			
		||||
          `watch(source, callback) signature.`
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const instance = currentInstance
 | 
			
		||||
  const suspense = currentSuspense
 | 
			
		||||
 | 
			
		||||
@ -168,6 +182,21 @@ function doWatch(
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // in SSR there is no need to setup an actual effect, and it should be noop
 | 
			
		||||
  // unless it's eager
 | 
			
		||||
  if (__NODE_JS__ && isInSSRComponentSetup) {
 | 
			
		||||
    if (!cb) {
 | 
			
		||||
      getter()
 | 
			
		||||
    } else if (immediate) {
 | 
			
		||||
      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
 | 
			
		||||
        getter(),
 | 
			
		||||
        undefined,
 | 
			
		||||
        registerCleanup
 | 
			
		||||
      ])
 | 
			
		||||
    }
 | 
			
		||||
    return NOOP
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
 | 
			
		||||
  const applyCb = cb
 | 
			
		||||
    ? () => {
 | 
			
		||||
@ -219,23 +248,19 @@ function doWatch(
 | 
			
		||||
    scheduler: applyCb ? () => scheduler(applyCb) : scheduler
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (lazy && cb) {
 | 
			
		||||
    oldValue = runner()
 | 
			
		||||
  } else {
 | 
			
		||||
    if (__DEV__ && lazy && !cb) {
 | 
			
		||||
      warn(
 | 
			
		||||
        `watch() lazy option is only respected when using the ` +
 | 
			
		||||
          `watch(getter, callback) signature.`
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  recordInstanceBoundEffect(runner)
 | 
			
		||||
 | 
			
		||||
  // initial run
 | 
			
		||||
  if (applyCb) {
 | 
			
		||||
      scheduler(applyCb)
 | 
			
		||||
    if (immediate) {
 | 
			
		||||
      applyCb()
 | 
			
		||||
    } else {
 | 
			
		||||
      scheduler(runner)
 | 
			
		||||
      oldValue = runner()
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    runner()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  recordInstanceBoundEffect(runner)
 | 
			
		||||
  return () => {
 | 
			
		||||
    stop(runner)
 | 
			
		||||
    if (instance) {
 | 
			
		||||
 | 
			
		||||
@ -135,8 +135,7 @@ const KeepAliveImpl = {
 | 
			
		||||
      ([include, exclude]) => {
 | 
			
		||||
        include && pruneCache(name => matches(include, name))
 | 
			
		||||
        exclude && pruneCache(name => matches(exclude, name))
 | 
			
		||||
      },
 | 
			
		||||
      { lazy: true }
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    onBeforeUnmount(() => {
 | 
			
		||||
 | 
			
		||||
@ -25,22 +25,39 @@ watch([source, source2, source3] as const, (values, oldValues) => {
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// lazy watcher will have consistent types for oldValue.
 | 
			
		||||
watch(source, (value, oldValue) => {
 | 
			
		||||
  expectType<string>(value)
 | 
			
		||||
  expectType<string>(oldValue)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
watch([source, source2, source3], (values, oldValues) => {
 | 
			
		||||
  expectType<(string | number)[]>(values)
 | 
			
		||||
  expectType<(string | number)[]>(oldValues)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// const array
 | 
			
		||||
watch([source, source2, source3] as const, (values, oldValues) => {
 | 
			
		||||
  expectType<Readonly<[string, string, number]>>(values)
 | 
			
		||||
  expectType<Readonly<[string, string, number]>>(oldValues)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// source + immediate: true
 | 
			
		||||
watch(
 | 
			
		||||
  source,
 | 
			
		||||
  (value, oldValue) => {
 | 
			
		||||
    expectType<string>(value)
 | 
			
		||||
    expectType<string>(oldValue)
 | 
			
		||||
    expectType<string | undefined>(oldValue)
 | 
			
		||||
  },
 | 
			
		||||
  { lazy: true }
 | 
			
		||||
  { immediate: true }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  [source, source2, source3],
 | 
			
		||||
  (values, oldValues) => {
 | 
			
		||||
    expectType<(string | number)[]>(values)
 | 
			
		||||
    expectType<(string | number)[]>(oldValues)
 | 
			
		||||
    expectType<(string | number | undefined)[]>(oldValues)
 | 
			
		||||
  },
 | 
			
		||||
  { lazy: true }
 | 
			
		||||
  { immediate: true }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// const array
 | 
			
		||||
@ -48,7 +65,9 @@ watch(
 | 
			
		||||
  [source, source2, source3] as const,
 | 
			
		||||
  (values, oldValues) => {
 | 
			
		||||
    expectType<Readonly<[string, string, number]>>(values)
 | 
			
		||||
    expectType<Readonly<[string, string, number]>>(oldValues)
 | 
			
		||||
    expectType<
 | 
			
		||||
      Readonly<[string | undefined, string | undefined, number | undefined]>
 | 
			
		||||
    >(oldValues)
 | 
			
		||||
  },
 | 
			
		||||
  { lazy: true }
 | 
			
		||||
  { immediate: true }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user