apiWatch.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. import {
  2. effect,
  3. stop,
  4. isRef,
  5. Ref,
  6. ComputedRef,
  7. ReactiveEffectOptions,
  8. isReactive
  9. } from '@vue/reactivity'
  10. import { queueJob } from './scheduler'
  11. import {
  12. EMPTY_OBJ,
  13. isObject,
  14. isArray,
  15. isFunction,
  16. isString,
  17. hasChanged,
  18. NOOP,
  19. remove
  20. } from '@vue/shared'
  21. import {
  22. currentInstance,
  23. ComponentInternalInstance,
  24. isInSSRComponentSetup,
  25. recordInstanceBoundEffect
  26. } from './component'
  27. import {
  28. ErrorCodes,
  29. callWithErrorHandling,
  30. callWithAsyncErrorHandling
  31. } from './errorHandling'
  32. import { queuePostRenderEffect } from './renderer'
  33. import { warn } from './warning'
  34. export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void
  35. export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
  36. export type WatchCallback<V = any, OV = any> = (
  37. value: V,
  38. oldValue: OV,
  39. onInvalidate: InvalidateCbRegistrator
  40. ) => any
  41. type MapSources<T> = {
  42. [K in keyof T]: T[K] extends WatchSource<infer V>
  43. ? V
  44. : T[K] extends object ? T[K] : never
  45. }
  46. type MapOldSources<T, Immediate> = {
  47. [K in keyof T]: T[K] extends WatchSource<infer V>
  48. ? Immediate extends true ? (V | undefined) : V
  49. : T[K] extends object
  50. ? Immediate extends true ? (T[K] | undefined) : T[K]
  51. : never
  52. }
  53. type InvalidateCbRegistrator = (cb: () => void) => void
  54. export interface WatchOptionsBase {
  55. flush?: 'pre' | 'post' | 'sync'
  56. onTrack?: ReactiveEffectOptions['onTrack']
  57. onTrigger?: ReactiveEffectOptions['onTrigger']
  58. }
  59. export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
  60. immediate?: Immediate
  61. deep?: boolean
  62. }
  63. export type WatchStopHandle = () => void
  64. const invoke = (fn: Function) => fn()
  65. // Simple effect.
  66. export function watchEffect(
  67. effect: WatchEffect,
  68. options?: WatchOptionsBase
  69. ): WatchStopHandle {
  70. return doWatch(effect, null, options)
  71. }
  72. // initial value for watchers to trigger on undefined initial values
  73. const INITIAL_WATCHER_VALUE = {}
  74. // overload #1: array of multiple sources + cb
  75. // Readonly constraint helps the callback to correctly infer value types based
  76. // on position in the source array. Otherwise the values will get a union type
  77. // of all possible value types.
  78. export function watch<
  79. T extends Readonly<Array<WatchSource<unknown> | object>>,
  80. Immediate extends Readonly<boolean> = false
  81. >(
  82. sources: T,
  83. cb: WatchCallback<MapSources<T>, MapOldSources<T, Immediate>>,
  84. options?: WatchOptions<Immediate>
  85. ): WatchStopHandle
  86. // overload #2: single source + cb
  87. export function watch<T, Immediate extends Readonly<boolean> = false>(
  88. source: WatchSource<T>,
  89. cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
  90. options?: WatchOptions<Immediate>
  91. ): WatchStopHandle
  92. // overload #3: watching reactive object w/ cb
  93. export function watch<
  94. T extends object,
  95. Immediate extends Readonly<boolean> = false
  96. >(
  97. source: T,
  98. cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
  99. options?: WatchOptions<Immediate>
  100. ): WatchStopHandle
  101. // implementation
  102. export function watch<T = any>(
  103. source: WatchSource<T> | WatchSource<T>[],
  104. cb: WatchCallback<T>,
  105. options?: WatchOptions
  106. ): WatchStopHandle {
  107. if (__DEV__ && !isFunction(cb)) {
  108. warn(
  109. `\`watch(fn, options?)\` signature has been moved to a separate API. ` +
  110. `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
  111. `supports \`watch(source, cb, options?) signature.`
  112. )
  113. }
  114. return doWatch(source, cb, options)
  115. }
  116. function doWatch(
  117. source: WatchSource | WatchSource[] | WatchEffect,
  118. cb: WatchCallback | null,
  119. { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
  120. instance = currentInstance
  121. ): WatchStopHandle {
  122. if (__DEV__ && !cb) {
  123. if (immediate !== undefined) {
  124. warn(
  125. `watch() "immediate" option is only respected when using the ` +
  126. `watch(source, callback, options?) signature.`
  127. )
  128. }
  129. if (deep !== undefined) {
  130. warn(
  131. `watch() "deep" option is only respected when using the ` +
  132. `watch(source, callback, options?) signature.`
  133. )
  134. }
  135. }
  136. const warnInvalidSource = (s: unknown) => {
  137. warn(
  138. `Invalid watch source: `,
  139. s,
  140. `A watch source can only be a getter/effect function, a ref, ` +
  141. `a reactive object, or an array of these types.`
  142. )
  143. }
  144. let getter: () => any
  145. if (isArray(source)) {
  146. getter = () =>
  147. source.map(s => {
  148. if (isRef(s)) {
  149. return s.value
  150. } else if (isReactive(s)) {
  151. return traverse(s)
  152. } else if (isFunction(s)) {
  153. return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
  154. } else {
  155. __DEV__ && warnInvalidSource(s)
  156. }
  157. })
  158. } else if (isRef(source)) {
  159. getter = () => source.value
  160. } else if (isReactive(source)) {
  161. getter = () => source
  162. deep = true
  163. } else if (isFunction(source)) {
  164. if (cb) {
  165. // getter with cb
  166. getter = () =>
  167. callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
  168. } else {
  169. // no cb -> simple effect
  170. getter = () => {
  171. if (instance && instance.isUnmounted) {
  172. return
  173. }
  174. if (cleanup) {
  175. cleanup()
  176. }
  177. return callWithErrorHandling(
  178. source,
  179. instance,
  180. ErrorCodes.WATCH_CALLBACK,
  181. [onInvalidate]
  182. )
  183. }
  184. }
  185. } else {
  186. getter = NOOP
  187. __DEV__ && warnInvalidSource(source)
  188. }
  189. if (cb && deep) {
  190. const baseGetter = getter
  191. getter = () => traverse(baseGetter())
  192. }
  193. let cleanup: () => void
  194. const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
  195. cleanup = runner.options.onStop = () => {
  196. callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  197. }
  198. }
  199. // in SSR there is no need to setup an actual effect, and it should be noop
  200. // unless it's eager
  201. if (__NODE_JS__ && isInSSRComponentSetup) {
  202. if (!cb) {
  203. getter()
  204. } else if (immediate) {
  205. callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
  206. getter(),
  207. undefined,
  208. onInvalidate
  209. ])
  210. }
  211. return NOOP
  212. }
  213. let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
  214. const applyCb = cb
  215. ? () => {
  216. if (instance && instance.isUnmounted) {
  217. return
  218. }
  219. const newValue = runner()
  220. if (deep || hasChanged(newValue, oldValue)) {
  221. // cleanup before running cb again
  222. if (cleanup) {
  223. cleanup()
  224. }
  225. callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
  226. newValue,
  227. // pass undefined as the old value when it's changed for the first time
  228. oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
  229. onInvalidate
  230. ])
  231. oldValue = newValue
  232. }
  233. }
  234. : void 0
  235. let scheduler: (job: () => any) => void
  236. if (flush === 'sync') {
  237. scheduler = invoke
  238. } else if (flush === 'pre') {
  239. scheduler = job => {
  240. if (!instance || instance.isMounted) {
  241. queueJob(job)
  242. } else {
  243. // with 'pre' option, the first call must happen before
  244. // the component is mounted so it is called synchronously.
  245. job()
  246. }
  247. }
  248. } else {
  249. scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
  250. }
  251. const runner = effect(getter, {
  252. lazy: true,
  253. // so it runs before component update effects in pre flush mode
  254. computed: true,
  255. onTrack,
  256. onTrigger,
  257. scheduler: applyCb ? () => scheduler(applyCb) : scheduler
  258. })
  259. recordInstanceBoundEffect(runner)
  260. // initial run
  261. if (applyCb) {
  262. if (immediate) {
  263. applyCb()
  264. } else {
  265. oldValue = runner()
  266. }
  267. } else {
  268. runner()
  269. }
  270. return () => {
  271. stop(runner)
  272. if (instance) {
  273. remove(instance.effects!, runner)
  274. }
  275. }
  276. }
  277. // this.$watch
  278. export function instanceWatch(
  279. this: ComponentInternalInstance,
  280. source: string | Function,
  281. cb: Function,
  282. options?: WatchOptions
  283. ): WatchStopHandle {
  284. const publicThis = this.proxy as any
  285. const getter = isString(source)
  286. ? () => publicThis[source]
  287. : source.bind(publicThis)
  288. return doWatch(getter, cb.bind(publicThis), options, this)
  289. }
  290. function traverse(value: unknown, seen: Set<unknown> = new Set()) {
  291. if (!isObject(value) || seen.has(value)) {
  292. return value
  293. }
  294. seen.add(value)
  295. if (isArray(value)) {
  296. for (let i = 0; i < value.length; i++) {
  297. traverse(value[i], seen)
  298. }
  299. } else if (value instanceof Map) {
  300. value.forEach((v, key) => {
  301. // to register mutation dep for existing keys
  302. traverse(value.get(key), seen)
  303. })
  304. } else if (value instanceof Set) {
  305. value.forEach(v => {
  306. traverse(v, seen)
  307. })
  308. } else {
  309. for (const key in value) {
  310. traverse(value[key], seen)
  311. }
  312. }
  313. return value
  314. }