| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- import { TrackOpTypes, TriggerOpTypes } from './operations'
- import { extend, isArray, isIntegerKey, isMap } from '@vue/shared'
- import { EffectScope, recordEffectScope } from './effectScope'
- import {
- createDep,
- Dep,
- finalizeDepMarkers,
- initDepMarkers,
- newTracked,
- wasTracked
- } from './Dep'
- // The main WeakMap that stores {target -> key -> dep} connections.
- // Conceptually, it's easier to think of a dependency as a Dep class
- // which maintains a Set of subscribers, but we simply store them as
- // raw Sets to reduce memory overhead.
- type KeyToDepMap = Map<any, Dep>
- const targetMap = new WeakMap<any, KeyToDepMap>()
- // The number of effects currently being tracked recursively.
- let effectTrackDepth = 0
- export let trackOpBit = 1
- /**
- * The bitwise track markers support at most 30 levels op recursion.
- * This value is chosen to enable modern JS engines to use a SMI on all platforms.
- * When recursion depth is greater, fall back to using a full cleanup.
- */
- const maxMarkerBits = 30
- export type EffectScheduler = () => void
- export type DebuggerEvent = {
- effect: ReactiveEffect
- } & DebuggerEventExtraInfo
- export type DebuggerEventExtraInfo = {
- target: object
- type: TrackOpTypes | TriggerOpTypes
- key: any
- newValue?: any
- oldValue?: any
- oldTarget?: Map<any, any> | Set<any>
- }
- const effectStack: ReactiveEffect[] = []
- let activeEffect: ReactiveEffect | undefined
- export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
- export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
- export class ReactiveEffect<T = any> {
- active = true
- deps: Dep[] = []
- // can be attached after creation
- onStop?: () => void
- // dev only
- onTrack?: (event: DebuggerEvent) => void
- // dev only
- onTrigger?: (event: DebuggerEvent) => void
- constructor(
- public fn: () => T,
- public scheduler: EffectScheduler | null = null,
- scope?: EffectScope | null,
- // allow recursive self-invocation
- public allowRecurse = false
- ) {
- recordEffectScope(this, scope)
- }
- run() {
- if (!this.active) {
- return this.fn()
- }
- if (!effectStack.includes(this)) {
- try {
- effectStack.push((activeEffect = this))
- enableTracking()
- trackOpBit = 1 << ++effectTrackDepth
- if (effectTrackDepth <= maxMarkerBits) {
- initDepMarkers(this)
- } else {
- cleanupEffect(this)
- }
- return this.fn()
- } finally {
- if (effectTrackDepth <= maxMarkerBits) {
- finalizeDepMarkers(this)
- }
- trackOpBit = 1 << --effectTrackDepth
- resetTracking()
- effectStack.pop()
- const n = effectStack.length
- activeEffect = n > 0 ? effectStack[n - 1] : undefined
- }
- }
- }
- stop() {
- if (this.active) {
- cleanupEffect(this)
- if (this.onStop) {
- this.onStop()
- }
- this.active = false
- }
- }
- }
- function cleanupEffect(effect: ReactiveEffect) {
- const { deps } = effect
- if (deps.length) {
- for (let i = 0; i < deps.length; i++) {
- deps[i].delete(effect)
- }
- deps.length = 0
- }
- }
- export interface ReactiveEffectOptions {
- lazy?: boolean
- scheduler?: EffectScheduler
- scope?: EffectScope
- allowRecurse?: boolean
- onStop?: () => void
- onTrack?: (event: DebuggerEvent) => void
- onTrigger?: (event: DebuggerEvent) => void
- }
- export interface ReactiveEffectRunner<T = any> {
- (): T
- effect: ReactiveEffect
- }
- export function effect<T = any>(
- fn: () => T,
- options?: ReactiveEffectOptions
- ): ReactiveEffectRunner {
- if ((fn as ReactiveEffectRunner).effect) {
- fn = (fn as ReactiveEffectRunner).effect.fn
- }
- const _effect = new ReactiveEffect(fn)
- if (options) {
- extend(_effect, options)
- if (options.scope) recordEffectScope(_effect, options.scope)
- }
- if (!options || !options.lazy) {
- _effect.run()
- }
- const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
- runner.effect = _effect
- return runner
- }
- export function stop(runner: ReactiveEffectRunner) {
- runner.effect.stop()
- }
- let shouldTrack = true
- const trackStack: boolean[] = []
- export function pauseTracking() {
- trackStack.push(shouldTrack)
- shouldTrack = false
- }
- export function enableTracking() {
- trackStack.push(shouldTrack)
- shouldTrack = true
- }
- export function resetTracking() {
- const last = trackStack.pop()
- shouldTrack = last === undefined ? true : last
- }
- export function track(target: object, type: TrackOpTypes, key: unknown) {
- if (!isTracking()) {
- return
- }
- let depsMap = targetMap.get(target)
- if (!depsMap) {
- targetMap.set(target, (depsMap = new Map()))
- }
- let dep = depsMap.get(key)
- if (!dep) {
- depsMap.set(key, (dep = createDep()))
- }
- const eventInfo = __DEV__
- ? { effect: activeEffect, target, type, key }
- : undefined
- trackEffects(dep, eventInfo)
- }
- export function isTracking() {
- return shouldTrack && activeEffect !== undefined
- }
- export function trackEffects(
- dep: Dep,
- debuggerEventExtraInfo?: DebuggerEventExtraInfo
- ) {
- let shouldTrack = false
- if (effectTrackDepth <= maxMarkerBits) {
- if (!newTracked(dep)) {
- dep.n |= trackOpBit // set newly tracked
- shouldTrack = !wasTracked(dep)
- }
- } else {
- // Full cleanup mode.
- shouldTrack = !dep.has(activeEffect!)
- }
- if (shouldTrack) {
- dep.add(activeEffect!)
- activeEffect!.deps.push(dep)
- if (__DEV__ && activeEffect!.onTrack) {
- activeEffect!.onTrack(
- Object.assign(
- {
- effect: activeEffect!
- },
- debuggerEventExtraInfo
- )
- )
- }
- }
- }
- export function trigger(
- target: object,
- type: TriggerOpTypes,
- key?: unknown,
- newValue?: unknown,
- oldValue?: unknown,
- oldTarget?: Map<unknown, unknown> | Set<unknown>
- ) {
- const depsMap = targetMap.get(target)
- if (!depsMap) {
- // never been tracked
- return
- }
- let deps: (Dep | undefined)[] = []
- if (type === TriggerOpTypes.CLEAR) {
- // collection being cleared
- // trigger all effects for target
- deps = [...depsMap.values()]
- } else if (key === 'length' && isArray(target)) {
- depsMap.forEach((dep, key) => {
- if (key === 'length' || key >= (newValue as number)) {
- deps.push(dep)
- }
- })
- } else {
- // schedule runs for SET | ADD | DELETE
- if (key !== void 0) {
- deps.push(depsMap.get(key))
- }
- // also run for iteration key on ADD | DELETE | Map.SET
- switch (type) {
- case TriggerOpTypes.ADD:
- if (!isArray(target)) {
- deps.push(depsMap.get(ITERATE_KEY))
- if (isMap(target)) {
- deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
- }
- } else if (isIntegerKey(key)) {
- // new index added to array -> length changes
- deps.push(depsMap.get('length'))
- }
- break
- case TriggerOpTypes.DELETE:
- if (!isArray(target)) {
- deps.push(depsMap.get(ITERATE_KEY))
- if (isMap(target)) {
- deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
- }
- }
- break
- case TriggerOpTypes.SET:
- if (isMap(target)) {
- deps.push(depsMap.get(ITERATE_KEY))
- }
- break
- }
- }
- const eventInfo = __DEV__
- ? { target, type, key, newValue, oldValue, oldTarget }
- : undefined
- if (deps.length === 1) {
- if (deps[0]) {
- triggerEffects(deps[0], eventInfo)
- }
- } else {
- const effects: ReactiveEffect[] = []
- for (const dep of deps) {
- if (dep) {
- effects.push(...dep)
- }
- }
- triggerEffects(createDep(effects), eventInfo)
- }
- }
- export function triggerEffects(
- dep: Dep,
- debuggerEventExtraInfo?: DebuggerEventExtraInfo
- ) {
- // spread into array for stabilization
- for (const effect of [...dep]) {
- if (effect !== activeEffect || effect.allowRecurse) {
- if (__DEV__ && effect.onTrigger) {
- effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
- }
- if (effect.scheduler) {
- effect.scheduler()
- } else {
- effect.run()
- }
- }
- }
- }
|