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, render,
serializeInner, serializeInner,
nextTick, nextTick,
watch, watchEffect,
defineComponent, defineComponent,
triggerEvent, triggerEvent,
TestElement TestElement
@ -55,7 +55,7 @@ describe('api: setup context', () => {
const Child = defineComponent({ const Child = defineComponent({
setup(props: { count: number }) { setup(props: { count: number }) {
watch(() => { watchEffect(() => {
dummy = props.count dummy = props.count
}) })
return () => h('div', props.count) return () => h('div', props.count)
@ -88,7 +88,7 @@ describe('api: setup context', () => {
}, },
setup(props) { setup(props) {
watch(() => { watchEffect(() => {
dummy = props.count dummy = props.count
}) })
return () => h('div', 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 { render, nodeOps, serializeInner } from '@vue/runtime-test'
import { import {
ITERATE_KEY, ITERATE_KEY,
@ -13,10 +21,10 @@ import { mockWarn } from '@vue/shared'
describe('api: watch', () => { describe('api: watch', () => {
mockWarn() mockWarn()
it('watch(effect)', async () => { it('effect', async () => {
const state = reactive({ count: 0 }) const state = reactive({ count: 0 })
let dummy let dummy
watch(() => { watchEffect(() => {
dummy = state.count dummy = state.count
}) })
expect(dummy).toBe(0) expect(dummy).toBe(0)
@ -117,10 +125,10 @@ describe('api: watch', () => {
expect(dummy).toMatchObject([[2, true], [1, false]]) expect(dummy).toMatchObject([[2, true], [1, false]])
}) })
it('stopping the watcher', async () => { it('stopping the watcher (effect)', async () => {
const state = reactive({ count: 0 }) const state = reactive({ count: 0 })
let dummy let dummy
const stop = watch(() => { const stop = watchEffect(() => {
dummy = state.count dummy = state.count
}) })
expect(dummy).toBe(0) expect(dummy).toBe(0)
@ -132,11 +140,32 @@ describe('api: watch', () => {
expect(dummy).toBe(0) 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 () => { it('cleanup registration (effect)', async () => {
const state = reactive({ count: 0 }) const state = reactive({ count: 0 })
const cleanup = jest.fn() const cleanup = jest.fn()
let dummy let dummy
const stop = watch(onCleanup => { const stop = watchEffect(onCleanup => {
onCleanup(cleanup) onCleanup(cleanup)
dummy = state.count dummy = state.count
}) })
@ -187,7 +216,7 @@ describe('api: watch', () => {
const Comp = { const Comp = {
setup() { setup() {
watch(() => { watchEffect(() => {
assertion(count.value) assertion(count.value)
}) })
return () => count.value return () => count.value
@ -221,7 +250,7 @@ describe('api: watch', () => {
const Comp = { const Comp = {
setup() { setup() {
watch( watchEffect(
() => { () => {
assertion(count.value, count2.value) assertion(count.value, count2.value)
}, },
@ -263,7 +292,7 @@ describe('api: watch', () => {
const Comp = { const Comp = {
setup() { setup() {
watch( watchEffect(
() => { () => {
assertion(count.value) assertion(count.value)
}, },
@ -363,14 +392,14 @@ describe('api: watch', () => {
expect(spy).toHaveBeenCalledTimes(3) 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) const count = ref(0)
let dummy let dummy
// @ts-ignore watchEffect(
watch(
() => { () => {
dummy = count.value dummy = count.value
}, },
// @ts-ignore
{ immediate: false } { immediate: false }
) )
expect(dummy).toBe(0) expect(dummy).toBe(0)
@ -388,7 +417,7 @@ describe('api: watch', () => {
events.push(e) events.push(e)
}) })
const obj = reactive({ foo: 1, bar: 2 }) const obj = reactive({ foo: 1, bar: 2 })
watch( watchEffect(
() => { () => {
dummy = [obj.foo, 'bar' in obj, Object.keys(obj)] dummy = [obj.foo, 'bar' in obj, Object.keys(obj)]
}, },
@ -423,7 +452,7 @@ describe('api: watch', () => {
events.push(e) events.push(e)
}) })
const obj = reactive({ foo: 1 }) const obj = reactive({ foo: 1 })
watch( watchEffect(
() => { () => {
dummy = obj.foo dummy = obj.foo
}, },

View File

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

View File

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

View File

@ -71,6 +71,14 @@ export type StopHandle = () => void
const invoke = (fn: Function) => fn() 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 // initial value for watchers to trigger on undefined initial values
const INITIAL_WATCHER_VALUE = {} const INITIAL_WATCHER_VALUE = {}
@ -110,6 +118,13 @@ export function watch<T = any>(
// watch(source, cb) // watch(source, cb)
return doWatch(effectOrSource, cbOrOptions, options) return doWatch(effectOrSource, cbOrOptions, options)
} else { } 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) // watch(effect)
return doWatch(effectOrSource, null, cbOrOptions) return doWatch(effectOrSource, null, cbOrOptions)
} }

View File

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

View File

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

View File

@ -53,7 +53,7 @@
</div> </div>
<script> <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 STORAGE_KEY = 'todos-vuejs-3.x'
const todoStorage = { const todoStorage = {
@ -119,7 +119,7 @@ createApp({
}) })
}) })
watch(() => { watchEffect(() => {
todoStorage.save(state.todos) todoStorage.save(state.todos)
}) })