baseWatch.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import {
  2. EMPTY_OBJ,
  3. NOOP,
  4. hasChanged,
  5. isArray,
  6. isFunction,
  7. isMap,
  8. isObject,
  9. isPlainObject,
  10. isPromise,
  11. isSet,
  12. } from '@vue/shared'
  13. import { warn } from './warning'
  14. import type { ComputedRef } from './computed'
  15. import { ReactiveFlags } from './constants'
  16. import {
  17. type DebuggerOptions,
  18. EffectFlags,
  19. ReactiveEffect,
  20. pauseTracking,
  21. resetTracking,
  22. } from './effect'
  23. import { isReactive, isShallow } from './reactive'
  24. import { type Ref, isRef } from './ref'
  25. import { type SchedulerJob, SchedulerJobFlags } from './scheduler'
  26. // These errors were transferred from `packages/runtime-core/src/errorHandling.ts`
  27. // along with baseWatch to maintain code compatibility. Hence,
  28. // it is essential to keep these values unchanged.
  29. export enum BaseWatchErrorCodes {
  30. WATCH_GETTER = 2,
  31. WATCH_CALLBACK,
  32. WATCH_CLEANUP,
  33. }
  34. type WatchEffect = (onCleanup: OnCleanup) => void
  35. type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
  36. type WatchCallback<V = any, OV = any> = (
  37. value: V,
  38. oldValue: OV,
  39. onCleanup: OnCleanup,
  40. ) => any
  41. type OnCleanup = (cleanupFn: () => void) => void
  42. export interface BaseWatchOptions<Immediate = boolean> extends DebuggerOptions {
  43. immediate?: Immediate
  44. deep?: boolean
  45. once?: boolean
  46. scheduler?: WatchScheduler
  47. onError?: HandleError
  48. onWarn?: HandleWarn
  49. }
  50. // initial value for watchers to trigger on undefined initial values
  51. const INITIAL_WATCHER_VALUE = {}
  52. export type WatchScheduler = (
  53. job: SchedulerJob,
  54. effect: ReactiveEffect,
  55. immediateFirstRun: boolean,
  56. hasCb: boolean,
  57. ) => void
  58. export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void
  59. export type HandleWarn = (msg: string, ...args: any[]) => void
  60. const DEFAULT_SCHEDULER: WatchScheduler = (
  61. job,
  62. effect,
  63. immediateFirstRun,
  64. hasCb,
  65. ) => {
  66. if (immediateFirstRun) {
  67. !hasCb && effect.run()
  68. } else {
  69. job()
  70. }
  71. }
  72. const DEFAULT_HANDLE_ERROR: HandleError = (err: unknown) => {
  73. throw err
  74. }
  75. const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()
  76. let activeWatcher: ReactiveEffect | undefined = undefined
  77. /**
  78. * Returns the current active effect if there is one.
  79. */
  80. export function getCurrentWatcher() {
  81. return activeWatcher
  82. }
  83. /**
  84. * Registers a cleanup callback on the current active effect. This
  85. * registered cleanup callback will be invoked right before the
  86. * associated effect re-runs.
  87. *
  88. * @param cleanupFn - The callback function to attach to the effect's cleanup.
  89. */
  90. export function onWatcherCleanup(cleanupFn: () => void, failSilently = false) {
  91. if (activeWatcher) {
  92. const cleanups =
  93. cleanupMap.get(activeWatcher) ||
  94. cleanupMap.set(activeWatcher, []).get(activeWatcher)!
  95. cleanups.push(cleanupFn)
  96. } else if (__DEV__ && !failSilently) {
  97. warn(
  98. `onWatcherCleanup() was called when there was no active watcher` +
  99. ` to associate with.`,
  100. )
  101. }
  102. }
  103. export function baseWatch(
  104. source: WatchSource | WatchSource[] | WatchEffect | object,
  105. cb?: WatchCallback | null,
  106. {
  107. immediate,
  108. deep,
  109. once,
  110. scheduler = DEFAULT_SCHEDULER,
  111. onWarn = __DEV__ ? warn : NOOP,
  112. onError = DEFAULT_HANDLE_ERROR,
  113. onTrack,
  114. onTrigger,
  115. }: BaseWatchOptions = EMPTY_OBJ,
  116. ): ReactiveEffect | undefined {
  117. const warnInvalidSource = (s: unknown) => {
  118. onWarn(
  119. `Invalid watch source: `,
  120. s,
  121. `A watch source can only be a getter/effect function, a ref, ` +
  122. `a reactive object, or an array of these types.`,
  123. )
  124. }
  125. const reactiveGetter = (source: object) =>
  126. deep === true
  127. ? source // traverse will happen in wrapped getter below
  128. : // for deep: false, only traverse root-level properties
  129. traverse(source, deep === false ? 1 : undefined)
  130. let effect: ReactiveEffect
  131. let getter: () => any
  132. let cleanup: (() => void) | undefined
  133. let forceTrigger = false
  134. let isMultiSource = false
  135. if (isRef(source)) {
  136. getter = () => source.value
  137. forceTrigger = isShallow(source)
  138. } else if (isReactive(source)) {
  139. getter = () => reactiveGetter(source)
  140. forceTrigger = true
  141. } else if (isArray(source)) {
  142. isMultiSource = true
  143. forceTrigger = source.some(s => isReactive(s) || isShallow(s))
  144. getter = () =>
  145. source.map(s => {
  146. if (isRef(s)) {
  147. return s.value
  148. } else if (isReactive(s)) {
  149. return reactiveGetter(s)
  150. } else if (isFunction(s)) {
  151. return callWithErrorHandling(
  152. s,
  153. onError,
  154. BaseWatchErrorCodes.WATCH_GETTER,
  155. )
  156. } else {
  157. __DEV__ && warnInvalidSource(s)
  158. }
  159. })
  160. } else if (isFunction(source)) {
  161. if (cb) {
  162. // getter with cb
  163. getter = () =>
  164. callWithErrorHandling(source, onError, BaseWatchErrorCodes.WATCH_GETTER)
  165. } else {
  166. // no cb -> simple effect
  167. getter = () => {
  168. if (cleanup) {
  169. pauseTracking()
  170. try {
  171. cleanup()
  172. } finally {
  173. resetTracking()
  174. }
  175. }
  176. const currentEffect = activeWatcher
  177. activeWatcher = effect
  178. try {
  179. return callWithAsyncErrorHandling(
  180. source,
  181. onError,
  182. BaseWatchErrorCodes.WATCH_CALLBACK,
  183. [onWatcherCleanup],
  184. )
  185. } finally {
  186. activeWatcher = currentEffect
  187. }
  188. }
  189. }
  190. } else {
  191. getter = NOOP
  192. __DEV__ && warnInvalidSource(source)
  193. }
  194. if (cb && deep) {
  195. const baseGetter = getter
  196. getter = () => traverse(baseGetter())
  197. }
  198. if (once) {
  199. if (cb) {
  200. const _cb = cb
  201. cb = (...args) => {
  202. _cb(...args)
  203. effect?.stop()
  204. }
  205. } else {
  206. const _getter = getter
  207. getter = () => {
  208. _getter()
  209. effect?.stop()
  210. }
  211. }
  212. }
  213. let oldValue: any = isMultiSource
  214. ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
  215. : INITIAL_WATCHER_VALUE
  216. const job: SchedulerJob = (immediateFirstRun?: boolean) => {
  217. if (
  218. !(effect.flags & EffectFlags.ACTIVE) ||
  219. (!effect.dirty && !immediateFirstRun)
  220. ) {
  221. return
  222. }
  223. if (cb) {
  224. // watch(source, cb)
  225. const newValue = effect.run()
  226. if (
  227. deep ||
  228. forceTrigger ||
  229. (isMultiSource
  230. ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
  231. : hasChanged(newValue, oldValue))
  232. ) {
  233. // cleanup before running cb again
  234. if (cleanup) {
  235. cleanup()
  236. }
  237. const currentWatcher = activeWatcher
  238. activeWatcher = effect
  239. try {
  240. callWithAsyncErrorHandling(
  241. cb!,
  242. onError,
  243. BaseWatchErrorCodes.WATCH_CALLBACK,
  244. [
  245. newValue,
  246. // pass undefined as the old value when it's changed for the first time
  247. oldValue === INITIAL_WATCHER_VALUE
  248. ? undefined
  249. : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
  250. ? []
  251. : oldValue,
  252. onWatcherCleanup,
  253. ],
  254. )
  255. oldValue = newValue
  256. } finally {
  257. activeWatcher = currentWatcher
  258. }
  259. }
  260. } else {
  261. // watchEffect
  262. effect.run()
  263. }
  264. }
  265. // important: mark the job as a watcher callback so that scheduler knows
  266. // it is allowed to self-trigger (#1727)
  267. if (cb) job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
  268. effect = new ReactiveEffect(getter)
  269. effect.scheduler = () => scheduler(job, effect, false, !!cb)
  270. cleanup = effect.onStop = () => {
  271. const cleanups = cleanupMap.get(effect)
  272. if (cleanups) {
  273. cleanups.forEach(cleanup =>
  274. callWithErrorHandling(
  275. cleanup,
  276. onError,
  277. BaseWatchErrorCodes.WATCH_CLEANUP,
  278. ),
  279. )
  280. cleanupMap.delete(effect)
  281. }
  282. }
  283. if (__DEV__) {
  284. effect.onTrack = onTrack
  285. effect.onTrigger = onTrigger
  286. }
  287. // initial run
  288. if (cb) {
  289. scheduler(job, effect, true, !!cb)
  290. if (immediate) {
  291. job(true)
  292. } else {
  293. oldValue = effect.run()
  294. }
  295. } else {
  296. scheduler(job, effect, true, !!cb)
  297. }
  298. return effect
  299. }
  300. export function traverse(
  301. value: unknown,
  302. depth?: number,
  303. currentDepth = 0,
  304. seen?: Set<unknown>,
  305. ) {
  306. if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
  307. return value
  308. }
  309. if (depth && depth > 0) {
  310. if (currentDepth >= depth) {
  311. return value
  312. }
  313. currentDepth++
  314. }
  315. seen = seen || new Set()
  316. if (seen.has(value)) {
  317. return value
  318. }
  319. seen.add(value)
  320. if (isRef(value)) {
  321. traverse(value.value, depth, currentDepth, seen)
  322. } else if (isArray(value)) {
  323. for (let i = 0; i < value.length; i++) {
  324. traverse(value[i], depth, currentDepth, seen)
  325. }
  326. } else if (isSet(value) || isMap(value)) {
  327. value.forEach((v: any) => {
  328. traverse(v, depth, currentDepth, seen)
  329. })
  330. } else if (isPlainObject(value)) {
  331. for (const key in value) {
  332. traverse(value[key], depth, currentDepth, seen)
  333. }
  334. }
  335. return value
  336. }
  337. function callWithErrorHandling(
  338. fn: Function,
  339. handleError: HandleError,
  340. type: BaseWatchErrorCodes,
  341. args?: unknown[],
  342. ) {
  343. let res
  344. try {
  345. res = args ? fn(...args) : fn()
  346. } catch (err) {
  347. handleError(err, type)
  348. }
  349. return res
  350. }
  351. function callWithAsyncErrorHandling(
  352. fn: Function | Function[],
  353. handleError: HandleError,
  354. type: BaseWatchErrorCodes,
  355. args?: unknown[],
  356. ): any[] {
  357. if (isFunction(fn)) {
  358. const res = callWithErrorHandling(fn, handleError, type, args)
  359. if (res && isPromise(res)) {
  360. res.catch(err => {
  361. handleError(err, type)
  362. })
  363. }
  364. return res
  365. }
  366. const values = []
  367. for (let i = 0; i < fn.length; i++) {
  368. values.push(callWithAsyncErrorHandling(fn[i], handleError, type, args))
  369. }
  370. return values
  371. }