effect.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import { TrackOpTypes, TriggerOpTypes } from './operations'
  2. import { extend, isArray, isIntegerKey, isMap } from '@vue/shared'
  3. import { EffectScope, recordEffectScope } from './effectScope'
  4. import {
  5. createDep,
  6. Dep,
  7. finalizeDepMarkers,
  8. initDepMarkers,
  9. newTracked,
  10. wasTracked
  11. } from './Dep'
  12. // The main WeakMap that stores {target -> key -> dep} connections.
  13. // Conceptually, it's easier to think of a dependency as a Dep class
  14. // which maintains a Set of subscribers, but we simply store them as
  15. // raw Sets to reduce memory overhead.
  16. type KeyToDepMap = Map<any, Dep>
  17. const targetMap = new WeakMap<any, KeyToDepMap>()
  18. // The number of effects currently being tracked recursively.
  19. let effectTrackDepth = 0
  20. export let trackOpBit = 1
  21. /**
  22. * The bitwise track markers support at most 30 levels op recursion.
  23. * This value is chosen to enable modern JS engines to use a SMI on all platforms.
  24. * When recursion depth is greater, fall back to using a full cleanup.
  25. */
  26. const maxMarkerBits = 30
  27. export type EffectScheduler = () => void
  28. export type DebuggerEvent = {
  29. effect: ReactiveEffect
  30. } & DebuggerEventExtraInfo
  31. export type DebuggerEventExtraInfo = {
  32. target: object
  33. type: TrackOpTypes | TriggerOpTypes
  34. key: any
  35. newValue?: any
  36. oldValue?: any
  37. oldTarget?: Map<any, any> | Set<any>
  38. }
  39. const effectStack: ReactiveEffect[] = []
  40. let activeEffect: ReactiveEffect | undefined
  41. export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
  42. export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
  43. export class ReactiveEffect<T = any> {
  44. active = true
  45. deps: Dep[] = []
  46. // can be attached after creation
  47. onStop?: () => void
  48. // dev only
  49. onTrack?: (event: DebuggerEvent) => void
  50. // dev only
  51. onTrigger?: (event: DebuggerEvent) => void
  52. constructor(
  53. public fn: () => T,
  54. public scheduler: EffectScheduler | null = null,
  55. scope?: EffectScope | null,
  56. // allow recursive self-invocation
  57. public allowRecurse = false
  58. ) {
  59. recordEffectScope(this, scope)
  60. }
  61. run() {
  62. if (!this.active) {
  63. return this.fn()
  64. }
  65. if (!effectStack.includes(this)) {
  66. try {
  67. effectStack.push((activeEffect = this))
  68. enableTracking()
  69. trackOpBit = 1 << ++effectTrackDepth
  70. if (effectTrackDepth <= maxMarkerBits) {
  71. initDepMarkers(this)
  72. } else {
  73. cleanupEffect(this)
  74. }
  75. return this.fn()
  76. } finally {
  77. if (effectTrackDepth <= maxMarkerBits) {
  78. finalizeDepMarkers(this)
  79. }
  80. trackOpBit = 1 << --effectTrackDepth
  81. resetTracking()
  82. effectStack.pop()
  83. const n = effectStack.length
  84. activeEffect = n > 0 ? effectStack[n - 1] : undefined
  85. }
  86. }
  87. }
  88. stop() {
  89. if (this.active) {
  90. cleanupEffect(this)
  91. if (this.onStop) {
  92. this.onStop()
  93. }
  94. this.active = false
  95. }
  96. }
  97. }
  98. function cleanupEffect(effect: ReactiveEffect) {
  99. const { deps } = effect
  100. if (deps.length) {
  101. for (let i = 0; i < deps.length; i++) {
  102. deps[i].delete(effect)
  103. }
  104. deps.length = 0
  105. }
  106. }
  107. export interface ReactiveEffectOptions {
  108. lazy?: boolean
  109. scheduler?: EffectScheduler
  110. scope?: EffectScope
  111. allowRecurse?: boolean
  112. onStop?: () => void
  113. onTrack?: (event: DebuggerEvent) => void
  114. onTrigger?: (event: DebuggerEvent) => void
  115. }
  116. export interface ReactiveEffectRunner<T = any> {
  117. (): T
  118. effect: ReactiveEffect
  119. }
  120. export function effect<T = any>(
  121. fn: () => T,
  122. options?: ReactiveEffectOptions
  123. ): ReactiveEffectRunner {
  124. if ((fn as ReactiveEffectRunner).effect) {
  125. fn = (fn as ReactiveEffectRunner).effect.fn
  126. }
  127. const _effect = new ReactiveEffect(fn)
  128. if (options) {
  129. extend(_effect, options)
  130. if (options.scope) recordEffectScope(_effect, options.scope)
  131. }
  132. if (!options || !options.lazy) {
  133. _effect.run()
  134. }
  135. const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  136. runner.effect = _effect
  137. return runner
  138. }
  139. export function stop(runner: ReactiveEffectRunner) {
  140. runner.effect.stop()
  141. }
  142. let shouldTrack = true
  143. const trackStack: boolean[] = []
  144. export function pauseTracking() {
  145. trackStack.push(shouldTrack)
  146. shouldTrack = false
  147. }
  148. export function enableTracking() {
  149. trackStack.push(shouldTrack)
  150. shouldTrack = true
  151. }
  152. export function resetTracking() {
  153. const last = trackStack.pop()
  154. shouldTrack = last === undefined ? true : last
  155. }
  156. export function track(target: object, type: TrackOpTypes, key: unknown) {
  157. if (!isTracking()) {
  158. return
  159. }
  160. let depsMap = targetMap.get(target)
  161. if (!depsMap) {
  162. targetMap.set(target, (depsMap = new Map()))
  163. }
  164. let dep = depsMap.get(key)
  165. if (!dep) {
  166. depsMap.set(key, (dep = createDep()))
  167. }
  168. const eventInfo = __DEV__
  169. ? { effect: activeEffect, target, type, key }
  170. : undefined
  171. trackEffects(dep, eventInfo)
  172. }
  173. export function isTracking() {
  174. return shouldTrack && activeEffect !== undefined
  175. }
  176. export function trackEffects(
  177. dep: Dep,
  178. debuggerEventExtraInfo?: DebuggerEventExtraInfo
  179. ) {
  180. let shouldTrack = false
  181. if (effectTrackDepth <= maxMarkerBits) {
  182. if (!newTracked(dep)) {
  183. dep.n |= trackOpBit // set newly tracked
  184. shouldTrack = !wasTracked(dep)
  185. }
  186. } else {
  187. // Full cleanup mode.
  188. shouldTrack = !dep.has(activeEffect!)
  189. }
  190. if (shouldTrack) {
  191. dep.add(activeEffect!)
  192. activeEffect!.deps.push(dep)
  193. if (__DEV__ && activeEffect!.onTrack) {
  194. activeEffect!.onTrack(
  195. Object.assign(
  196. {
  197. effect: activeEffect!
  198. },
  199. debuggerEventExtraInfo
  200. )
  201. )
  202. }
  203. }
  204. }
  205. export function trigger(
  206. target: object,
  207. type: TriggerOpTypes,
  208. key?: unknown,
  209. newValue?: unknown,
  210. oldValue?: unknown,
  211. oldTarget?: Map<unknown, unknown> | Set<unknown>
  212. ) {
  213. const depsMap = targetMap.get(target)
  214. if (!depsMap) {
  215. // never been tracked
  216. return
  217. }
  218. let deps: (Dep | undefined)[] = []
  219. if (type === TriggerOpTypes.CLEAR) {
  220. // collection being cleared
  221. // trigger all effects for target
  222. deps = [...depsMap.values()]
  223. } else if (key === 'length' && isArray(target)) {
  224. depsMap.forEach((dep, key) => {
  225. if (key === 'length' || key >= (newValue as number)) {
  226. deps.push(dep)
  227. }
  228. })
  229. } else {
  230. // schedule runs for SET | ADD | DELETE
  231. if (key !== void 0) {
  232. deps.push(depsMap.get(key))
  233. }
  234. // also run for iteration key on ADD | DELETE | Map.SET
  235. switch (type) {
  236. case TriggerOpTypes.ADD:
  237. if (!isArray(target)) {
  238. deps.push(depsMap.get(ITERATE_KEY))
  239. if (isMap(target)) {
  240. deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
  241. }
  242. } else if (isIntegerKey(key)) {
  243. // new index added to array -> length changes
  244. deps.push(depsMap.get('length'))
  245. }
  246. break
  247. case TriggerOpTypes.DELETE:
  248. if (!isArray(target)) {
  249. deps.push(depsMap.get(ITERATE_KEY))
  250. if (isMap(target)) {
  251. deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
  252. }
  253. }
  254. break
  255. case TriggerOpTypes.SET:
  256. if (isMap(target)) {
  257. deps.push(depsMap.get(ITERATE_KEY))
  258. }
  259. break
  260. }
  261. }
  262. const eventInfo = __DEV__
  263. ? { target, type, key, newValue, oldValue, oldTarget }
  264. : undefined
  265. if (deps.length === 1) {
  266. if (deps[0]) {
  267. triggerEffects(deps[0], eventInfo)
  268. }
  269. } else {
  270. const effects: ReactiveEffect[] = []
  271. for (const dep of deps) {
  272. if (dep) {
  273. effects.push(...dep)
  274. }
  275. }
  276. triggerEffects(createDep(effects), eventInfo)
  277. }
  278. }
  279. export function triggerEffects(
  280. dep: Dep,
  281. debuggerEventExtraInfo?: DebuggerEventExtraInfo
  282. ) {
  283. // spread into array for stabilization
  284. for (const effect of [...dep]) {
  285. if (effect !== activeEffect || effect.allowRecurse) {
  286. if (__DEV__ && effect.onTrigger) {
  287. effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
  288. }
  289. if (effect.scheduler) {
  290. effect.scheduler()
  291. } else {
  292. effect.run()
  293. }
  294. }
  295. }
  296. }