watch.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. import {
  2. EMPTY_OBJ,
  3. NOOP,
  4. hasChanged,
  5. isArray,
  6. isFunction,
  7. isMap,
  8. isObject,
  9. isPlainObject,
  10. isSet,
  11. } from '@vue/shared'
  12. import type { ComputedRef } from './computed'
  13. import { ReactiveFlags } from './constants'
  14. import { type DebuggerOptions, ReactiveEffect, cleanup } from './effect'
  15. import { isReactive, isShallow } from './reactive'
  16. import { type Ref, isRef } from './ref'
  17. import { setActiveSub } from './system'
  18. import { warn } from './warning'
  19. // These errors were transferred from `packages/runtime-core/src/errorHandling.ts`
  20. // to @vue/reactivity to allow co-location with the moved base watch logic, hence
  21. // it is essential to keep these values unchanged.
  22. export enum WatchErrorCodes {
  23. WATCH_GETTER = 2,
  24. WATCH_CALLBACK,
  25. WATCH_CLEANUP,
  26. }
  27. export type WatchEffect = (onCleanup: OnCleanup) => void
  28. export type WatchSource<T = any> = Ref<T, any> | ComputedRef<T> | (() => T)
  29. export type WatchCallback<V = any, OV = any> = (
  30. value: V,
  31. oldValue: OV,
  32. onCleanup: OnCleanup,
  33. ) => any
  34. export type OnCleanup = (cleanupFn: () => void) => void
  35. export interface WatchOptions<Immediate = boolean> extends DebuggerOptions {
  36. immediate?: Immediate
  37. deep?: boolean | number
  38. once?: boolean
  39. onWarn?: (msg: string, ...args: any[]) => void
  40. /**
  41. * @internal
  42. */
  43. call?: (
  44. fn: Function | Function[],
  45. type: WatchErrorCodes,
  46. args?: unknown[],
  47. ) => void
  48. }
  49. export type WatchStopHandle = () => void
  50. export interface WatchHandle extends WatchStopHandle {
  51. pause: () => void
  52. resume: () => void
  53. stop: () => void
  54. }
  55. // initial value for watchers to trigger on undefined initial values
  56. const INITIAL_WATCHER_VALUE = {}
  57. let activeWatcher: WatcherEffect | undefined = undefined
  58. /**
  59. * Returns the current active effect if there is one.
  60. */
  61. export function getCurrentWatcher(): ReactiveEffect<any> | undefined {
  62. return activeWatcher
  63. }
  64. /**
  65. * Registers a cleanup callback on the current active effect. This
  66. * registered cleanup callback will be invoked right before the
  67. * associated effect re-runs.
  68. *
  69. * @param cleanupFn - The callback function to attach to the effect's cleanup.
  70. * @param failSilently - if `true`, will not throw warning when called without
  71. * an active effect.
  72. * @param owner - The effect that this cleanup function should be attached to.
  73. * By default, the current active effect.
  74. */
  75. export function onWatcherCleanup(
  76. cleanupFn: () => void,
  77. failSilently = false,
  78. owner: WatcherEffect | undefined = activeWatcher,
  79. ): void {
  80. if (owner) {
  81. const { call } = owner.options
  82. if (call) {
  83. owner.cleanups[owner.cleanupsLength++] = () =>
  84. call(cleanupFn, WatchErrorCodes.WATCH_CLEANUP)
  85. } else {
  86. owner.cleanups[owner.cleanupsLength++] = cleanupFn
  87. }
  88. } else if (__DEV__ && !failSilently) {
  89. warn(
  90. `onWatcherCleanup() was called when there was no active watcher` +
  91. ` to associate with.`,
  92. )
  93. }
  94. }
  95. export class WatcherEffect extends ReactiveEffect {
  96. forceTrigger: boolean
  97. isMultiSource: boolean
  98. oldValue: any
  99. boundCleanup: typeof onWatcherCleanup = fn =>
  100. onWatcherCleanup(fn, false, this)
  101. constructor(
  102. source: WatchSource | WatchSource[] | WatchEffect | object,
  103. public cb?: WatchCallback<any, any> | null | undefined,
  104. public options: WatchOptions = EMPTY_OBJ,
  105. ) {
  106. const { deep, once, call, onWarn } = options
  107. let getter: () => any
  108. let forceTrigger = false
  109. let isMultiSource = false
  110. if (isRef(source)) {
  111. getter = () => source.value
  112. forceTrigger = isShallow(source)
  113. } else if (isReactive(source)) {
  114. getter = () => reactiveGetter(source, deep)
  115. forceTrigger = true
  116. } else if (isArray(source)) {
  117. isMultiSource = true
  118. forceTrigger = source.some(s => isReactive(s) || isShallow(s))
  119. getter = () =>
  120. source.map(s => {
  121. if (isRef(s)) {
  122. return s.value
  123. } else if (isReactive(s)) {
  124. return reactiveGetter(s, deep)
  125. } else if (isFunction(s)) {
  126. return call ? call(s, WatchErrorCodes.WATCH_GETTER) : s()
  127. } else {
  128. __DEV__ && warnInvalidSource(s, onWarn)
  129. }
  130. })
  131. } else if (isFunction(source)) {
  132. if (cb) {
  133. // getter with cb
  134. getter = call
  135. ? () => call(source, WatchErrorCodes.WATCH_GETTER)
  136. : (source as () => any)
  137. } else {
  138. // no cb -> simple effect
  139. getter = () => {
  140. if (this.cleanupsLength) {
  141. const prevSub = setActiveSub()
  142. try {
  143. cleanup(this)
  144. } finally {
  145. setActiveSub(prevSub)
  146. }
  147. }
  148. const currentEffect = activeWatcher
  149. activeWatcher = this
  150. try {
  151. return call
  152. ? call(source, WatchErrorCodes.WATCH_CALLBACK, [
  153. this.boundCleanup,
  154. ])
  155. : source(this.boundCleanup)
  156. } finally {
  157. activeWatcher = currentEffect
  158. }
  159. }
  160. }
  161. } else {
  162. getter = NOOP
  163. __DEV__ && warnInvalidSource(source, onWarn)
  164. }
  165. if (cb && deep) {
  166. const baseGetter = getter
  167. const depth = deep === true ? Infinity : deep
  168. getter = () => traverse(baseGetter(), depth)
  169. }
  170. super(getter)
  171. this.forceTrigger = forceTrigger
  172. this.isMultiSource = isMultiSource
  173. if (once && cb) {
  174. const _cb = cb
  175. cb = (...args) => {
  176. _cb(...args)
  177. this.stop()
  178. }
  179. }
  180. this.cb = cb
  181. this.oldValue = isMultiSource
  182. ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
  183. : INITIAL_WATCHER_VALUE
  184. if (__DEV__) {
  185. this.onTrack = options.onTrack
  186. this.onTrigger = options.onTrigger
  187. }
  188. }
  189. run(initialRun = false): void {
  190. const oldValue = this.oldValue
  191. const newValue = (this.oldValue = super.run())
  192. if (!this.cb) {
  193. return
  194. }
  195. const { immediate, deep, call } = this.options
  196. if (initialRun && !immediate) {
  197. return
  198. }
  199. if (
  200. deep ||
  201. this.forceTrigger ||
  202. (this.isMultiSource
  203. ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
  204. : hasChanged(newValue, oldValue))
  205. ) {
  206. // cleanup before running cb again
  207. cleanup(this)
  208. const currentWatcher = activeWatcher
  209. activeWatcher = this
  210. try {
  211. const args = [
  212. newValue,
  213. // pass undefined as the old value when it's changed for the first time
  214. oldValue === INITIAL_WATCHER_VALUE
  215. ? undefined
  216. : this.isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
  217. ? []
  218. : oldValue,
  219. this.boundCleanup,
  220. ]
  221. call
  222. ? call(this.cb, WatchErrorCodes.WATCH_CALLBACK, args)
  223. : // @ts-expect-error
  224. this.cb(...args)
  225. } finally {
  226. activeWatcher = currentWatcher
  227. }
  228. }
  229. }
  230. }
  231. function reactiveGetter(source: object, deep: WatchOptions['deep']): unknown {
  232. // traverse will happen in wrapped getter below
  233. if (deep) return source
  234. // for `deep: false | 0` or shallow reactive, only traverse root-level properties
  235. if (isShallow(source) || deep === false || deep === 0)
  236. return traverse(source, 1)
  237. // for `deep: undefined` on a reactive object, deeply traverse all properties
  238. return traverse(source)
  239. }
  240. function warnInvalidSource(s: object, onWarn: WatchOptions['onWarn']): void {
  241. ;(onWarn || warn)(
  242. `Invalid watch source: `,
  243. s,
  244. `A watch source can only be a getter/effect function, a ref, ` +
  245. `a reactive object, or an array of these types.`,
  246. )
  247. }
  248. export function watch(
  249. source: WatchSource | WatchSource[] | WatchEffect | object,
  250. cb?: WatchCallback | null,
  251. options: WatchOptions = EMPTY_OBJ,
  252. ): WatchHandle {
  253. const effect = new WatcherEffect(source, cb, options)
  254. effect.run(true)
  255. const stop = effect.stop.bind(effect) as WatchHandle
  256. stop.pause = effect.pause.bind(effect)
  257. stop.resume = effect.resume.bind(effect)
  258. stop.stop = stop
  259. return stop
  260. }
  261. export function traverse(
  262. value: unknown,
  263. depth: number = Infinity,
  264. seen?: Map<unknown, number>,
  265. ): unknown {
  266. if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
  267. return value
  268. }
  269. seen = seen || new Map()
  270. if ((seen.get(value) || 0) >= depth) {
  271. return value
  272. }
  273. seen.set(value, depth)
  274. depth--
  275. if (isRef(value)) {
  276. traverse(value.value, depth, seen)
  277. } else if (isArray(value)) {
  278. for (let i = 0; i < value.length; i++) {
  279. traverse(value[i], depth, seen)
  280. }
  281. } else if (isSet(value) || isMap(value)) {
  282. value.forEach((v: any) => {
  283. traverse(v, depth, seen)
  284. })
  285. } else if (isPlainObject(value)) {
  286. for (const key in value) {
  287. traverse(value[key], depth, seen)
  288. }
  289. for (const key of Object.getOwnPropertySymbols(value)) {
  290. if (Object.prototype.propertyIsEnumerable.call(value, key)) {
  291. traverse(value[key as any], depth, seen)
  292. }
  293. }
  294. }
  295. return value
  296. }