effect.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import { TrackOpTypes, TriggerOpTypes } from './operations'
  2. import { EMPTY_OBJ, extend, isArray } from '@vue/shared'
  3. // The main WeakMap that stores {target -> key -> dep} connections.
  4. // Conceptually, it's easier to think of a dependency as a Dep class
  5. // which maintains a Set of subscribers, but we simply store them as
  6. // raw Sets to reduce memory overhead.
  7. type Dep = Set<ReactiveEffect>
  8. type KeyToDepMap = Map<any, Dep>
  9. const targetMap = new WeakMap<any, KeyToDepMap>()
  10. export interface ReactiveEffect<T = any> {
  11. (): T
  12. _isEffect: true
  13. active: boolean
  14. raw: () => T
  15. deps: Array<Dep>
  16. options: ReactiveEffectOptions
  17. }
  18. export interface ReactiveEffectOptions {
  19. lazy?: boolean
  20. computed?: boolean
  21. scheduler?: (run: Function) => void
  22. onTrack?: (event: DebuggerEvent) => void
  23. onTrigger?: (event: DebuggerEvent) => void
  24. onStop?: () => void
  25. }
  26. export type DebuggerEvent = {
  27. effect: ReactiveEffect
  28. target: object
  29. type: TrackOpTypes | TriggerOpTypes
  30. key: any
  31. } & DebuggerEventExtraInfo
  32. export interface DebuggerEventExtraInfo {
  33. newValue?: any
  34. oldValue?: any
  35. oldTarget?: Map<any, any> | Set<any>
  36. }
  37. const effectStack: ReactiveEffect[] = []
  38. export let activeEffect: ReactiveEffect | undefined
  39. export const ITERATE_KEY = Symbol('iterate')
  40. export function isEffect(fn: any): fn is ReactiveEffect {
  41. return fn != null && fn._isEffect === true
  42. }
  43. export function effect<T = any>(
  44. fn: () => T,
  45. options: ReactiveEffectOptions = EMPTY_OBJ
  46. ): ReactiveEffect<T> {
  47. if (isEffect(fn)) {
  48. fn = fn.raw
  49. }
  50. const effect = createReactiveEffect(fn, options)
  51. if (!options.lazy) {
  52. effect()
  53. }
  54. return effect
  55. }
  56. export function stop(effect: ReactiveEffect) {
  57. if (effect.active) {
  58. cleanup(effect)
  59. if (effect.options.onStop) {
  60. effect.options.onStop()
  61. }
  62. effect.active = false
  63. }
  64. }
  65. function createReactiveEffect<T = any>(
  66. fn: () => T,
  67. options: ReactiveEffectOptions
  68. ): ReactiveEffect<T> {
  69. const effect = function reactiveEffect(...args: unknown[]): unknown {
  70. return run(effect, fn, args)
  71. } as ReactiveEffect
  72. effect._isEffect = true
  73. effect.active = true
  74. effect.raw = fn
  75. effect.deps = []
  76. effect.options = options
  77. return effect
  78. }
  79. function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  80. if (!effect.active) {
  81. return fn(...args)
  82. }
  83. if (!effectStack.includes(effect)) {
  84. cleanup(effect)
  85. try {
  86. enableTracking()
  87. effectStack.push(effect)
  88. activeEffect = effect
  89. return fn(...args)
  90. } finally {
  91. effectStack.pop()
  92. resetTracking()
  93. activeEffect = effectStack[effectStack.length - 1]
  94. }
  95. }
  96. }
  97. function cleanup(effect: ReactiveEffect) {
  98. const { deps } = effect
  99. if (deps.length) {
  100. for (let i = 0; i < deps.length; i++) {
  101. deps[i].delete(effect)
  102. }
  103. deps.length = 0
  104. }
  105. }
  106. let shouldTrack = true
  107. const trackStack: boolean[] = []
  108. export function pauseTracking() {
  109. trackStack.push(shouldTrack)
  110. shouldTrack = false
  111. }
  112. export function enableTracking() {
  113. trackStack.push(shouldTrack)
  114. shouldTrack = true
  115. }
  116. export function resetTracking() {
  117. const last = trackStack.pop()
  118. shouldTrack = last === undefined ? true : last
  119. }
  120. export function track(target: object, type: TrackOpTypes, key: unknown) {
  121. if (!shouldTrack || activeEffect === undefined) {
  122. return
  123. }
  124. let depsMap = targetMap.get(target)
  125. if (depsMap === void 0) {
  126. targetMap.set(target, (depsMap = new Map()))
  127. }
  128. let dep = depsMap.get(key)
  129. if (dep === void 0) {
  130. depsMap.set(key, (dep = new Set()))
  131. }
  132. if (!dep.has(activeEffect)) {
  133. dep.add(activeEffect)
  134. activeEffect.deps.push(dep)
  135. if (__DEV__ && activeEffect.options.onTrack) {
  136. activeEffect.options.onTrack({
  137. effect: activeEffect,
  138. target,
  139. type,
  140. key
  141. })
  142. }
  143. }
  144. }
  145. export function trigger(
  146. target: object,
  147. type: TriggerOpTypes,
  148. key?: unknown,
  149. extraInfo?: DebuggerEventExtraInfo
  150. ) {
  151. const depsMap = targetMap.get(target)
  152. if (depsMap === void 0) {
  153. // never been tracked
  154. return
  155. }
  156. const effects = new Set<ReactiveEffect>()
  157. const computedRunners = new Set<ReactiveEffect>()
  158. if (type === TriggerOpTypes.CLEAR) {
  159. // collection being cleared, trigger all effects for target
  160. depsMap.forEach(dep => {
  161. addRunners(effects, computedRunners, dep)
  162. })
  163. } else {
  164. // schedule runs for SET | ADD | DELETE
  165. if (key !== void 0) {
  166. addRunners(effects, computedRunners, depsMap.get(key))
  167. }
  168. // also run for iteration key on ADD | DELETE
  169. if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
  170. const iterationKey = isArray(target) ? 'length' : ITERATE_KEY
  171. addRunners(effects, computedRunners, depsMap.get(iterationKey))
  172. }
  173. }
  174. const run = (effect: ReactiveEffect) => {
  175. scheduleRun(effect, target, type, key, extraInfo)
  176. }
  177. // Important: computed effects must be run first so that computed getters
  178. // can be invalidated before any normal effects that depend on them are run.
  179. computedRunners.forEach(run)
  180. effects.forEach(run)
  181. }
  182. function addRunners(
  183. effects: Set<ReactiveEffect>,
  184. computedRunners: Set<ReactiveEffect>,
  185. effectsToAdd: Set<ReactiveEffect> | undefined
  186. ) {
  187. if (effectsToAdd !== void 0) {
  188. effectsToAdd.forEach(effect => {
  189. if (effect.options.computed) {
  190. computedRunners.add(effect)
  191. } else {
  192. effects.add(effect)
  193. }
  194. })
  195. }
  196. }
  197. function scheduleRun(
  198. effect: ReactiveEffect,
  199. target: object,
  200. type: TriggerOpTypes,
  201. key: unknown,
  202. extraInfo?: DebuggerEventExtraInfo
  203. ) {
  204. if (__DEV__ && effect.options.onTrigger) {
  205. const event: DebuggerEvent = {
  206. effect,
  207. target,
  208. key,
  209. type
  210. }
  211. effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event)
  212. }
  213. if (effect.options.scheduler !== void 0) {
  214. effect.options.scheduler(effect)
  215. } else {
  216. effect()
  217. }
  218. }