From a0eb39f7f11ccf009cef2b42cc8efb51feedadb2 Mon Sep 17 00:00:00 2001 From: LittleSound <464388324@qq.com> Date: Wed, 22 Jun 2022 23:25:45 +0800 Subject: [PATCH 1/8] tentative --- packages/shared/watchTriggerable/index.ts | 33 +++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 packages/shared/watchTriggerable/index.ts diff --git a/packages/shared/watchTriggerable/index.ts b/packages/shared/watchTriggerable/index.ts new file mode 100644 index 00000000000..b6203087c82 --- /dev/null +++ b/packages/shared/watchTriggerable/index.ts @@ -0,0 +1,33 @@ +import { ref } from 'vue-demi' +import type { WatchIgnorableReturn } from '../watchIgnorable' +import { watchIgnorable } from '../watchIgnorable' +import type { WatchWithFilterOptions } from '../watchWithFilter' + +export interface WatchTriggerableReturn extends WatchIgnorableReturn { + trigger: () => void +} + +export function watchTriggerable = false>( + source: any, + cb: any, + options: WatchWithFilterOptions = {}, +): WatchTriggerableReturn { + const triggerCounter = ref(0) + + const trigger = () => triggerCounter.value += 1 + + const _source = Array.isArray(source) + ? [triggerCounter, ...source] + : [triggerCounter, source] + + const _cb = ( + [, ...value]: any[], + [, ...oldValue]: any[], + onCleanup: any, + ) => cb(value, oldValue, onCleanup) + + return { + ...watchIgnorable(_source, _cb, options), + trigger, + } +} From 9e6c6a66666ed9e71705d747e0b8d9a58fd3695d Mon Sep 17 00:00:00 2001 From: LittleSound <464388324@qq.com> Date: Sat, 2 Jul 2022 18:14:15 +0800 Subject: [PATCH 2/8] first version --- .../shared/watchTriggerable/index.test.ts | 85 +++++++++++++++++++ packages/shared/watchTriggerable/index.ts | 63 +++++++++++--- 2 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 packages/shared/watchTriggerable/index.test.ts diff --git a/packages/shared/watchTriggerable/index.test.ts b/packages/shared/watchTriggerable/index.test.ts new file mode 100644 index 00000000000..837c80bd820 --- /dev/null +++ b/packages/shared/watchTriggerable/index.test.ts @@ -0,0 +1,85 @@ +import { nextTick, reactive, ref } from 'vue' +import { watchTriggerable } from '.' +describe('watchTriggerable', () => { + test('this should work', async () => { + const source = ref(0) + const effect = ref(0) + let cleanupCount = -1 + const { trigger } = watchTriggerable(source, (value, oldValue, onCleanup) => { + onCleanup(() => { + cleanupCount = value + }) + expect(value).toBe(source.value) + effect.value = value + }) + + // By default watch will be executed on the next tick + source.value = 1 + expect(effect.value).toBe(0) + await nextTick() + expect(effect.value).toBe(source.value) + expect(cleanupCount).toBe(-1) + + source.value = 2 + expect(cleanupCount).toBe(-1) + await nextTick() + expect(effect.value).toBe(source.value) + expect(cleanupCount).toBe(1) + + // trigger is executed immediately + effect.value = 0 + trigger() + expect(effect.value).toBe(source.value) + expect(cleanupCount).toBe(2) + }) + + test('source array', async () => { + const source1 = ref(0) + const source2 = reactive({ a: 'a' }) + const effect1 = ref(-1) + const effect2 = ref('z') + let cleanupCount = -1 + const { trigger } = watchTriggerable([source1, () => source2.a], ([value1, value2], [old1, old2], onCleanup) => { + onCleanup(() => { + cleanupCount = value1 + }) + expect(value1).toBe(source1.value) + effect1.value = value1 + effect2.value = value2 + }) + + trigger() + expect(effect1.value).toBe(source1.value) + expect(effect2.value).toBe(source2.a) + expect(cleanupCount).toBe(-1) + + source1.value = 1 + source2.a = 'b' + await nextTick() + expect(effect1.value).toBe(source1.value) + expect(effect2.value).toBe(source2.a) + expect(cleanupCount).toBe(0) + }) + + test('source reactive object', async () => { + const source = reactive({ a: 'a' }) + const effect = ref('') + let cleanupCount = 0 + const { trigger } = watchTriggerable(source, (value, old, onCleanup) => { + onCleanup(() => { + cleanupCount += 1 + }) + expect(value).toBe(source) + effect.value = value.a + }) + + trigger() + expect(effect.value).toBe(source.a) + expect(cleanupCount).toBe(0) + + source.a = 'b' + await nextTick() + expect(effect.value).toBe(source.a) + expect(cleanupCount).toBe(1) + }) +}) diff --git a/packages/shared/watchTriggerable/index.ts b/packages/shared/watchTriggerable/index.ts index b6203087c82..2380c4df1ae 100644 --- a/packages/shared/watchTriggerable/index.ts +++ b/packages/shared/watchTriggerable/index.ts @@ -1,4 +1,6 @@ -import { ref } from 'vue-demi' +import type { WatchCallback, WatchSource } from 'vue-demi' +import { isReactive, unref } from 'vue-demi' +import type { MapOldSources, MapSources } from '../utils' import type { WatchIgnorableReturn } from '../watchIgnorable' import { watchIgnorable } from '../watchIgnorable' import type { WatchWithFilterOptions } from '../watchWithFilter' @@ -7,27 +9,66 @@ export interface WatchTriggerableReturn extends WatchIgnorableReturn { trigger: () => void } +export function watchTriggerable[]>, Immediate extends Readonly = false>(sources: [...T], cb: WatchCallback, MapOldSources>, options?: WatchWithFilterOptions): WatchTriggerableReturn +export function watchTriggerable = false>(source: WatchSource, cb: WatchCallback, options?: WatchWithFilterOptions): WatchTriggerableReturn +export function watchTriggerable = false>(source: T, cb: WatchCallback, options?: WatchWithFilterOptions): WatchTriggerableReturn + export function watchTriggerable = false>( source: any, cb: any, options: WatchWithFilterOptions = {}, ): WatchTriggerableReturn { - const triggerCounter = ref(0) + let cleanupFn: () => void - const trigger = () => triggerCounter.value += 1 + function onEffect() { + if (cleanupFn) + cleanupFn() + } - const _source = Array.isArray(source) - ? [triggerCounter, ...source] - : [triggerCounter, source] + function onCleanup(callback: () => void) { + cleanupFn = callback + } const _cb = ( - [, ...value]: any[], - [, ...oldValue]: any[], - onCleanup: any, - ) => cb(value, oldValue, onCleanup) + value: any, + oldValue: any, + // onCleanup: any, + ) => { + onEffect() + cb(value, oldValue, onCleanup) + } + const res = watchIgnorable(source, _cb, options) + const { ignoreUpdates } = res + + const trigger = () => { + ignoreUpdates(() => { + _cb(getWatchSources(source), getOldValue(source)) + }) + } return { - ...watchIgnorable(_source, _cb, options), + ...res, trigger, } } + +function getWatchSources(sources: any) { + if (isReactive(sources)) + return sources + if (Array.isArray(sources)) + return sources.map(item => getOneWatchSource(item)) + return getOneWatchSource(sources) +} + +function getOneWatchSource(source: Readonly>) { + return typeof source === 'function' + ? source() + : unref(source) +} + +// For calls triggered by trigger, the old value is unknown, so it cannot be returned +function getOldValue(source: any) { + return Array.isArray(source) + ? source.map(() => undefined) + : undefined +} From 1d35ca18e9e9b572a11352c8e7b936ee6daf5368 Mon Sep 17 00:00:00 2001 From: LittleSound <464388324@qq.com> Date: Sat, 2 Jul 2022 18:32:20 +0800 Subject: [PATCH 3/8] ci: change 'vue' to 'vue-demi' --- packages/shared/watchTriggerable/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/watchTriggerable/index.test.ts b/packages/shared/watchTriggerable/index.test.ts index 837c80bd820..82403a4527a 100644 --- a/packages/shared/watchTriggerable/index.test.ts +++ b/packages/shared/watchTriggerable/index.test.ts @@ -1,4 +1,4 @@ -import { nextTick, reactive, ref } from 'vue' +import { nextTick, reactive, ref } from 'vue-demi' import { watchTriggerable } from '.' describe('watchTriggerable', () => { test('this should work', async () => { From 6aa9fcba62a7ca8e5165eb74a589db7b45a23461 Mon Sep 17 00:00:00 2001 From: LittleSound <464388324@qq.com> Date: Sat, 2 Jul 2022 23:55:29 +0800 Subject: [PATCH 4/8] docs: demo --- packages/shared/watchTriggerable/demo.vue | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 packages/shared/watchTriggerable/demo.vue diff --git a/packages/shared/watchTriggerable/demo.vue b/packages/shared/watchTriggerable/demo.vue new file mode 100644 index 00000000000..3cf81deef21 --- /dev/null +++ b/packages/shared/watchTriggerable/demo.vue @@ -0,0 +1,49 @@ + + + From 7e2c8f475b4f98983540aebcea274f5e08fc1457 Mon Sep 17 00:00:00 2001 From: LittleSound <464388324@qq.com> Date: Sun, 3 Jul 2022 00:20:54 +0800 Subject: [PATCH 5/8] docs: Add documents --- packages/shared/index.ts | 1 + packages/shared/watchTriggerable/index.md | 57 +++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 packages/shared/watchTriggerable/index.md diff --git a/packages/shared/index.ts b/packages/shared/index.ts index 0d09adc7198..34c951b4937 100644 --- a/packages/shared/index.ts +++ b/packages/shared/index.ts @@ -49,5 +49,6 @@ export * from './watchIgnorable' export * from './watchOnce' export * from './watchPausable' export * from './watchThrottled' +export * from './watchTriggerable' export * from './watchWithFilter' export * from './whenever' diff --git a/packages/shared/watchTriggerable/index.md b/packages/shared/watchTriggerable/index.md new file mode 100644 index 00000000000..06fa6fd8a2c --- /dev/null +++ b/packages/shared/watchTriggerable/index.md @@ -0,0 +1,57 @@ +--- +category: Watch +--- + +# watchTriggerable + +watch that can be triggered manually + +## Usage + +A `watch` wrapper that supports manual triggering of `WatchCallback`, which returns an additional `trigger` to execute a `WatchCallback` immediately. + +```ts +import { watchTriggerable } from '@vueuse/core' +import { nextTick, ref } from 'vue' + +const source = ref(0) + +const { trigger, ignoreUpdates } = watchTriggerable( + source, + v => console.log(`Changed to ${v}!`), +) + +source.value = 'bar' +await nextTick() // logs: Changed to bar! + +// Execution of WatchCallback via `trigger` does not require waiting +trigger() // logs: Changed to bar! +``` + +### `onCleanup` +When you want to manually call a `watch` that uses the onCleanup parameter; simply taking the `WatchCallback` out and calling it doesn't make it easy to implement the `onCleanup` parameter. + +Using `watchTriggerable` will solve this problem. +```ts +import { watchTriggerable } from '@vueuse/core' +import { ref } from 'vue' + +const source = ref(0) + +const { trigger } = watchTriggerable( + source, + async (v, _, onCleanup) => { + let canceled = false + onCleanup(() => canceled = true) + + await new Promise(resolve => setTimeout(resolve, 500)) + if (canceled) + return + + console.log(`The value is "${v}"\n`) + }, +) + +source.value = 1 // no log +trigger() // logs (after 500 ms): The value is "1" +``` From 38060d55f8bcd54ad0f241f47f611943a68d7af9 Mon Sep 17 00:00:00 2001 From: LittleSound <464388324@qq.com> Date: Sun, 3 Jul 2022 00:32:18 +0800 Subject: [PATCH 6/8] chore: additional Notes and Minor Modifications --- packages/shared/watchTriggerable/index.md | 2 +- packages/shared/watchTriggerable/index.ts | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/shared/watchTriggerable/index.md b/packages/shared/watchTriggerable/index.md index 06fa6fd8a2c..958d615008f 100644 --- a/packages/shared/watchTriggerable/index.md +++ b/packages/shared/watchTriggerable/index.md @@ -4,7 +4,7 @@ category: Watch # watchTriggerable -watch that can be triggered manually +Watch that can be triggered manually ## Usage diff --git a/packages/shared/watchTriggerable/index.ts b/packages/shared/watchTriggerable/index.ts index 2380c4df1ae..641d33dc0da 100644 --- a/packages/shared/watchTriggerable/index.ts +++ b/packages/shared/watchTriggerable/index.ts @@ -5,7 +5,11 @@ import type { WatchIgnorableReturn } from '../watchIgnorable' import { watchIgnorable } from '../watchIgnorable' import type { WatchWithFilterOptions } from '../watchWithFilter' +// Watch that can be triggered manually +// A `watch` wrapper that supports manual triggering of `WatchCallback`, which returns an additional `trigger` to execute a `WatchCallback` immediately. + export interface WatchTriggerableReturn extends WatchIgnorableReturn { + /** Execute `WatchCallback` immediately */ trigger: () => void } @@ -18,13 +22,18 @@ export function watchTriggerable = false>( cb: any, options: WatchWithFilterOptions = {}, ): WatchTriggerableReturn { - let cleanupFn: () => void + let cleanupFn: (() => void) | undefined function onEffect() { - if (cleanupFn) - cleanupFn() + if (!cleanupFn) + return + + const fn = cleanupFn + cleanupFn = undefined + fn() } + /** Register the function `cleanupFn` */ function onCleanup(callback: () => void) { cleanupFn = callback } @@ -32,9 +41,10 @@ export function watchTriggerable = false>( const _cb = ( value: any, oldValue: any, - // onCleanup: any, ) => { + // When a new side effect occurs, clean up the previous side effect onEffect() + cb(value, oldValue, onCleanup) } const res = watchIgnorable(source, _cb, options) From 40bc56f6b8966691ef05252c69fffe37442bee1f Mon Sep 17 00:00:00 2001 From: LittleSound <464388324@qq.com> Date: Sun, 3 Jul 2022 00:59:05 +0800 Subject: [PATCH 7/8] feat: trigger should await --- packages/contributors.json | 4 ++++ .../shared/watchTriggerable/index.test.ts | 12 ++++++++++ packages/shared/watchTriggerable/index.ts | 22 ++++++++++++------- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/contributors.json b/packages/contributors.json index 238831518ee..0d37f71b8a3 100644 --- a/packages/contributors.json +++ b/packages/contributors.json @@ -166,6 +166,7 @@ "phaust", "marktnoonan", "dm4t2", + "melishev", "mauriciabad", "mxmvshnvsk", "AldeonMoriak", @@ -185,6 +186,7 @@ "a1xon", "octref", "praburangki", + "preeteshjain", "QiroNT", "ramonakira", "Redemption198", @@ -223,10 +225,12 @@ "monkeywithacupcake", "katsuyaU", "koheing", + "kongmoumou", "laozei6401", "leovoon", "likeswinds", "lxhyl", + "lxnelyclxud", "lzdFeiFei", "meteorlxy", "odex21", diff --git a/packages/shared/watchTriggerable/index.test.ts b/packages/shared/watchTriggerable/index.test.ts index 82403a4527a..449e7011656 100644 --- a/packages/shared/watchTriggerable/index.test.ts +++ b/packages/shared/watchTriggerable/index.test.ts @@ -82,4 +82,16 @@ describe('watchTriggerable', () => { expect(effect.value).toBe(source.a) expect(cleanupCount).toBe(1) }) + + test('trigger should await', async () => { + const source = ref(1) + const effect = ref(0) + const { trigger } = watchTriggerable(source, async (value) => { + await new Promise(resolve => setTimeout(resolve, 10)) + effect.value = value + }) + + await trigger() + expect(effect.value).toBe(source.value) + }) }) diff --git a/packages/shared/watchTriggerable/index.ts b/packages/shared/watchTriggerable/index.ts index 641d33dc0da..6caa9a58476 100644 --- a/packages/shared/watchTriggerable/index.ts +++ b/packages/shared/watchTriggerable/index.ts @@ -1,4 +1,4 @@ -import type { WatchCallback, WatchSource } from 'vue-demi' +import type { WatchSource } from 'vue-demi' import { isReactive, unref } from 'vue-demi' import type { MapOldSources, MapSources } from '../utils' import type { WatchIgnorableReturn } from '../watchIgnorable' @@ -8,14 +8,18 @@ import type { WatchWithFilterOptions } from '../watchWithFilter' // Watch that can be triggered manually // A `watch` wrapper that supports manual triggering of `WatchCallback`, which returns an additional `trigger` to execute a `WatchCallback` immediately. -export interface WatchTriggerableReturn extends WatchIgnorableReturn { +export interface WatchTriggerableReturn extends WatchIgnorableReturn { /** Execute `WatchCallback` immediately */ - trigger: () => void + trigger: () => FnReturnT } -export function watchTriggerable[]>, Immediate extends Readonly = false>(sources: [...T], cb: WatchCallback, MapOldSources>, options?: WatchWithFilterOptions): WatchTriggerableReturn -export function watchTriggerable = false>(source: WatchSource, cb: WatchCallback, options?: WatchWithFilterOptions): WatchTriggerableReturn -export function watchTriggerable = false>(source: T, cb: WatchCallback, options?: WatchWithFilterOptions): WatchTriggerableReturn +type OnCleanup = (cleanupFn: () => void) => void + +export type WatchTriggerableCallback = (value: V, oldValue: OV, onCleanup: OnCleanup) => R + +export function watchTriggerable[]>, FnReturnT>(sources: [...T], cb: WatchTriggerableCallback, MapOldSources, FnReturnT>, options?: WatchWithFilterOptions): WatchTriggerableReturn +export function watchTriggerable(source: WatchSource, cb: WatchTriggerableCallback, options?: WatchWithFilterOptions): WatchTriggerableReturn +export function watchTriggerable(source: T, cb: WatchTriggerableCallback, options?: WatchWithFilterOptions): WatchTriggerableReturn export function watchTriggerable = false>( source: any, @@ -45,15 +49,17 @@ export function watchTriggerable = false>( // When a new side effect occurs, clean up the previous side effect onEffect() - cb(value, oldValue, onCleanup) + return cb(value, oldValue, onCleanup) } const res = watchIgnorable(source, _cb, options) const { ignoreUpdates } = res const trigger = () => { + let res: any ignoreUpdates(() => { - _cb(getWatchSources(source), getOldValue(source)) + res = _cb(getWatchSources(source), getOldValue(source)) }) + return res } return { From 08bf3352b081336dfdd0c4031669b4b269f0c2ba Mon Sep 17 00:00:00 2001 From: LittleSound <464388324@qq.com> Date: Sun, 3 Jul 2022 01:06:37 +0800 Subject: [PATCH 8/8] docs: add await --- packages/shared/watchTriggerable/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/watchTriggerable/index.md b/packages/shared/watchTriggerable/index.md index 958d615008f..b8d401af476 100644 --- a/packages/shared/watchTriggerable/index.md +++ b/packages/shared/watchTriggerable/index.md @@ -53,5 +53,5 @@ const { trigger } = watchTriggerable( ) source.value = 1 // no log -trigger() // logs (after 500 ms): The value is "1" +await trigger() // logs (after 500 ms): The value is "1" ```