effect.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import { extend } from '@vue/shared'
  2. import type { TrackOpTypes, TriggerOpTypes } from './constants'
  3. import { setupOnTrigger } from './debug'
  4. import { activeEffectScope } from './effectScope'
  5. import {
  6. type Link,
  7. ReactiveFlags,
  8. type ReactiveNode,
  9. activeSub,
  10. checkDirty,
  11. endTracking,
  12. link,
  13. setActiveSub,
  14. startTracking,
  15. unlink,
  16. } from './system'
  17. import { warn } from './warning'
  18. export type EffectScheduler = (...args: any[]) => any
  19. export type DebuggerEvent = {
  20. effect: ReactiveNode
  21. } & DebuggerEventExtraInfo
  22. export type DebuggerEventExtraInfo = {
  23. target: object
  24. type: TrackOpTypes | TriggerOpTypes
  25. key: any
  26. newValue?: any
  27. oldValue?: any
  28. oldTarget?: Map<any, any> | Set<any>
  29. }
  30. export interface DebuggerOptions {
  31. onTrack?: (event: DebuggerEvent) => void
  32. onTrigger?: (event: DebuggerEvent) => void
  33. }
  34. export interface ReactiveEffectOptions extends DebuggerOptions {
  35. scheduler?: EffectScheduler
  36. onStop?: () => void
  37. }
  38. export interface ReactiveEffectRunner<T = any> {
  39. (): T
  40. effect: ReactiveEffect
  41. }
  42. export enum EffectFlags {
  43. /**
  44. * ReactiveEffect only
  45. */
  46. ALLOW_RECURSE = 1 << 7,
  47. PAUSED = 1 << 8,
  48. STOP = 1 << 10,
  49. }
  50. export class ReactiveEffect<T = any>
  51. implements ReactiveEffectOptions, ReactiveNode
  52. {
  53. deps: Link | undefined = undefined
  54. depsTail: Link | undefined = undefined
  55. subs: Link | undefined = undefined
  56. subsTail: Link | undefined = undefined
  57. flags: number = ReactiveFlags.Watching | ReactiveFlags.Dirty
  58. /**
  59. * @internal
  60. */
  61. cleanups: (() => void)[] = []
  62. /**
  63. * @internal
  64. */
  65. cleanupsLength = 0
  66. // dev only
  67. onTrack?: (event: DebuggerEvent) => void
  68. // dev only
  69. onTrigger?: (event: DebuggerEvent) => void
  70. // @ts-expect-error
  71. fn(): T {}
  72. constructor(fn?: () => T) {
  73. if (fn !== undefined) {
  74. this.fn = fn
  75. }
  76. if (activeEffectScope) {
  77. link(this, activeEffectScope)
  78. }
  79. }
  80. get active(): boolean {
  81. return !(this.flags & EffectFlags.STOP)
  82. }
  83. pause(): void {
  84. this.flags |= EffectFlags.PAUSED
  85. }
  86. resume(): void {
  87. const flags = (this.flags &= ~EffectFlags.PAUSED)
  88. if (flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) {
  89. this.notify()
  90. }
  91. }
  92. notify(): void {
  93. if (!(this.flags & EffectFlags.PAUSED) && this.dirty) {
  94. this.run()
  95. }
  96. }
  97. run(): T {
  98. if (!this.active) {
  99. return this.fn()
  100. }
  101. cleanup(this)
  102. const prevSub = startTracking(this)
  103. try {
  104. return this.fn()
  105. } finally {
  106. endTracking(this, prevSub)
  107. const flags = this.flags
  108. if (
  109. (flags & (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)) ===
  110. (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)
  111. ) {
  112. this.flags = flags & ~ReactiveFlags.Recursed
  113. this.notify()
  114. }
  115. }
  116. }
  117. stop(): void {
  118. if (!this.active) {
  119. return
  120. }
  121. this.flags = EffectFlags.STOP
  122. let dep = this.deps
  123. while (dep !== undefined) {
  124. dep = unlink(dep, this)
  125. }
  126. const sub = this.subs
  127. if (sub !== undefined) {
  128. unlink(sub)
  129. }
  130. cleanup(this)
  131. }
  132. get dirty(): boolean {
  133. const flags = this.flags
  134. if (flags & ReactiveFlags.Dirty) {
  135. return true
  136. }
  137. if (flags & ReactiveFlags.Pending) {
  138. if (checkDirty(this.deps!, this)) {
  139. this.flags = flags | ReactiveFlags.Dirty
  140. return true
  141. } else {
  142. this.flags = flags & ~ReactiveFlags.Pending
  143. }
  144. }
  145. return false
  146. }
  147. }
  148. if (__DEV__) {
  149. setupOnTrigger(ReactiveEffect)
  150. }
  151. export function effect<T = any>(
  152. fn: () => T,
  153. options?: ReactiveEffectOptions,
  154. ): ReactiveEffectRunner<T> {
  155. if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
  156. fn = (fn as ReactiveEffectRunner).effect.fn
  157. }
  158. const e = new ReactiveEffect(fn)
  159. if (options) {
  160. const { onStop, scheduler } = options
  161. if (onStop) {
  162. options.onStop = undefined
  163. const stop = e.stop.bind(e)
  164. e.stop = () => {
  165. stop()
  166. onStop()
  167. }
  168. }
  169. if (scheduler) {
  170. options.scheduler = undefined
  171. e.notify = () => {
  172. if (!(e.flags & EffectFlags.PAUSED)) {
  173. scheduler()
  174. }
  175. }
  176. }
  177. extend(e, options)
  178. }
  179. try {
  180. e.run()
  181. } catch (err) {
  182. e.stop()
  183. throw err
  184. }
  185. const runner = e.run.bind(e) as ReactiveEffectRunner
  186. runner.effect = e
  187. return runner
  188. }
  189. /**
  190. * Stops the effect associated with the given runner.
  191. *
  192. * @param runner - Association with the effect to stop tracking.
  193. */
  194. export function stop(runner: ReactiveEffectRunner): void {
  195. runner.effect.stop()
  196. }
  197. const resetTrackingStack: (ReactiveNode | undefined)[] = []
  198. /**
  199. * Temporarily pauses tracking.
  200. */
  201. export function pauseTracking(): void {
  202. resetTrackingStack.push(activeSub)
  203. setActiveSub()
  204. }
  205. /**
  206. * Re-enables effect tracking (if it was paused).
  207. */
  208. export function enableTracking(): void {
  209. const isPaused = activeSub === undefined
  210. if (!isPaused) {
  211. // Add the current active effect to the trackResetStack so it can be
  212. // restored by calling resetTracking.
  213. resetTrackingStack.push(activeSub)
  214. } else {
  215. // Add a placeholder to the trackResetStack so we can it can be popped
  216. // to restore the previous active effect.
  217. resetTrackingStack.push(undefined)
  218. for (let i = resetTrackingStack.length - 1; i >= 0; i--) {
  219. if (resetTrackingStack[i] !== undefined) {
  220. setActiveSub(resetTrackingStack[i])
  221. break
  222. }
  223. }
  224. }
  225. }
  226. /**
  227. * Resets the previous global effect tracking state.
  228. */
  229. export function resetTracking(): void {
  230. if (__DEV__ && resetTrackingStack.length === 0) {
  231. warn(
  232. `resetTracking() was called when there was no active tracking ` +
  233. `to reset.`,
  234. )
  235. }
  236. if (resetTrackingStack.length) {
  237. setActiveSub(resetTrackingStack.pop()!)
  238. } else {
  239. setActiveSub()
  240. }
  241. }
  242. export function cleanup(
  243. sub: ReactiveNode & { cleanups: (() => void)[]; cleanupsLength: number },
  244. ): void {
  245. const l = sub.cleanupsLength
  246. if (l) {
  247. for (let i = 0; i < l; i++) {
  248. sub.cleanups[i]()
  249. }
  250. sub.cleanupsLength = 0
  251. }
  252. }
  253. /**
  254. * Registers a cleanup function for the current active effect.
  255. * The cleanup function is called right before the next effect run, or when the
  256. * effect is stopped.
  257. *
  258. * Throws a warning if there is no current active effect. The warning can be
  259. * suppressed by passing `true` to the second argument.
  260. *
  261. * @param fn - the cleanup function to be registered
  262. * @param failSilently - if `true`, will not throw warning when called without
  263. * an active effect.
  264. */
  265. export function onEffectCleanup(fn: () => void, failSilently = false): void {
  266. if (activeSub instanceof ReactiveEffect) {
  267. activeSub.cleanups[activeSub.cleanupsLength++] = () => cleanupEffect(fn)
  268. } else if (__DEV__ && !failSilently) {
  269. warn(
  270. `onEffectCleanup() was called when there was no active effect` +
  271. ` to associate with.`,
  272. )
  273. }
  274. }
  275. function cleanupEffect(fn: () => void) {
  276. // run cleanup without active effect
  277. const prevSub = setActiveSub()
  278. try {
  279. fn()
  280. } finally {
  281. setActiveSub(prevSub)
  282. }
  283. }