apiWatch.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import {
  2. type WatchOptions as BaseWatchOptions,
  3. type DebuggerOptions,
  4. EffectFlags,
  5. type ReactiveMarker,
  6. type WatchCallback,
  7. type WatchEffect,
  8. type WatchHandle,
  9. type WatchSource,
  10. WatcherEffect,
  11. } from '@vue/reactivity'
  12. import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler'
  13. import { EMPTY_OBJ, NOOP, extend, isFunction, isString } from '@vue/shared'
  14. import {
  15. type ComponentInternalInstance,
  16. type GenericComponentInstance,
  17. currentInstance,
  18. isInSSRComponentSetup,
  19. setCurrentInstance,
  20. } from './component'
  21. import { callWithAsyncErrorHandling } from './errorHandling'
  22. import { queuePostRenderEffect } from './renderer'
  23. import { warn } from './warning'
  24. import type { ObjectWatchOptionItem } from './componentOptions'
  25. import { useSSRContext } from './helpers/useSsrContext'
  26. import type { ComponentPublicInstance } from './componentPublicInstance'
  27. export type {
  28. WatchHandle,
  29. WatchStopHandle,
  30. WatchEffect,
  31. WatchSource,
  32. WatchCallback,
  33. OnCleanup,
  34. } from '@vue/reactivity'
  35. type MaybeUndefined<T, I> = I extends true ? T | undefined : T
  36. type MapSources<T, Immediate> = {
  37. [K in keyof T]: T[K] extends WatchSource<infer V>
  38. ? MaybeUndefined<V, Immediate>
  39. : T[K] extends object
  40. ? MaybeUndefined<T[K], Immediate>
  41. : never
  42. }
  43. export interface WatchEffectOptions extends DebuggerOptions {
  44. flush?: 'pre' | 'post' | 'sync'
  45. }
  46. export interface WatchOptions<Immediate = boolean> extends WatchEffectOptions {
  47. immediate?: Immediate
  48. deep?: boolean | number
  49. once?: boolean
  50. }
  51. // Simple effect.
  52. export function watchEffect(
  53. effect: WatchEffect,
  54. options?: WatchEffectOptions,
  55. ): WatchHandle {
  56. return doWatch(effect, null, options)
  57. }
  58. export function watchPostEffect(
  59. effect: WatchEffect,
  60. options?: DebuggerOptions,
  61. ): WatchHandle {
  62. return doWatch(
  63. effect,
  64. null,
  65. __DEV__
  66. ? extend({}, options as WatchEffectOptions, { flush: 'post' })
  67. : { flush: 'post' },
  68. )
  69. }
  70. export function watchSyncEffect(
  71. effect: WatchEffect,
  72. options?: DebuggerOptions,
  73. ): WatchHandle {
  74. return doWatch(
  75. effect,
  76. null,
  77. __DEV__
  78. ? extend({}, options as WatchEffectOptions, { flush: 'sync' })
  79. : { flush: 'sync' },
  80. )
  81. }
  82. export type MultiWatchSources = (WatchSource<unknown> | object)[]
  83. // overload: single source + cb
  84. export function watch<T, Immediate extends Readonly<boolean> = false>(
  85. source: WatchSource<T>,
  86. cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
  87. options?: WatchOptions<Immediate>,
  88. ): WatchHandle
  89. // overload: reactive array or tuple of multiple sources + cb
  90. export function watch<
  91. T extends Readonly<MultiWatchSources>,
  92. Immediate extends Readonly<boolean> = false,
  93. >(
  94. sources: readonly [...T] | T,
  95. cb: [T] extends [ReactiveMarker]
  96. ? WatchCallback<T, MaybeUndefined<T, Immediate>>
  97. : WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
  98. options?: WatchOptions<Immediate>,
  99. ): WatchHandle
  100. // overload: array of multiple sources + cb
  101. export function watch<
  102. T extends MultiWatchSources,
  103. Immediate extends Readonly<boolean> = false,
  104. >(
  105. sources: [...T],
  106. cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
  107. options?: WatchOptions<Immediate>,
  108. ): WatchHandle
  109. // overload: watching reactive object w/ cb
  110. export function watch<
  111. T extends object,
  112. Immediate extends Readonly<boolean> = false,
  113. >(
  114. source: T,
  115. cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
  116. options?: WatchOptions<Immediate>,
  117. ): WatchHandle
  118. // implementation
  119. export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  120. source: T | WatchSource<T>,
  121. cb: WatchCallback,
  122. options?: WatchOptions<Immediate>,
  123. ): WatchHandle {
  124. if (__DEV__ && !isFunction(cb)) {
  125. warn(
  126. `\`watch(fn, options?)\` signature has been moved to a separate API. ` +
  127. `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
  128. `supports \`watch(source, cb, options?) signature.`,
  129. )
  130. }
  131. return doWatch(source as any, cb, options)
  132. }
  133. class RenderWatcherEffect extends WatcherEffect {
  134. job: SchedulerJob
  135. constructor(
  136. instance: GenericComponentInstance | null,
  137. source: WatchSource | WatchSource[] | WatchEffect | object,
  138. cb: WatchCallback | null,
  139. options: BaseWatchOptions,
  140. private flush: 'pre' | 'post' | 'sync',
  141. ) {
  142. super(source, cb, options)
  143. const job: SchedulerJob = () => {
  144. if (this.dirty) {
  145. this.run()
  146. }
  147. }
  148. // important: mark the job as a watcher callback so that scheduler knows
  149. // it is allowed to self-trigger (#1727)
  150. if (cb) {
  151. this.flags |= EffectFlags.ALLOW_RECURSE
  152. job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
  153. }
  154. if (instance) {
  155. job.i = instance
  156. }
  157. this.job = job
  158. }
  159. notify(): void {
  160. const flags = this.flags
  161. if (!(flags & EffectFlags.PAUSED)) {
  162. const flush = this.flush
  163. const job = this.job
  164. if (flush === 'post') {
  165. queuePostRenderEffect(job, undefined, job.i ? job.i.suspense : null)
  166. } else if (flush === 'pre') {
  167. queueJob(job, job.i ? job.i.uid : undefined, true)
  168. } else {
  169. job()
  170. }
  171. }
  172. }
  173. }
  174. function doWatch(
  175. source: WatchSource | WatchSource[] | WatchEffect | object,
  176. cb: WatchCallback | null,
  177. options: WatchOptions = EMPTY_OBJ,
  178. ): WatchHandle {
  179. const { immediate, deep, flush = 'pre', once } = options
  180. if (__DEV__ && !cb) {
  181. if (immediate !== undefined) {
  182. warn(
  183. `watch() "immediate" option is only respected when using the ` +
  184. `watch(source, callback, options?) signature.`,
  185. )
  186. }
  187. if (deep !== undefined) {
  188. warn(
  189. `watch() "deep" option is only respected when using the ` +
  190. `watch(source, callback, options?) signature.`,
  191. )
  192. }
  193. if (once !== undefined) {
  194. warn(
  195. `watch() "once" option is only respected when using the ` +
  196. `watch(source, callback, options?) signature.`,
  197. )
  198. }
  199. }
  200. const baseWatchOptions: BaseWatchOptions = extend({}, options)
  201. if (__DEV__) baseWatchOptions.onWarn = warn
  202. // immediate watcher or watchEffect
  203. const runsImmediately = (cb && immediate) || (!cb && flush !== 'post')
  204. let ssrCleanup: (() => void)[] | undefined
  205. if (__SSR__ && isInSSRComponentSetup) {
  206. if (flush === 'sync') {
  207. const ctx = useSSRContext()!
  208. ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
  209. } else if (!runsImmediately) {
  210. const watchStopHandle = () => {}
  211. watchStopHandle.stop = NOOP
  212. watchStopHandle.resume = NOOP
  213. watchStopHandle.pause = NOOP
  214. return watchStopHandle
  215. }
  216. }
  217. const instance = currentInstance
  218. baseWatchOptions.call = (fn, type, args) =>
  219. callWithAsyncErrorHandling(fn, instance, type, args)
  220. const effect = new RenderWatcherEffect(
  221. instance,
  222. source,
  223. cb,
  224. baseWatchOptions,
  225. flush,
  226. )
  227. // initial run
  228. if (cb) {
  229. effect.run(true)
  230. } else if (flush === 'post') {
  231. queuePostRenderEffect(effect.job, undefined, instance && instance.suspense)
  232. } else {
  233. effect.run(true)
  234. }
  235. const stop = effect.stop.bind(effect) as WatchHandle
  236. stop.pause = effect.pause.bind(effect)
  237. stop.resume = effect.resume.bind(effect)
  238. stop.stop = stop
  239. if (__SSR__ && isInSSRComponentSetup) {
  240. if (ssrCleanup) {
  241. ssrCleanup.push(stop)
  242. } else if (runsImmediately) {
  243. stop()
  244. }
  245. }
  246. return stop
  247. }
  248. // this.$watch
  249. export function instanceWatch(
  250. this: ComponentInternalInstance,
  251. source: string | Function,
  252. value: WatchCallback | ObjectWatchOptionItem,
  253. options?: WatchOptions,
  254. ): WatchHandle {
  255. const publicThis = this.proxy
  256. const getter = isString(source)
  257. ? source.includes('.')
  258. ? createPathGetter(publicThis!, source)
  259. : () => publicThis![source as keyof typeof publicThis]
  260. : source.bind(publicThis, publicThis)
  261. let cb
  262. if (isFunction(value)) {
  263. cb = value
  264. } else {
  265. cb = value.handler as Function
  266. options = value
  267. }
  268. const prev = setCurrentInstance(this)
  269. const res = doWatch(getter, cb.bind(publicThis), options)
  270. setCurrentInstance(...prev)
  271. return res
  272. }
  273. export function createPathGetter(
  274. ctx: ComponentPublicInstance,
  275. path: string,
  276. ): () => WatchSource | WatchSource[] | WatchEffect | object {
  277. const segments = path.split('.')
  278. return (): WatchSource | WatchSource[] | WatchEffect | object => {
  279. let cur = ctx
  280. for (let i = 0; i < segments.length && cur; i++) {
  281. cur = cur[segments[i] as keyof typeof cur]
  282. }
  283. return cur
  284. }
  285. }