apiWatch.ts 12 KB

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