apiWatch.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. import {
  2. type ComputedRef,
  3. type DebuggerOptions,
  4. type EffectScheduler,
  5. ReactiveEffect,
  6. ReactiveFlags,
  7. type ReactiveMarker,
  8. type Ref,
  9. getCurrentScope,
  10. isReactive,
  11. isRef,
  12. isShallow,
  13. } from '@vue/reactivity'
  14. import { type SchedulerJob, queueJob } from './scheduler'
  15. import {
  16. EMPTY_OBJ,
  17. NOOP,
  18. extend,
  19. hasChanged,
  20. isArray,
  21. isFunction,
  22. isMap,
  23. isObject,
  24. isPlainObject,
  25. isSet,
  26. isString,
  27. remove,
  28. } from '@vue/shared'
  29. import {
  30. type ComponentInternalInstance,
  31. currentInstance,
  32. isInSSRComponentSetup,
  33. setCurrentInstance,
  34. } from './component'
  35. import {
  36. ErrorCodes,
  37. callWithAsyncErrorHandling,
  38. callWithErrorHandling,
  39. } from './errorHandling'
  40. import { queuePostRenderEffect } from './renderer'
  41. import { warn } from './warning'
  42. import { DeprecationTypes } from './compat/compatConfig'
  43. import { checkCompatEnabled, isCompatEnabled } from './compat/compatConfig'
  44. import type { ObjectWatchOptionItem } from './componentOptions'
  45. import { useSSRContext } from './helpers/useSsrContext'
  46. export type WatchEffect = (onCleanup: OnCleanup) => void
  47. export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
  48. export type WatchCallback<V = any, OV = any> = (
  49. value: V,
  50. oldValue: OV,
  51. onCleanup: OnCleanup,
  52. ) => any
  53. type MaybeUndefined<T, I> = I extends true ? T | undefined : T
  54. type MapSources<T, Immediate> = {
  55. [K in keyof T]: T[K] extends WatchSource<infer V>
  56. ? MaybeUndefined<V, Immediate>
  57. : T[K] extends object
  58. ? MaybeUndefined<T[K], Immediate>
  59. : never
  60. }
  61. export type OnCleanup = (cleanupFn: () => void) => void
  62. export interface WatchOptionsBase extends DebuggerOptions {
  63. flush?: 'pre' | 'post' | 'sync'
  64. }
  65. export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
  66. immediate?: Immediate
  67. deep?: boolean
  68. once?: boolean
  69. }
  70. export type WatchStopHandle = () => void
  71. // Simple effect.
  72. export function watchEffect(
  73. effect: WatchEffect,
  74. options?: WatchOptionsBase,
  75. ): WatchStopHandle {
  76. return doWatch(effect, null, options)
  77. }
  78. export function watchPostEffect(
  79. effect: WatchEffect,
  80. options?: DebuggerOptions,
  81. ) {
  82. return doWatch(
  83. effect,
  84. null,
  85. __DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },
  86. )
  87. }
  88. export function watchSyncEffect(
  89. effect: WatchEffect,
  90. options?: DebuggerOptions,
  91. ) {
  92. return doWatch(
  93. effect,
  94. null,
  95. __DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' },
  96. )
  97. }
  98. // initial value for watchers to trigger on undefined initial values
  99. const INITIAL_WATCHER_VALUE = {}
  100. type MultiWatchSources = (WatchSource<unknown> | object)[]
  101. // overload: single source + cb
  102. export function watch<T, Immediate extends Readonly<boolean> = false>(
  103. source: WatchSource<T>,
  104. cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
  105. options?: WatchOptions<Immediate>,
  106. ): WatchStopHandle
  107. // overload: reactive array or tuple of multiple sources + cb
  108. export function watch<
  109. T extends Readonly<MultiWatchSources>,
  110. Immediate extends Readonly<boolean> = false,
  111. >(
  112. sources: readonly [...T] | T,
  113. cb: [T] extends [ReactiveMarker]
  114. ? WatchCallback<T, MaybeUndefined<T, Immediate>>
  115. : WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
  116. options?: WatchOptions<Immediate>,
  117. ): WatchStopHandle
  118. // overload: array of multiple sources + cb
  119. export function watch<
  120. T extends MultiWatchSources,
  121. Immediate extends Readonly<boolean> = false,
  122. >(
  123. sources: [...T],
  124. cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
  125. options?: WatchOptions<Immediate>,
  126. ): WatchStopHandle
  127. // overload: watching reactive object w/ cb
  128. export function watch<
  129. T extends object,
  130. Immediate extends Readonly<boolean> = false,
  131. >(
  132. source: T,
  133. cb: WatchCallback<T, MaybeUndefined<T, Immediate>>,
  134. options?: WatchOptions<Immediate>,
  135. ): WatchStopHandle
  136. // implementation
  137. export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  138. source: T | WatchSource<T>,
  139. cb: any,
  140. options?: WatchOptions<Immediate>,
  141. ): WatchStopHandle {
  142. if (__DEV__ && !isFunction(cb)) {
  143. warn(
  144. `\`watch(fn, options?)\` signature has been moved to a separate API. ` +
  145. `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
  146. `supports \`watch(source, cb, options?) signature.`,
  147. )
  148. }
  149. return doWatch(source as any, cb, options)
  150. }
  151. function doWatch(
  152. source: WatchSource | WatchSource[] | WatchEffect | object,
  153. cb: WatchCallback | null,
  154. {
  155. immediate,
  156. deep,
  157. flush,
  158. once,
  159. onTrack,
  160. onTrigger,
  161. }: WatchOptions = EMPTY_OBJ,
  162. ): WatchStopHandle {
  163. if (cb && once) {
  164. const _cb = cb
  165. cb = (...args) => {
  166. _cb(...args)
  167. unwatch()
  168. }
  169. }
  170. // TODO remove in 3.5
  171. if (__DEV__ && deep !== void 0 && typeof deep === 'number') {
  172. warn(
  173. `watch() "deep" option with number value will be used as watch depth in future versions. ` +
  174. `Please use a boolean instead to avoid potential breakage.`,
  175. )
  176. }
  177. if (__DEV__ && !cb) {
  178. if (immediate !== undefined) {
  179. warn(
  180. `watch() "immediate" option is only respected when using the ` +
  181. `watch(source, callback, options?) signature.`,
  182. )
  183. }
  184. if (deep !== undefined) {
  185. warn(
  186. `watch() "deep" option is only respected when using the ` +
  187. `watch(source, callback, options?) signature.`,
  188. )
  189. }
  190. if (once !== undefined) {
  191. warn(
  192. `watch() "once" option is only respected when using the ` +
  193. `watch(source, callback, options?) signature.`,
  194. )
  195. }
  196. }
  197. const warnInvalidSource = (s: unknown) => {
  198. warn(
  199. `Invalid watch source: `,
  200. s,
  201. `A watch source can only be a getter/effect function, a ref, ` +
  202. `a reactive object, or an array of these types.`,
  203. )
  204. }
  205. const instance = currentInstance
  206. const reactiveGetter = (source: object) =>
  207. deep === true
  208. ? source // traverse will happen in wrapped getter below
  209. : // for deep: false, only traverse root-level properties
  210. traverse(source, deep === false ? 1 : undefined)
  211. let getter: () => any
  212. let forceTrigger = false
  213. let isMultiSource = false
  214. if (isRef(source)) {
  215. getter = () => source.value
  216. forceTrigger = isShallow(source)
  217. } else if (isReactive(source)) {
  218. getter = () => reactiveGetter(source)
  219. forceTrigger = true
  220. } else if (isArray(source)) {
  221. isMultiSource = true
  222. forceTrigger = source.some(s => isReactive(s) || isShallow(s))
  223. getter = () =>
  224. source.map(s => {
  225. if (isRef(s)) {
  226. return s.value
  227. } else if (isReactive(s)) {
  228. return reactiveGetter(s)
  229. } else if (isFunction(s)) {
  230. return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
  231. } else {
  232. __DEV__ && warnInvalidSource(s)
  233. }
  234. })
  235. } else if (isFunction(source)) {
  236. if (cb) {
  237. // getter with cb
  238. getter = () =>
  239. callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
  240. } else {
  241. // no cb -> simple effect
  242. getter = () => {
  243. if (cleanup) {
  244. cleanup()
  245. }
  246. return callWithAsyncErrorHandling(
  247. source,
  248. instance,
  249. ErrorCodes.WATCH_CALLBACK,
  250. [onCleanup],
  251. )
  252. }
  253. }
  254. } else {
  255. getter = NOOP
  256. __DEV__ && warnInvalidSource(source)
  257. }
  258. // 2.x array mutation watch compat
  259. if (__COMPAT__ && cb && !deep) {
  260. const baseGetter = getter
  261. getter = () => {
  262. const val = baseGetter()
  263. if (
  264. isArray(val) &&
  265. checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
  266. ) {
  267. traverse(val)
  268. }
  269. return val
  270. }
  271. }
  272. if (cb && deep) {
  273. const baseGetter = getter
  274. getter = () => traverse(baseGetter())
  275. }
  276. let cleanup: (() => void) | undefined
  277. let onCleanup: OnCleanup = (fn: () => void) => {
  278. cleanup = effect.onStop = () => {
  279. callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  280. cleanup = effect.onStop = undefined
  281. }
  282. }
  283. // in SSR there is no need to setup an actual effect, and it should be noop
  284. // unless it's eager or sync flush
  285. let ssrCleanup: (() => void)[] | undefined
  286. if (__SSR__ && isInSSRComponentSetup) {
  287. // we will also not call the invalidate callback (+ runner is not set up)
  288. onCleanup = NOOP
  289. if (!cb) {
  290. getter()
  291. } else if (immediate) {
  292. callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
  293. getter(),
  294. isMultiSource ? [] : undefined,
  295. onCleanup,
  296. ])
  297. }
  298. if (flush === 'sync') {
  299. const ctx = useSSRContext()!
  300. ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
  301. } else {
  302. return NOOP
  303. }
  304. }
  305. let oldValue: any = isMultiSource
  306. ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
  307. : INITIAL_WATCHER_VALUE
  308. const job: SchedulerJob = () => {
  309. if (!effect.active || !effect.dirty) {
  310. return
  311. }
  312. if (cb) {
  313. // watch(source, cb)
  314. const newValue = effect.run()
  315. if (
  316. deep ||
  317. forceTrigger ||
  318. (isMultiSource
  319. ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
  320. : hasChanged(newValue, oldValue)) ||
  321. (__COMPAT__ &&
  322. isArray(newValue) &&
  323. isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
  324. ) {
  325. // cleanup before running cb again
  326. if (cleanup) {
  327. cleanup()
  328. }
  329. callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
  330. newValue,
  331. // pass undefined as the old value when it's changed for the first time
  332. oldValue === INITIAL_WATCHER_VALUE
  333. ? undefined
  334. : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
  335. ? []
  336. : oldValue,
  337. onCleanup,
  338. ])
  339. oldValue = newValue
  340. }
  341. } else {
  342. // watchEffect
  343. effect.run()
  344. }
  345. }
  346. // important: mark the job as a watcher callback so that scheduler knows
  347. // it is allowed to self-trigger (#1727)
  348. job.allowRecurse = !!cb
  349. let scheduler: EffectScheduler
  350. if (flush === 'sync') {
  351. scheduler = job as any // the scheduler function gets called directly
  352. } else if (flush === 'post') {
  353. scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  354. } else {
  355. // default: 'pre'
  356. job.pre = true
  357. if (instance) job.id = instance.uid
  358. scheduler = () => queueJob(job)
  359. }
  360. const effect = new ReactiveEffect(getter, NOOP, scheduler)
  361. const scope = getCurrentScope()
  362. const unwatch = () => {
  363. effect.stop()
  364. if (scope) {
  365. remove(scope.effects, effect)
  366. }
  367. }
  368. if (__DEV__) {
  369. effect.onTrack = onTrack
  370. effect.onTrigger = onTrigger
  371. }
  372. // initial run
  373. if (cb) {
  374. if (immediate) {
  375. job()
  376. } else {
  377. oldValue = effect.run()
  378. }
  379. } else if (flush === 'post') {
  380. queuePostRenderEffect(
  381. effect.run.bind(effect),
  382. instance && instance.suspense,
  383. )
  384. } else {
  385. effect.run()
  386. }
  387. if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
  388. return unwatch
  389. }
  390. // this.$watch
  391. export function instanceWatch(
  392. this: ComponentInternalInstance,
  393. source: string | Function,
  394. value: WatchCallback | ObjectWatchOptionItem,
  395. options?: WatchOptions,
  396. ): WatchStopHandle {
  397. const publicThis = this.proxy as any
  398. const getter = isString(source)
  399. ? source.includes('.')
  400. ? createPathGetter(publicThis, source)
  401. : () => publicThis[source]
  402. : source.bind(publicThis, publicThis)
  403. let cb
  404. if (isFunction(value)) {
  405. cb = value
  406. } else {
  407. cb = value.handler as Function
  408. options = value
  409. }
  410. const reset = setCurrentInstance(this)
  411. const res = doWatch(getter, cb.bind(publicThis), options)
  412. reset()
  413. return res
  414. }
  415. export function createPathGetter(ctx: any, path: string) {
  416. const segments = path.split('.')
  417. return () => {
  418. let cur = ctx
  419. for (let i = 0; i < segments.length && cur; i++) {
  420. cur = cur[segments[i]]
  421. }
  422. return cur
  423. }
  424. }
  425. export function traverse(
  426. value: unknown,
  427. depth = Infinity,
  428. seen?: Set<unknown>,
  429. ) {
  430. if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
  431. return value
  432. }
  433. seen = seen || new Set()
  434. if (seen.has(value)) {
  435. return value
  436. }
  437. seen.add(value)
  438. depth--
  439. if (isRef(value)) {
  440. traverse(value.value, depth, seen)
  441. } else if (isArray(value)) {
  442. for (let i = 0; i < value.length; i++) {
  443. traverse(value[i], depth, seen)
  444. }
  445. } else if (isSet(value) || isMap(value)) {
  446. value.forEach((v: any) => {
  447. traverse(v, depth, seen)
  448. })
  449. } else if (isPlainObject(value)) {
  450. for (const key in value) {
  451. traverse(value[key], depth, seen)
  452. }
  453. for (const key of Object.getOwnPropertySymbols(value)) {
  454. if (Object.prototype.propertyIsEnumerable.call(value, key)) {
  455. traverse(value[key as any], depth, seen)
  456. }
  457. }
  458. }
  459. return value
  460. }