apiWatch.ts 11 KB

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