apiWatch.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. import {
  2. isRef,
  3. isShallow,
  4. Ref,
  5. ComputedRef,
  6. ReactiveEffect,
  7. isReactive,
  8. ReactiveFlags,
  9. EffectScheduler,
  10. DebuggerOptions
  11. } from '@vue/reactivity'
  12. import { SchedulerJob, queuePreFlushCb } from './scheduler'
  13. import {
  14. EMPTY_OBJ,
  15. isObject,
  16. isArray,
  17. isFunction,
  18. isString,
  19. hasChanged,
  20. NOOP,
  21. remove,
  22. isMap,
  23. isSet,
  24. isPlainObject
  25. } from '@vue/shared'
  26. import {
  27. currentInstance,
  28. ComponentInternalInstance,
  29. isInSSRComponentSetup,
  30. setCurrentInstance,
  31. unsetCurrentInstance
  32. } from './component'
  33. import {
  34. ErrorCodes,
  35. callWithErrorHandling,
  36. callWithAsyncErrorHandling
  37. } from './errorHandling'
  38. import { queuePostRenderEffect } from './renderer'
  39. import { warn } from './warning'
  40. import { DeprecationTypes } from './compat/compatConfig'
  41. import { checkCompatEnabled, isCompatEnabled } from './compat/compatConfig'
  42. import { ObjectWatchOptionItem } from './componentOptions'
  43. export type WatchEffect = (onCleanup: OnCleanup) => void
  44. export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
  45. export type WatchCallback<V = any, OV = any> = (
  46. value: V,
  47. oldValue: OV,
  48. onCleanup: OnCleanup
  49. ) => any
  50. type MapSources<T, Immediate> = {
  51. [K in keyof T]: T[K] extends WatchSource<infer V>
  52. ? Immediate extends true
  53. ? V | undefined
  54. : V
  55. : T[K] extends object
  56. ? Immediate extends true
  57. ? T[K] | undefined
  58. : T[K]
  59. : never
  60. }
  61. 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. }
  69. export type WatchStopHandle = () => void
  70. // Simple effect.
  71. export function watchEffect(
  72. effect: WatchEffect,
  73. options?: WatchOptionsBase
  74. ): WatchStopHandle {
  75. return doWatch(effect, null, options)
  76. }
  77. export function watchPostEffect(
  78. effect: WatchEffect,
  79. options?: DebuggerOptions
  80. ) {
  81. return doWatch(
  82. effect,
  83. null,
  84. (__DEV__
  85. ? Object.assign(options || {}, { flush: 'post' })
  86. : { flush: 'post' }) as WatchOptionsBase
  87. )
  88. }
  89. export function watchSyncEffect(
  90. effect: WatchEffect,
  91. options?: DebuggerOptions
  92. ) {
  93. return doWatch(
  94. effect,
  95. null,
  96. (__DEV__
  97. ? Object.assign(options || {}, { flush: 'sync' })
  98. : { flush: 'sync' }) as WatchOptionsBase
  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, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
  158. ): WatchStopHandle {
  159. if (__DEV__ && !cb) {
  160. if (immediate !== undefined) {
  161. warn(
  162. `watch() "immediate" option is only respected when using the ` +
  163. `watch(source, callback, options?) signature.`
  164. )
  165. }
  166. if (deep !== undefined) {
  167. warn(
  168. `watch() "deep" option is only respected when using the ` +
  169. `watch(source, callback, options?) signature.`
  170. )
  171. }
  172. }
  173. const warnInvalidSource = (s: unknown) => {
  174. warn(
  175. `Invalid watch source: `,
  176. s,
  177. `A watch source can only be a getter/effect function, a ref, ` +
  178. `a reactive object, or an array of these types.`
  179. )
  180. }
  181. const instance = currentInstance
  182. let getter: () => any
  183. let forceTrigger = false
  184. let isMultiSource = false
  185. if (isRef(source)) {
  186. getter = () => source.value
  187. forceTrigger = isShallow(source)
  188. } else if (isReactive(source)) {
  189. getter = () => source
  190. deep = true
  191. } else if (isArray(source)) {
  192. isMultiSource = true
  193. forceTrigger = source.some(isReactive)
  194. getter = () =>
  195. source.map(s => {
  196. if (isRef(s)) {
  197. return s.value
  198. } else if (isReactive(s)) {
  199. return traverse(s)
  200. } else if (isFunction(s)) {
  201. return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
  202. } else {
  203. __DEV__ && warnInvalidSource(s)
  204. }
  205. })
  206. } else if (isFunction(source)) {
  207. if (cb) {
  208. // getter with cb
  209. getter = () =>
  210. callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
  211. } else {
  212. // no cb -> simple effect
  213. getter = () => {
  214. if (instance && instance.isUnmounted) {
  215. return
  216. }
  217. if (cleanup) {
  218. cleanup()
  219. }
  220. return callWithAsyncErrorHandling(
  221. source,
  222. instance,
  223. ErrorCodes.WATCH_CALLBACK,
  224. [onCleanup]
  225. )
  226. }
  227. }
  228. } else {
  229. getter = NOOP
  230. __DEV__ && warnInvalidSource(source)
  231. }
  232. // 2.x array mutation watch compat
  233. if (__COMPAT__ && cb && !deep) {
  234. const baseGetter = getter
  235. getter = () => {
  236. const val = baseGetter()
  237. if (
  238. isArray(val) &&
  239. checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
  240. ) {
  241. traverse(val)
  242. }
  243. return val
  244. }
  245. }
  246. if (cb && deep) {
  247. const baseGetter = getter
  248. getter = () => traverse(baseGetter())
  249. }
  250. let cleanup: () => void
  251. let onCleanup: OnCleanup = (fn: () => void) => {
  252. cleanup = effect.onStop = () => {
  253. callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  254. }
  255. }
  256. // in SSR there is no need to setup an actual effect, and it should be noop
  257. // unless it's eager
  258. if (__SSR__ && isInSSRComponentSetup) {
  259. // we will also not call the invalidate callback (+ runner is not set up)
  260. onCleanup = NOOP
  261. if (!cb) {
  262. getter()
  263. } else if (immediate) {
  264. callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
  265. getter(),
  266. isMultiSource ? [] : undefined,
  267. onCleanup
  268. ])
  269. }
  270. return NOOP
  271. }
  272. let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
  273. const job: SchedulerJob = () => {
  274. if (!effect.active) {
  275. return
  276. }
  277. if (cb) {
  278. // watch(source, cb)
  279. const newValue = effect.run()
  280. if (
  281. deep ||
  282. forceTrigger ||
  283. (isMultiSource
  284. ? (newValue as any[]).some((v, i) =>
  285. hasChanged(v, (oldValue as any[])[i])
  286. )
  287. : hasChanged(newValue, oldValue)) ||
  288. (__COMPAT__ &&
  289. isArray(newValue) &&
  290. isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
  291. ) {
  292. // cleanup before running cb again
  293. if (cleanup) {
  294. cleanup()
  295. }
  296. callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
  297. newValue,
  298. // pass undefined as the old value when it's changed for the first time
  299. oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
  300. onCleanup
  301. ])
  302. oldValue = newValue
  303. }
  304. } else {
  305. // watchEffect
  306. effect.run()
  307. }
  308. }
  309. // important: mark the job as a watcher callback so that scheduler knows
  310. // it is allowed to self-trigger (#1727)
  311. job.allowRecurse = !!cb
  312. let scheduler: EffectScheduler
  313. if (flush === 'sync') {
  314. scheduler = job as any // the scheduler function gets called directly
  315. } else if (flush === 'post') {
  316. scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  317. } else {
  318. // default: 'pre'
  319. scheduler = () => {
  320. if (!instance || instance.isMounted) {
  321. queuePreFlushCb(job)
  322. } else {
  323. // with 'pre' option, the first call must happen before
  324. // the component is mounted so it is called synchronously.
  325. job()
  326. }
  327. }
  328. }
  329. const effect = new ReactiveEffect(getter, scheduler)
  330. if (__DEV__) {
  331. effect.onTrack = onTrack
  332. effect.onTrigger = onTrigger
  333. }
  334. // initial run
  335. if (cb) {
  336. if (immediate) {
  337. job()
  338. } else {
  339. oldValue = effect.run()
  340. }
  341. } else if (flush === 'post') {
  342. queuePostRenderEffect(
  343. effect.run.bind(effect),
  344. instance && instance.suspense
  345. )
  346. } else {
  347. effect.run()
  348. }
  349. return () => {
  350. effect.stop()
  351. if (instance && instance.scope) {
  352. remove(instance.scope.effects!, effect)
  353. }
  354. }
  355. }
  356. // this.$watch
  357. export function instanceWatch(
  358. this: ComponentInternalInstance,
  359. source: string | Function,
  360. value: WatchCallback | ObjectWatchOptionItem,
  361. options?: WatchOptions
  362. ): WatchStopHandle {
  363. const publicThis = this.proxy as any
  364. const getter = isString(source)
  365. ? source.includes('.')
  366. ? createPathGetter(publicThis, source)
  367. : () => publicThis[source]
  368. : source.bind(publicThis, publicThis)
  369. let cb
  370. if (isFunction(value)) {
  371. cb = value
  372. } else {
  373. cb = value.handler as Function
  374. options = value
  375. }
  376. const cur = currentInstance
  377. setCurrentInstance(this)
  378. const res = doWatch(getter, cb.bind(publicThis), options)
  379. if (cur) {
  380. setCurrentInstance(cur)
  381. } else {
  382. unsetCurrentInstance()
  383. }
  384. return res
  385. }
  386. export function createPathGetter(ctx: any, path: string) {
  387. const segments = path.split('.')
  388. return () => {
  389. let cur = ctx
  390. for (let i = 0; i < segments.length && cur; i++) {
  391. cur = cur[segments[i]]
  392. }
  393. return cur
  394. }
  395. }
  396. export function traverse(value: unknown, seen?: Set<unknown>) {
  397. if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
  398. return value
  399. }
  400. seen = seen || new Set()
  401. if (seen.has(value)) {
  402. return value
  403. }
  404. seen.add(value)
  405. if (isRef(value)) {
  406. traverse(value.value, seen)
  407. } else if (isArray(value)) {
  408. for (let i = 0; i < value.length; i++) {
  409. traverse(value[i], seen)
  410. }
  411. } else if (isSet(value) || isMap(value)) {
  412. value.forEach((v: any) => {
  413. traverse(v, seen)
  414. })
  415. } else if (isPlainObject(value)) {
  416. for (const key in value) {
  417. traverse((value as any)[key], seen)
  418. }
  419. }
  420. return value
  421. }