| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- import {
- effect,
- stop,
- isRef,
- Ref,
- ComputedRef,
- ReactiveEffectOptions
- } from '@vue/reactivity'
- import { queueJob } from './scheduler'
- import {
- EMPTY_OBJ,
- isObject,
- isArray,
- isFunction,
- isString,
- hasChanged,
- NOOP,
- remove
- } from '@vue/shared'
- import {
- currentInstance,
- ComponentInternalInstance,
- currentSuspense,
- Data,
- isInSSRComponentSetup,
- recordInstanceBoundEffect
- } from './component'
- import {
- ErrorCodes,
- callWithErrorHandling,
- callWithAsyncErrorHandling
- } from './errorHandling'
- import { onBeforeUnmount } from './apiLifecycle'
- import { queuePostRenderEffect } from './renderer'
- import { warn } from './warning'
- export type WatchEffect = (onCleanup: CleanupRegistrator) => void
- export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
- export type WatchCallback<V = any, OV = any> = (
- value: V,
- oldValue: OV,
- onCleanup: CleanupRegistrator
- ) => any
- type MapSources<T> = {
- [K in keyof T]: T[K] extends WatchSource<infer V> ? V : never
- }
- type MapOldSources<T, Immediate> = {
- [K in keyof T]: T[K] extends WatchSource<infer V>
- ? Immediate extends true ? (V | undefined) : V
- : never
- }
- export type CleanupRegistrator = (invalidate: () => void) => void
- export interface BaseWatchOptions {
- flush?: 'pre' | 'post' | 'sync'
- 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()
- // 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 = {}
- // overload #1: single source + cb
- export function watch<T, Immediate extends Readonly<boolean> = false>(
- source: WatchSource<T>,
- cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
- options?: WatchOptions<Immediate>
- ): StopHandle
- // overload #2: array of multiple sources + cb
- // Readonly constraint helps the callback to correctly infer value types based
- // on position in the source array. Otherwise the values will get a union type
- // of all possible value types.
- export function watch<
- T extends Readonly<WatchSource<unknown>[]>,
- Immediate extends Readonly<boolean> = false
- >(
- sources: T,
- cb: WatchCallback<MapSources<T>, MapOldSources<T, Immediate>>,
- options?: WatchOptions<Immediate>
- ): StopHandle
- // implementation
- export function watch<T = any>(
- source: WatchSource<T> | WatchSource<T>[],
- cb: WatchCallback<T>,
- options?: WatchOptions
- ): StopHandle {
- if (__DEV__ && !isFunction(cb)) {
- warn(
- `\`watch(fn, options?)\` signature has been moved to a separate API. ` +
- `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
- `supports \`watch(source, cb, options?) signature.`
- )
- }
- return doWatch(source, cb, options)
- }
- function doWatch(
- source: WatchSource | WatchSource[] | WatchEffect,
- cb: WatchCallback | null,
- { 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, options?) signature.`
- )
- }
- if (deep !== undefined) {
- warn(
- `watch() "deep" option is only respected when using the ` +
- `watch(source, callback, options?) signature.`
- )
- }
- }
- const instance = currentInstance
- const suspense = currentSuspense
- let getter: () => any
- if (isArray(source)) {
- getter = () =>
- source.map(
- s =>
- isRef(s)
- ? s.value
- : callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
- )
- } else if (isRef(source)) {
- getter = () => source.value
- } else if (cb) {
- // getter with cb
- getter = () =>
- callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
- } else {
- // no cb -> simple effect
- getter = () => {
- if (instance && instance.isUnmounted) {
- return
- }
- if (cleanup) {
- cleanup()
- }
- return callWithErrorHandling(
- source,
- instance,
- ErrorCodes.WATCH_CALLBACK,
- [registerCleanup]
- )
- }
- }
- if (cb && deep) {
- const baseGetter = getter
- getter = () => traverse(baseGetter())
- }
- let cleanup: Function
- const registerCleanup: CleanupRegistrator = (fn: () => void) => {
- cleanup = runner.options.onStop = () => {
- callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
- }
- }
- // 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
- ? () => {
- if (instance && instance.isUnmounted) {
- return
- }
- const newValue = runner()
- if (deep || hasChanged(newValue, oldValue)) {
- // cleanup before running cb again
- if (cleanup) {
- cleanup()
- }
- callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
- newValue,
- // pass undefined as the old value when it's changed for the first time
- oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
- registerCleanup
- ])
- oldValue = newValue
- }
- }
- : void 0
- let scheduler: (job: () => any) => void
- if (flush === 'sync') {
- scheduler = invoke
- } else if (flush === 'pre') {
- scheduler = job => {
- if (!instance || instance.vnode.el != null) {
- queueJob(job)
- } else {
- // with 'pre' option, the first call must happen before
- // the component is mounted so it is called synchronously.
- job()
- }
- }
- } else {
- scheduler = job => {
- queuePostRenderEffect(job, suspense)
- }
- }
- const runner = effect(getter, {
- lazy: true,
- // so it runs before component update effects in pre flush mode
- computed: true,
- onTrack,
- onTrigger,
- scheduler: applyCb ? () => scheduler(applyCb) : scheduler
- })
- recordInstanceBoundEffect(runner)
- // initial run
- if (applyCb) {
- if (immediate) {
- applyCb()
- } else {
- oldValue = runner()
- }
- } else {
- runner()
- }
- return () => {
- stop(runner)
- if (instance) {
- remove(instance.effects!, runner)
- }
- }
- }
- // this.$watch
- export function instanceWatch(
- this: ComponentInternalInstance,
- source: string | Function,
- cb: Function,
- options?: WatchOptions
- ): StopHandle {
- const ctx = this.proxy as Data
- const getter = isString(source) ? () => ctx[source] : source.bind(ctx)
- const stop = watch(getter, cb.bind(ctx), options)
- onBeforeUnmount(stop, this)
- return stop
- }
- function traverse(value: unknown, seen: Set<unknown> = new Set()) {
- if (!isObject(value) || seen.has(value)) {
- return
- }
- seen.add(value)
- if (isArray(value)) {
- for (let i = 0; i < value.length; i++) {
- traverse(value[i], seen)
- }
- } else if (value instanceof Map) {
- value.forEach((v, key) => {
- // to register mutation dep for existing keys
- traverse(value.get(key), seen)
- })
- } else if (value instanceof Set) {
- value.forEach(v => {
- traverse(v, seen)
- })
- } else {
- for (const key in value) {
- traverse(value[key], seen)
- }
- }
- return value
- }
|