vue3-core/packages/reactivity/src/effect.ts

312 lines
7.7 KiB
TypeScript
Raw Normal View History

import { TrackOpTypes, TriggerOpTypes } from './operations'
import { EMPTY_OBJ, isArray, isIntegerKey, isMap } from '@vue/shared'
2018-11-14 00:03:35 +08:00
// 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 Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
export interface ReactiveEffect<T = any> {
(): T
2019-10-22 23:26:48 +08:00
_isEffect: true
id: number
2018-11-14 00:03:35 +08:00
active: boolean
raw: () => T
2018-11-14 00:03:35 +08:00
deps: Array<Dep>
options: ReactiveEffectOptions
allowRecurse: boolean
2018-11-14 00:03:35 +08:00
}
export interface ReactiveEffectOptions {
lazy?: boolean
scheduler?: (job: ReactiveEffect) => void
2019-09-07 00:58:31 +08:00
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
2019-06-06 13:25:05 +08:00
onStop?: () => void
/**
* Indicates whether the job is allowed to recursively trigger itself when
* managed by the scheduler.
*
* By default, a job cannot trigger itself because some built-in method calls,
* e.g. Array.prototype.push actually performs reads as well (#1740) which
* can lead to confusing infinite loops.
* The allowed cases are component update functions and watch callbacks.
* Component update functions may update child component props, which in turn
* trigger flush: "pre" watch callbacks that mutates state that the parent
* relies on (#1801). Watch callbacks doesn't track its dependencies so if it
* triggers itself again, it's likely intentional and it is the user's
* responsibility to perform recursive state mutation that eventually
* stabilizes (#1727).
*/
allowRecurse?: boolean
2018-11-14 00:03:35 +08:00
}
2019-10-22 23:53:17 +08:00
export type DebuggerEvent = {
2018-11-14 00:03:35 +08:00
effect: ReactiveEffect
} & DebuggerEventExtraInfo
export type DebuggerEventExtraInfo = {
2019-10-22 23:26:48 +08:00
target: object
type: TrackOpTypes | TriggerOpTypes
2019-10-22 23:26:48 +08:00
key: any
newValue?: any
oldValue?: any
oldTarget?: Map<any, any> | Set<any>
2018-11-14 00:03:35 +08:00
}
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined
2018-11-14 00:03:35 +08:00
export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
2018-11-14 00:03:35 +08:00
export function isEffect(fn: any): fn is ReactiveEffect {
2020-03-19 06:14:51 +08:00
return fn && fn._isEffect === true
}
export function effect<T = any>(
fn: () => T,
2019-06-12 00:03:50 +08:00
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
2019-06-12 00:03:50 +08:00
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
export function stop(effect: ReactiveEffect) {
if (effect.active) {
cleanup(effect)
if (effect.options.onStop) {
effect.options.onStop()
2019-06-12 00:03:50 +08:00
}
effect.active = false
}
}
let uid = 0
function createReactiveEffect<T = any>(
fn: () => T,
2018-11-14 00:03:35 +08:00
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
const n = effectStack.length
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
}
} as ReactiveEffect
effect.id = uid++
effect.allowRecurse = !!options.allowRecurse
2019-10-22 23:26:48 +08:00
effect._isEffect = true
2018-11-14 00:03:35 +08:00
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
2018-11-14 00:03:35 +08:00
return effect
}
2019-06-12 00:03:50 +08:00
function cleanup(effect: ReactiveEffect) {
2019-06-02 16:35:19 +08:00
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
2018-11-14 00:03:35 +08:00
}
}
2019-09-05 06:20:47 +08:00
let shouldTrack = true
const trackStack: boolean[] = []
2019-09-05 06:20:47 +08:00
export function pauseTracking() {
trackStack.push(shouldTrack)
2019-09-05 06:20:47 +08:00
shouldTrack = false
}
export function enableTracking() {
trackStack.push(shouldTrack)
2019-09-05 06:20:47 +08:00
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 = new Set()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
export function isTracking() {
return shouldTrack && activeEffect !== undefined
}
export function trackEffects(
dep: Set<ReactiveEffect>,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (!dep.has(activeEffect!)) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.options.onTrack) {
activeEffect!.options.onTrack(
Object.assign(
{
effect: activeEffect!
},
debuggerEventExtraInfo
)
)
2018-11-14 00:03:35 +08:00
}
}
}
export function trigger(
2019-10-22 23:26:48 +08:00
target: object,
type: TriggerOpTypes,
2019-10-22 23:26:48 +08:00
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
2018-11-14 00:03:35 +08:00
) {
2019-05-29 17:36:53 +08:00
const depsMap = targetMap.get(target)
if (!depsMap) {
2019-05-29 17:36:53 +08:00
// never been tracked
return
}
let sets: DepSets = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
sets = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
sets.push(dep)
}
})
2018-11-14 00:03:35 +08:00
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
sets.push(depsMap.get(key))
2018-11-14 00:03:35 +08:00
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
sets.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
sets.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
sets.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
sets.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
sets.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
sets.push(depsMap.get(ITERATE_KEY))
}
break
2018-11-14 00:03:35 +08:00
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
triggerMultiEffects(sets, eventInfo)
}
type DepSets = (Dep | undefined)[]
export function triggerMultiEffects(
depSets: DepSets,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (depSets.length === 1) {
if (depSets[0]) {
triggerEffects(depSets[0], debuggerEventExtraInfo)
}
} else {
const sets = depSets.filter(s => !!s) as Dep[]
triggerEffects(concatSets(sets), debuggerEventExtraInfo)
}
}
function concatSets<T>(sets: Set<T>[]): Set<T> {
const all = ([] as T[]).concat(...sets.map(s => [...s!]))
return new Set(all)
}
export function triggerEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
2018-11-14 00:03:35 +08:00
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger(
Object.assign({ effect }, debuggerEventExtraInfo)
)
}
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
2018-11-14 00:03:35 +08:00
}
const immutableDeps = [...dep]
immutableDeps.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
run(effect)
}
})
2018-11-14 00:03:35 +08:00
}