effect.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. import { extend, hasChanged } from '@vue/shared'
  2. import type { ComputedRefImpl } from './computed'
  3. import type { TrackOpTypes, TriggerOpTypes } from './constants'
  4. import { type Link, globalVersion, targetMap } from './dep'
  5. import { activeEffectScope } from './effectScope'
  6. import { warn } from './warning'
  7. export type EffectScheduler = (...args: any[]) => any
  8. export type DebuggerEvent = {
  9. effect: Subscriber
  10. } & DebuggerEventExtraInfo
  11. export type DebuggerEventExtraInfo = {
  12. target: object
  13. type: TrackOpTypes | TriggerOpTypes
  14. key: any
  15. newValue?: any
  16. oldValue?: any
  17. oldTarget?: Map<any, any> | Set<any>
  18. }
  19. export interface DebuggerOptions {
  20. onTrack?: (event: DebuggerEvent) => void
  21. onTrigger?: (event: DebuggerEvent) => void
  22. }
  23. export interface ReactiveEffectOptions extends DebuggerOptions {
  24. scheduler?: EffectScheduler
  25. allowRecurse?: boolean
  26. onStop?: () => void
  27. }
  28. export interface ReactiveEffectRunner<T = any> {
  29. (): T
  30. effect: ReactiveEffect
  31. }
  32. export let activeSub: Subscriber | undefined
  33. export enum EffectFlags {
  34. /**
  35. * ReactiveEffect only
  36. */
  37. ACTIVE = 1 << 0,
  38. RUNNING = 1 << 1,
  39. TRACKING = 1 << 2,
  40. NOTIFIED = 1 << 3,
  41. DIRTY = 1 << 4,
  42. ALLOW_RECURSE = 1 << 5,
  43. PAUSED = 1 << 6,
  44. }
  45. /**
  46. * Subscriber is a type that tracks (or subscribes to) a list of deps.
  47. */
  48. export interface Subscriber extends DebuggerOptions {
  49. /**
  50. * Head of the doubly linked list representing the deps
  51. * @internal
  52. */
  53. deps?: Link
  54. /**
  55. * Tail of the same list
  56. * @internal
  57. */
  58. depsTail?: Link
  59. /**
  60. * @internal
  61. */
  62. flags: EffectFlags
  63. /**
  64. * @internal
  65. */
  66. next?: Subscriber
  67. /**
  68. * returning `true` indicates it's a computed that needs to call notify
  69. * on its dep too
  70. * @internal
  71. */
  72. notify(): true | void
  73. }
  74. const pausedQueueEffects = new WeakSet<ReactiveEffect>()
  75. export class ReactiveEffect<T = any>
  76. implements Subscriber, ReactiveEffectOptions
  77. {
  78. /**
  79. * @internal
  80. */
  81. deps?: Link = undefined
  82. /**
  83. * @internal
  84. */
  85. depsTail?: Link = undefined
  86. /**
  87. * @internal
  88. */
  89. flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING
  90. /**
  91. * @internal
  92. */
  93. next?: Subscriber = undefined
  94. /**
  95. * @internal
  96. */
  97. cleanup?: () => void = undefined
  98. scheduler?: EffectScheduler = undefined
  99. onStop?: () => void
  100. onTrack?: (event: DebuggerEvent) => void
  101. onTrigger?: (event: DebuggerEvent) => void
  102. constructor(public fn: () => T) {
  103. if (activeEffectScope && activeEffectScope.active) {
  104. activeEffectScope.effects.push(this)
  105. }
  106. }
  107. pause(): void {
  108. this.flags |= EffectFlags.PAUSED
  109. }
  110. resume(): void {
  111. if (this.flags & EffectFlags.PAUSED) {
  112. this.flags &= ~EffectFlags.PAUSED
  113. if (pausedQueueEffects.has(this)) {
  114. pausedQueueEffects.delete(this)
  115. this.trigger()
  116. }
  117. }
  118. }
  119. /**
  120. * @internal
  121. */
  122. notify(): void {
  123. if (
  124. this.flags & EffectFlags.RUNNING &&
  125. !(this.flags & EffectFlags.ALLOW_RECURSE)
  126. ) {
  127. return
  128. }
  129. if (!(this.flags & EffectFlags.NOTIFIED)) {
  130. batch(this)
  131. }
  132. }
  133. run(): T {
  134. // TODO cleanupEffect
  135. if (!(this.flags & EffectFlags.ACTIVE)) {
  136. // stopped during cleanup
  137. return this.fn()
  138. }
  139. this.flags |= EffectFlags.RUNNING
  140. cleanupEffect(this)
  141. prepareDeps(this)
  142. const prevEffect = activeSub
  143. const prevShouldTrack = shouldTrack
  144. activeSub = this
  145. shouldTrack = true
  146. try {
  147. return this.fn()
  148. } finally {
  149. if (__DEV__ && activeSub !== this) {
  150. warn(
  151. 'Active effect was not restored correctly - ' +
  152. 'this is likely a Vue internal bug.',
  153. )
  154. }
  155. cleanupDeps(this)
  156. activeSub = prevEffect
  157. shouldTrack = prevShouldTrack
  158. this.flags &= ~EffectFlags.RUNNING
  159. }
  160. }
  161. stop(): void {
  162. if (this.flags & EffectFlags.ACTIVE) {
  163. for (let link = this.deps; link; link = link.nextDep) {
  164. removeSub(link)
  165. }
  166. this.deps = this.depsTail = undefined
  167. cleanupEffect(this)
  168. this.onStop && this.onStop()
  169. this.flags &= ~EffectFlags.ACTIVE
  170. }
  171. }
  172. trigger(): void {
  173. if (this.flags & EffectFlags.PAUSED) {
  174. pausedQueueEffects.add(this)
  175. } else if (this.scheduler) {
  176. this.scheduler()
  177. } else {
  178. this.runIfDirty()
  179. }
  180. }
  181. /**
  182. * @internal
  183. */
  184. runIfDirty(): void {
  185. if (isDirty(this)) {
  186. this.run()
  187. }
  188. }
  189. get dirty(): boolean {
  190. return isDirty(this)
  191. }
  192. }
  193. /**
  194. * For debugging
  195. */
  196. // function printDeps(sub: Subscriber) {
  197. // let d = sub.deps
  198. // let ds = []
  199. // while (d) {
  200. // ds.push(d)
  201. // d = d.nextDep
  202. // }
  203. // return ds.map(d => ({
  204. // id: d.id,
  205. // prev: d.prevDep?.id,
  206. // next: d.nextDep?.id,
  207. // }))
  208. // }
  209. let batchDepth = 0
  210. let batchedSub: Subscriber | undefined
  211. export function batch(sub: Subscriber): void {
  212. sub.flags |= EffectFlags.NOTIFIED
  213. sub.next = batchedSub
  214. batchedSub = sub
  215. }
  216. /**
  217. * @internal
  218. */
  219. export function startBatch(): void {
  220. batchDepth++
  221. }
  222. /**
  223. * Run batched effects when all batches have ended
  224. * @internal
  225. */
  226. export function endBatch(): void {
  227. if (--batchDepth > 0) {
  228. return
  229. }
  230. let error: unknown
  231. while (batchedSub) {
  232. let e: Subscriber | undefined = batchedSub
  233. batchedSub = undefined
  234. while (e) {
  235. const next: Subscriber | undefined = e.next
  236. e.next = undefined
  237. e.flags &= ~EffectFlags.NOTIFIED
  238. if (e.flags & EffectFlags.ACTIVE) {
  239. try {
  240. // ACTIVE flag is effect-only
  241. ;(e as ReactiveEffect).trigger()
  242. } catch (err) {
  243. if (!error) error = err
  244. }
  245. }
  246. e = next
  247. }
  248. }
  249. if (error) throw error
  250. }
  251. function prepareDeps(sub: Subscriber) {
  252. // Prepare deps for tracking, starting from the head
  253. for (let link = sub.deps; link; link = link.nextDep) {
  254. // set all previous deps' (if any) version to -1 so that we can track
  255. // which ones are unused after the run
  256. link.version = -1
  257. // store previous active sub if link was being used in another context
  258. link.prevActiveLink = link.dep.activeLink
  259. link.dep.activeLink = link
  260. }
  261. }
  262. function cleanupDeps(sub: Subscriber) {
  263. // Cleanup unsued deps
  264. let head
  265. let tail = sub.depsTail
  266. let link = tail
  267. while (link) {
  268. const prev = link.prevDep
  269. if (link.version === -1) {
  270. if (link === tail) tail = prev
  271. // unused - remove it from the dep's subscribing effect list
  272. removeSub(link)
  273. // also remove it from this effect's dep list
  274. removeDep(link)
  275. } else {
  276. // The new head is the last node seen which wasn't removed
  277. // from the doubly-linked list
  278. head = link
  279. }
  280. // restore previous active link if any
  281. link.dep.activeLink = link.prevActiveLink
  282. link.prevActiveLink = undefined
  283. link = prev
  284. }
  285. // set the new head & tail
  286. sub.deps = head
  287. sub.depsTail = tail
  288. }
  289. function isDirty(sub: Subscriber): boolean {
  290. for (let link = sub.deps; link; link = link.nextDep) {
  291. if (
  292. link.dep.version !== link.version ||
  293. (link.dep.computed &&
  294. (refreshComputed(link.dep.computed) ||
  295. link.dep.version !== link.version))
  296. ) {
  297. return true
  298. }
  299. }
  300. // @ts-expect-error only for backwards compatibility where libs manually set
  301. // this flag - e.g. Pinia's testing module
  302. if (sub._dirty) {
  303. return true
  304. }
  305. return false
  306. }
  307. /**
  308. * Returning false indicates the refresh failed
  309. * @internal
  310. */
  311. export function refreshComputed(computed: ComputedRefImpl): undefined {
  312. if (
  313. computed.flags & EffectFlags.TRACKING &&
  314. !(computed.flags & EffectFlags.DIRTY)
  315. ) {
  316. return
  317. }
  318. computed.flags &= ~EffectFlags.DIRTY
  319. // Global version fast path when no reactive changes has happened since
  320. // last refresh.
  321. if (computed.globalVersion === globalVersion) {
  322. return
  323. }
  324. computed.globalVersion = globalVersion
  325. const dep = computed.dep
  326. computed.flags |= EffectFlags.RUNNING
  327. // In SSR there will be no render effect, so the computed has no subscriber
  328. // and therefore tracks no deps, thus we cannot rely on the dirty check.
  329. // Instead, computed always re-evaluate and relies on the globalVersion
  330. // fast path above for caching.
  331. if (
  332. dep.version > 0 &&
  333. !computed.isSSR &&
  334. computed.deps &&
  335. !isDirty(computed)
  336. ) {
  337. computed.flags &= ~EffectFlags.RUNNING
  338. return
  339. }
  340. const prevSub = activeSub
  341. const prevShouldTrack = shouldTrack
  342. activeSub = computed
  343. shouldTrack = true
  344. try {
  345. prepareDeps(computed)
  346. const value = computed.fn(computed._value)
  347. if (dep.version === 0 || hasChanged(value, computed._value)) {
  348. computed._value = value
  349. dep.version++
  350. }
  351. } catch (err) {
  352. dep.version++
  353. throw err
  354. } finally {
  355. activeSub = prevSub
  356. shouldTrack = prevShouldTrack
  357. cleanupDeps(computed)
  358. computed.flags &= ~EffectFlags.RUNNING
  359. }
  360. }
  361. function removeSub(link: Link, fromComputed = false) {
  362. const { dep, prevSub, nextSub } = link
  363. if (prevSub) {
  364. prevSub.nextSub = nextSub
  365. link.prevSub = undefined
  366. }
  367. if (nextSub) {
  368. nextSub.prevSub = prevSub
  369. link.nextSub = undefined
  370. }
  371. if (dep.subs === link) {
  372. // was previous tail, point new tail to prev
  373. dep.subs = prevSub
  374. }
  375. if (__DEV__ && dep.subsHead === link) {
  376. // was previous head, point new head to next
  377. dep.subsHead = nextSub
  378. }
  379. if (!dep.subs) {
  380. // last subscriber removed
  381. if (dep.computed) {
  382. // if computed, unsubscribe it from all its deps so this computed and its
  383. // value can be GCed
  384. dep.computed.flags &= ~EffectFlags.TRACKING
  385. for (let l = dep.computed.deps; l; l = l.nextDep) {
  386. removeSub(l, true)
  387. }
  388. } else if (dep.map && !fromComputed) {
  389. // property dep, remove it from the owner depsMap
  390. dep.map.delete(dep.key)
  391. if (!dep.map.size) targetMap.delete(dep.target!)
  392. }
  393. }
  394. }
  395. function removeDep(link: Link) {
  396. const { prevDep, nextDep } = link
  397. if (prevDep) {
  398. prevDep.nextDep = nextDep
  399. link.prevDep = undefined
  400. }
  401. if (nextDep) {
  402. nextDep.prevDep = prevDep
  403. link.nextDep = undefined
  404. }
  405. }
  406. export interface ReactiveEffectRunner<T = any> {
  407. (): T
  408. effect: ReactiveEffect
  409. }
  410. export function effect<T = any>(
  411. fn: () => T,
  412. options?: ReactiveEffectOptions,
  413. ): ReactiveEffectRunner<T> {
  414. if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
  415. fn = (fn as ReactiveEffectRunner).effect.fn
  416. }
  417. const e = new ReactiveEffect(fn)
  418. if (options) {
  419. extend(e, options)
  420. }
  421. try {
  422. e.run()
  423. } catch (err) {
  424. e.stop()
  425. throw err
  426. }
  427. const runner = e.run.bind(e) as ReactiveEffectRunner
  428. runner.effect = e
  429. return runner
  430. }
  431. /**
  432. * Stops the effect associated with the given runner.
  433. *
  434. * @param runner - Association with the effect to stop tracking.
  435. */
  436. export function stop(runner: ReactiveEffectRunner): void {
  437. runner.effect.stop()
  438. }
  439. /**
  440. * @internal
  441. */
  442. export let shouldTrack = true
  443. const trackStack: boolean[] = []
  444. /**
  445. * Temporarily pauses tracking.
  446. */
  447. export function pauseTracking(): void {
  448. trackStack.push(shouldTrack)
  449. shouldTrack = false
  450. }
  451. /**
  452. * Re-enables effect tracking (if it was paused).
  453. */
  454. export function enableTracking(): void {
  455. trackStack.push(shouldTrack)
  456. shouldTrack = true
  457. }
  458. /**
  459. * Resets the previous global effect tracking state.
  460. */
  461. export function resetTracking(): void {
  462. const last = trackStack.pop()
  463. shouldTrack = last === undefined ? true : last
  464. }
  465. /**
  466. * Registers a cleanup function for the current active effect.
  467. * The cleanup function is called right before the next effect run, or when the
  468. * effect is stopped.
  469. *
  470. * Throws a warning if there is no current active effect. The warning can be
  471. * suppressed by passing `true` to the second argument.
  472. *
  473. * @param fn - the cleanup function to be registered
  474. * @param failSilently - if `true`, will not throw warning when called without
  475. * an active effect.
  476. */
  477. export function onEffectCleanup(fn: () => void, failSilently = false): void {
  478. if (activeSub instanceof ReactiveEffect) {
  479. activeSub.cleanup = fn
  480. } else if (__DEV__ && !failSilently) {
  481. warn(
  482. `onEffectCleanup() was called when there was no active effect` +
  483. ` to associate with.`,
  484. )
  485. }
  486. }
  487. function cleanupEffect(e: ReactiveEffect) {
  488. const { cleanup } = e
  489. e.cleanup = undefined
  490. if (cleanup) {
  491. // run cleanup without active effect
  492. const prevSub = activeSub
  493. activeSub = undefined
  494. try {
  495. cleanup()
  496. } finally {
  497. activeSub = prevSub
  498. }
  499. }
  500. }