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

228 lines
5.4 KiB
TypeScript
Raw Normal View History

import { TrackOpTypes, TriggerOpTypes } from './operations'
import { EMPTY_OBJ, extend, isArray } 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
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
2018-11-14 00:03:35 +08:00
}
export interface ReactiveEffectOptions {
lazy?: boolean
2019-05-29 23:44:59 +08:00
computed?: boolean
2019-09-07 00:58:31 +08:00
scheduler?: (run: Function) => void
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
2019-06-06 13:25:05 +08:00
onStop?: () => void
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
2019-10-22 23:26:48 +08:00
target: object
type: TrackOpTypes | TriggerOpTypes
2019-10-22 23:26:48 +08:00
key: any
} & DebuggerEventExtraInfo
export interface DebuggerEventExtraInfo {
newValue?: any
oldValue?: any
oldTarget?: Map<any, any> | Set<any>
2018-11-14 00:03:35 +08:00
}
2019-10-17 11:12:57 +08:00
export const effectStack: ReactiveEffect[] = []
2018-11-14 00:03:35 +08:00
export const ITERATE_KEY = Symbol('iterate')
export function isEffect(fn: any): fn is ReactiveEffect {
2019-10-22 23:26:48 +08:00
return fn != null && 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
}
}
function createReactiveEffect<T = any>(
fn: () => T,
2018-11-14 00:03:35 +08:00
options: ReactiveEffectOptions
): ReactiveEffect<T> {
2019-10-22 23:26:48 +08:00
const effect = function reactiveEffect(...args: unknown[]): unknown {
return run(effect, fn, args)
} as ReactiveEffect
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-10-22 23:26:48 +08:00
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
2018-11-14 00:03:35 +08:00
if (!effect.active) {
return fn(...args)
}
2019-10-17 11:12:57 +08:00
if (!effectStack.includes(effect)) {
2018-11-14 00:03:35 +08:00
cleanup(effect)
try {
2019-10-17 11:12:57 +08:00
effectStack.push(effect)
2018-11-14 00:03:35 +08:00
return fn(...args)
} finally {
2019-10-17 11:12:57 +08:00
effectStack.pop()
2018-11-14 00:03:35 +08:00
}
}
}
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
export function pauseTracking() {
shouldTrack = false
}
export function resumeTracking() {
shouldTrack = true
}
export function track(target: object, type: TrackOpTypes, key: unknown) {
2019-10-17 11:12:57 +08:00
if (!shouldTrack || effectStack.length === 0) {
return
}
2019-10-17 11:12:57 +08:00
const effect = effectStack[effectStack.length - 1]
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (dep === void 0) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(effect)) {
dep.add(effect)
effect.deps.push(dep)
if (__DEV__ && effect.options.onTrack) {
effect.options.onTrack({
effect,
target,
type,
key
})
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,
extraInfo?: DebuggerEventExtraInfo
2018-11-14 00:03:35 +08:00
) {
2019-05-29 17:36:53 +08:00
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
if (type === TriggerOpTypes.CLEAR) {
2018-11-14 00:03:35 +08:00
// collection being cleared, trigger all effects for target
depsMap.forEach(dep => {
addRunners(effects, computedRunners, dep)
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
addRunners(effects, computedRunners, depsMap.get(key))
2018-11-14 00:03:35 +08:00
}
// also run for iteration key on ADD | DELETE
if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
const iterationKey = isArray(target) ? 'length' : ITERATE_KEY
2018-11-14 00:03:35 +08:00
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
}
const run = (effect: ReactiveEffect) => {
scheduleRun(effect, target, type, key, extraInfo)
}
// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run)
effects.forEach(run)
}
function addRunners(
effects: Set<ReactiveEffect>,
computedRunners: Set<ReactiveEffect>,
effectsToAdd: Set<ReactiveEffect> | undefined
) {
if (effectsToAdd !== void 0) {
effectsToAdd.forEach(effect => {
if (effect.options.computed) {
2018-11-14 00:03:35 +08:00
computedRunners.add(effect)
} else {
effects.add(effect)
}
})
}
}
function scheduleRun(
effect: ReactiveEffect,
2019-10-22 23:26:48 +08:00
target: object,
type: TriggerOpTypes,
2019-10-22 23:26:48 +08:00
key: unknown,
extraInfo?: DebuggerEventExtraInfo
2018-11-14 00:03:35 +08:00
) {
if (__DEV__ && effect.options.onTrigger) {
2019-10-22 23:26:48 +08:00
const event: DebuggerEvent = {
effect,
target,
key,
type
}
effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event)
2018-11-14 00:03:35 +08:00
}
if (effect.options.scheduler !== void 0) {
effect.options.scheduler(effect)
2018-11-14 00:03:35 +08:00
} else {
effect()
}
}