feat(runtime-core): add watchEffect API

BREAKING CHANGE: replae `watch(fn, options?)` with `watchEffect`

    The `watch(fn, options?)` signature has been replaced by the new
    `watchEffect` API, which has the same usage and behavior. `watch`
    now only supports the `watch(source, cb, options?)` signautre.
This commit is contained in:
Evan You 2020-02-22 08:19:10 +01:00
parent b36a76fe23
commit 99a2e18c97
8 changed files with 77 additions and 31 deletions

View File

@ -6,7 +6,7 @@ import {
render,
serializeInner,
nextTick,
watch,
watchEffect,
defineComponent,
triggerEvent,
TestElement
@ -55,7 +55,7 @@ describe('api: setup context', () => {
const Child = defineComponent({
setup(props: { count: number }) {
watch(() => {
watchEffect(() => {
dummy = props.count
})
return () => h('div', props.count)
@ -88,7 +88,7 @@ describe('api: setup context', () => {
},
setup(props) {
watch(() => {
watchEffect(() => {
dummy = props.count
})
return () => h('div', props.count)

View File

@ -1,4 +1,12 @@
import { watch, reactive, computed, nextTick, ref, h } from '../src/index'
import {
watch,
watchEffect,
reactive,
computed,
nextTick,
ref,
h
} from '../src/index'
import { render, nodeOps, serializeInner } from '@vue/runtime-test'
import {
ITERATE_KEY,
@ -13,10 +21,10 @@ import { mockWarn } from '@vue/shared'
describe('api: watch', () => {
mockWarn()
it('watch(effect)', async () => {
it('effect', async () => {
const state = reactive({ count: 0 })
let dummy
watch(() => {
watchEffect(() => {
dummy = state.count
})
expect(dummy).toBe(0)
@ -117,10 +125,10 @@ describe('api: watch', () => {
expect(dummy).toMatchObject([[2, true], [1, false]])
})
it('stopping the watcher', async () => {
it('stopping the watcher (effect)', async () => {
const state = reactive({ count: 0 })
let dummy
const stop = watch(() => {
const stop = watchEffect(() => {
dummy = state.count
})
expect(dummy).toBe(0)
@ -132,11 +140,32 @@ describe('api: watch', () => {
expect(dummy).toBe(0)
})
it('stopping the watcher (with source)', async () => {
const state = reactive({ count: 0 })
let dummy
const stop = watch(
() => state.count,
count => {
dummy = count
}
)
state.count++
await nextTick()
expect(dummy).toBe(1)
stop()
state.count++
await nextTick()
// should not update
expect(dummy).toBe(1)
})
it('cleanup registration (effect)', async () => {
const state = reactive({ count: 0 })
const cleanup = jest.fn()
let dummy
const stop = watch(onCleanup => {
const stop = watchEffect(onCleanup => {
onCleanup(cleanup)
dummy = state.count
})
@ -187,7 +216,7 @@ describe('api: watch', () => {
const Comp = {
setup() {
watch(() => {
watchEffect(() => {
assertion(count.value)
})
return () => count.value
@ -221,7 +250,7 @@ describe('api: watch', () => {
const Comp = {
setup() {
watch(
watchEffect(
() => {
assertion(count.value, count2.value)
},
@ -263,7 +292,7 @@ describe('api: watch', () => {
const Comp = {
setup() {
watch(
watchEffect(
() => {
assertion(count.value)
},
@ -363,14 +392,14 @@ describe('api: watch', () => {
expect(spy).toHaveBeenCalledTimes(3)
})
it('warn immediate option when using effect signature', async () => {
it('warn immediate option when using effect', async () => {
const count = ref(0)
let dummy
// @ts-ignore
watch(
watchEffect(
() => {
dummy = count.value
},
// @ts-ignore
{ immediate: false }
)
expect(dummy).toBe(0)
@ -388,7 +417,7 @@ describe('api: watch', () => {
events.push(e)
})
const obj = reactive({ foo: 1, bar: 2 })
watch(
watchEffect(
() => {
dummy = [obj.foo, 'bar' in obj, Object.keys(obj)]
},
@ -423,7 +452,7 @@ describe('api: watch', () => {
events.push(e)
})
const obj = reactive({ foo: 1 })
watch(
watchEffect(
() => {
dummy = obj.foo
},

View File

@ -9,6 +9,7 @@ import {
nextTick,
onMounted,
watch,
watchEffect,
onUnmounted,
onErrorCaptured
} from '@vue/runtime-test'
@ -163,7 +164,7 @@ describe('Suspense', () => {
// extra tick needed for Node 12+
deps.push(p.then(() => Promise.resolve()))
watch(() => {
watchEffect(() => {
calls.push('immediate effect')
})
@ -265,7 +266,7 @@ describe('Suspense', () => {
const p = new Promise(r => setTimeout(r, 1))
deps.push(p)
watch(() => {
watchEffect(() => {
calls.push('immediate effect')
})

View File

@ -7,7 +7,8 @@ import {
watch,
ref,
nextTick,
defineComponent
defineComponent,
watchEffect
} from '@vue/runtime-test'
import { setErrorRecovery } from '../src/errorHandling'
import { mockWarn } from '@vue/shared'
@ -241,7 +242,7 @@ describe('error handling', () => {
expect(fn).toHaveBeenCalledWith(err, 'ref function')
})
test('in watch (effect)', () => {
test('in effect', () => {
const err = new Error('foo')
const fn = jest.fn()
@ -257,7 +258,7 @@ describe('error handling', () => {
const Child = {
setup() {
watch(() => {
watchEffect(() => {
throw err
})
return () => null
@ -268,7 +269,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 +299,7 @@ describe('error handling', () => {
expect(fn).toHaveBeenCalledWith(err, 'watcher getter')
})
test('in watch (callback)', async () => {
test('in watch callback', async () => {
const err = new Error('foo')
const fn = jest.fn()
@ -332,7 +333,7 @@ describe('error handling', () => {
expect(fn).toHaveBeenCalledWith(err, 'watcher callback')
})
test('in watch cleanup', async () => {
test('in effect cleanup', async () => {
const err = new Error('foo')
const count = ref(0)
const fn = jest.fn()
@ -349,7 +350,7 @@ describe('error handling', () => {
const Child = {
setup() {
watch(onCleanup => {
watchEffect(onCleanup => {
count.value
onCleanup(() => {
throw err

View File

@ -71,6 +71,14 @@ export type StopHandle = () => void
const invoke = (fn: Function) => fn()
// Simple effect.
export function watchEffect(
effect: WatchEffect,
options?: BaseWatchOptions
): StopHandle {
return doWatch(effect, null, options)
}
// initial value for watchers to trigger on undefined initial values
const INITIAL_WATCHER_VALUE = {}
@ -110,6 +118,13 @@ export function watch<T = any>(
// watch(source, cb)
return doWatch(effectOrSource, cbOrOptions, options)
} else {
// TODO remove this in the next release
__DEV__ &&
warn(
`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
`Use \`watchEffect(fn, options?)\` instead. \`watch\` will only ` +
`support \`watch(source, cb, options?) signature in the next release.`
)
// watch(effect)
return doWatch(effectOrSource, null, cbOrOptions)
}

View File

@ -17,7 +17,7 @@ export {
markNonReactive
} from '@vue/reactivity'
export { computed } from './apiComputed'
export { watch } from './apiWatch'
export { watch, watchEffect } from './apiWatch'
export {
onBeforeMount,
onMounted,

View File

@ -22,7 +22,7 @@
</div>
<script>
const { createApp, ref, watch } = Vue
const { createApp, ref, watchEffect } = Vue
const API_URL = `https://api.github.com/repos/vuejs/vue-next/commits?per_page=3&sha=`
const truncate = v => {
@ -37,7 +37,7 @@ createApp({
const currentBranch = ref('master')
const commits = ref(null)
watch(() => {
watchEffect(() => {
fetch(`${API_URL}${currentBranch.value}`)
.then(res => res.json())
.then(data => {

View File

@ -53,7 +53,7 @@
</div>
<script>
const { createApp, reactive, computed, watch, onMounted, onUnmounted } = Vue
const { createApp, reactive, computed, watchEffect, onMounted, onUnmounted } = Vue
const STORAGE_KEY = 'todos-vuejs-3.x'
const todoStorage = {
@ -119,7 +119,7 @@ createApp({
})
})
watch(() => {
watchEffect(() => {
todoStorage.save(state.todos)
})