2019-12-04 00:30:24 +08:00
|
|
|
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
2020-09-14 23:26:34 +08:00
|
|
|
import { EMPTY_OBJ, isArray, isIntegerKey, isMap } from '@vue/shared'
|
2018-11-14 00:03:35 +08:00
|
|
|
|
2019-12-04 00:30:24 +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>()
|
|
|
|
|
2019-10-09 01:48:13 +08:00
|
|
|
export interface ReactiveEffect<T = any> {
|
2020-07-16 11:19:15 +08:00
|
|
|
(): T
|
2019-10-22 23:26:48 +08:00
|
|
|
_isEffect: true
|
2020-04-15 05:31:35 +08:00
|
|
|
id: number
|
2018-11-14 00:03:35 +08:00
|
|
|
active: boolean
|
2019-10-09 01:48:13 +08:00
|
|
|
raw: () => T
|
2018-11-14 00:03:35 +08:00
|
|
|
deps: Array<Dep>
|
2019-10-30 23:11:23 +08:00
|
|
|
options: ReactiveEffectOptions
|
2020-10-06 04:36:02 +08:00
|
|
|
allowRecurse: boolean
|
2018-11-14 00:03:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface ReactiveEffectOptions {
|
|
|
|
lazy?: boolean
|
2020-04-15 05:31:35 +08:00
|
|
|
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
|
2021-05-28 08:53:21 +08:00
|
|
|
/**
|
|
|
|
* 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).
|
|
|
|
*/
|
2020-09-16 22:52:31 +08:00
|
|
|
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
|
2021-06-24 05:22:21 +08:00
|
|
|
} & DebuggerEventExtraInfo
|
|
|
|
|
|
|
|
export type DebuggerEventExtraInfo = {
|
2019-10-22 23:26:48 +08:00
|
|
|
target: object
|
2019-12-04 00:30:24 +08:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2019-12-14 12:06:55 +08:00
|
|
|
const effectStack: ReactiveEffect[] = []
|
2020-04-25 01:02:44 +08:00
|
|
|
let activeEffect: ReactiveEffect | undefined
|
2018-11-14 00:03:35 +08:00
|
|
|
|
2020-03-25 00:43:06 +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
|
|
|
|
2019-10-10 00:16:29 +08:00
|
|
|
export function isEffect(fn: any): fn is ReactiveEffect {
|
2020-03-19 06:14:51 +08:00
|
|
|
return fn && fn._isEffect === true
|
2019-10-10 00:16:29 +08:00
|
|
|
}
|
|
|
|
|
2019-10-09 01:48:13 +08:00
|
|
|
export function effect<T = any>(
|
|
|
|
fn: () => T,
|
2019-06-12 00:03:50 +08:00
|
|
|
options: ReactiveEffectOptions = EMPTY_OBJ
|
2019-10-09 01:48:13 +08:00
|
|
|
): ReactiveEffect<T> {
|
2019-10-10 00:16:29 +08:00
|
|
|
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)
|
2019-10-30 23:11:23 +08:00
|
|
|
if (effect.options.onStop) {
|
|
|
|
effect.options.onStop()
|
2019-06-12 00:03:50 +08:00
|
|
|
}
|
|
|
|
effect.active = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-15 05:31:35 +08:00
|
|
|
let uid = 0
|
|
|
|
|
2019-10-09 01:48:13 +08:00
|
|
|
function createReactiveEffect<T = any>(
|
2020-07-16 11:19:15 +08:00
|
|
|
fn: () => T,
|
2018-11-14 00:03:35 +08:00
|
|
|
options: ReactiveEffectOptions
|
2019-10-09 01:48:13 +08:00
|
|
|
): ReactiveEffect<T> {
|
2020-07-16 11:19:15 +08:00
|
|
|
const effect = function reactiveEffect(): unknown {
|
2020-04-03 07:49:45 +08:00
|
|
|
if (!effect.active) {
|
2021-05-28 08:53:21 +08:00
|
|
|
return fn()
|
2020-04-03 07:49:45 +08:00
|
|
|
}
|
|
|
|
if (!effectStack.includes(effect)) {
|
|
|
|
cleanup(effect)
|
|
|
|
try {
|
|
|
|
enableTracking()
|
|
|
|
effectStack.push(effect)
|
|
|
|
activeEffect = effect
|
2020-07-16 11:19:15 +08:00
|
|
|
return fn()
|
2020-04-03 07:49:45 +08:00
|
|
|
} finally {
|
|
|
|
effectStack.pop()
|
|
|
|
resetTracking()
|
2021-06-24 05:22:21 +08:00
|
|
|
const n = effectStack.length
|
|
|
|
activeEffect = n > 0 ? effectStack[n - 1] : undefined
|
2020-04-03 07:49:45 +08:00
|
|
|
}
|
|
|
|
}
|
2019-10-10 00:16:29 +08:00
|
|
|
} as ReactiveEffect
|
2020-04-15 05:31:35 +08:00
|
|
|
effect.id = uid++
|
2020-10-06 04:36:02 +08:00
|
|
|
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 = []
|
2019-10-30 23:11:23 +08:00
|
|
|
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
|
2020-02-18 12:14:07 +08:00
|
|
|
const trackStack: boolean[] = []
|
2019-09-05 06:20:47 +08:00
|
|
|
|
|
|
|
export function pauseTracking() {
|
2020-02-18 12:14:07 +08:00
|
|
|
trackStack.push(shouldTrack)
|
2019-09-05 06:20:47 +08:00
|
|
|
shouldTrack = false
|
|
|
|
}
|
|
|
|
|
2020-02-18 12:14:07 +08:00
|
|
|
export function enableTracking() {
|
|
|
|
trackStack.push(shouldTrack)
|
2019-09-05 06:20:47 +08:00
|
|
|
shouldTrack = true
|
|
|
|
}
|
|
|
|
|
2020-02-18 12:14:07 +08:00
|
|
|
export function resetTracking() {
|
|
|
|
const last = trackStack.pop()
|
|
|
|
shouldTrack = last === undefined ? true : last
|
|
|
|
}
|
|
|
|
|
2019-12-04 00:30:24 +08:00
|
|
|
export function track(target: object, type: TrackOpTypes, key: unknown) {
|
2021-06-24 05:22:21 +08:00
|
|
|
if (!isTracking()) {
|
2019-10-16 13:58:11 +08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
let depsMap = targetMap.get(target)
|
2020-04-21 01:39:35 +08:00
|
|
|
if (!depsMap) {
|
2019-10-16 13:58:11 +08:00
|
|
|
targetMap.set(target, (depsMap = new Map()))
|
|
|
|
}
|
2019-12-04 00:30:24 +08:00
|
|
|
let dep = depsMap.get(key)
|
2020-04-21 01:39:35 +08:00
|
|
|
if (!dep) {
|
2019-12-04 00:30:24 +08:00
|
|
|
depsMap.set(key, (dep = new Set()))
|
2019-10-16 13:58:11 +08:00
|
|
|
}
|
2021-06-24 05:22:21 +08:00
|
|
|
|
|
|
|
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,
|
2019-12-04 00:30:24 +08:00
|
|
|
type: TriggerOpTypes,
|
2019-10-22 23:26:48 +08:00
|
|
|
key?: unknown,
|
2020-02-21 22:05:16 +08:00
|
|
|
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)
|
2020-04-21 01:39:35 +08:00
|
|
|
if (!depsMap) {
|
2019-05-29 17:36:53 +08:00
|
|
|
// never been tracked
|
|
|
|
return
|
|
|
|
}
|
2020-03-25 00:43:06 +08:00
|
|
|
|
2021-06-24 05:22:21 +08:00
|
|
|
let sets: DepSets = []
|
2020-02-22 12:17:30 +08:00
|
|
|
if (type === TriggerOpTypes.CLEAR) {
|
|
|
|
// collection being cleared
|
2020-02-21 18:38:07 +08:00
|
|
|
// trigger all effects for target
|
2021-06-24 05:22:21 +08:00
|
|
|
sets = [...depsMap.values()]
|
2020-02-22 12:17:30 +08:00
|
|
|
} else if (key === 'length' && isArray(target)) {
|
|
|
|
depsMap.forEach((dep, key) => {
|
|
|
|
if (key === 'length' || key >= (newValue as number)) {
|
2021-06-24 05:22:21 +08:00
|
|
|
sets.push(dep)
|
2020-02-22 12:17:30 +08:00
|
|
|
}
|
|
|
|
})
|
2018-11-14 00:03:35 +08:00
|
|
|
} else {
|
|
|
|
// schedule runs for SET | ADD | DELETE
|
|
|
|
if (key !== void 0) {
|
2021-06-24 05:22:21 +08:00
|
|
|
sets.push(depsMap.get(key))
|
2018-11-14 00:03:35 +08:00
|
|
|
}
|
2020-09-14 23:26:34 +08:00
|
|
|
|
2020-02-18 13:09:24 +08:00
|
|
|
// also run for iteration key on ADD | DELETE | Map.SET
|
2020-09-14 23:26:34 +08:00
|
|
|
switch (type) {
|
|
|
|
case TriggerOpTypes.ADD:
|
|
|
|
if (!isArray(target)) {
|
2021-06-24 05:22:21 +08:00
|
|
|
sets.push(depsMap.get(ITERATE_KEY))
|
2020-09-14 23:26:34 +08:00
|
|
|
if (isMap(target)) {
|
2021-06-24 05:22:21 +08:00
|
|
|
sets.push(depsMap.get(MAP_KEY_ITERATE_KEY))
|
2020-09-14 23:26:34 +08:00
|
|
|
}
|
|
|
|
} else if (isIntegerKey(key)) {
|
|
|
|
// new index added to array -> length changes
|
2021-06-24 05:22:21 +08:00
|
|
|
sets.push(depsMap.get('length'))
|
2020-09-14 23:26:34 +08:00
|
|
|
}
|
|
|
|
break
|
|
|
|
case TriggerOpTypes.DELETE:
|
|
|
|
if (!isArray(target)) {
|
2021-06-24 05:22:21 +08:00
|
|
|
sets.push(depsMap.get(ITERATE_KEY))
|
2020-09-14 23:26:34 +08:00
|
|
|
if (isMap(target)) {
|
2021-06-24 05:22:21 +08:00
|
|
|
sets.push(depsMap.get(MAP_KEY_ITERATE_KEY))
|
2020-09-14 23:26:34 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
break
|
|
|
|
case TriggerOpTypes.SET:
|
|
|
|
if (isMap(target)) {
|
2021-06-24 05:22:21 +08:00
|
|
|
sets.push(depsMap.get(ITERATE_KEY))
|
2020-09-14 23:26:34 +08:00
|
|
|
}
|
|
|
|
break
|
2018-11-14 00:03:35 +08:00
|
|
|
}
|
|
|
|
}
|
2020-03-25 00:43:06 +08:00
|
|
|
|
2021-06-24 05:22:21 +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) => {
|
2020-03-25 00:43:06 +08:00
|
|
|
if (__DEV__ && effect.options.onTrigger) {
|
2021-06-24 05:22:21 +08:00
|
|
|
effect.options.onTrigger(
|
|
|
|
Object.assign({ effect }, debuggerEventExtraInfo)
|
|
|
|
)
|
2020-03-25 00:43:06 +08:00
|
|
|
}
|
2020-04-21 01:39:35 +08:00
|
|
|
if (effect.options.scheduler) {
|
2020-03-25 00:43:06 +08:00
|
|
|
effect.options.scheduler(effect)
|
|
|
|
} else {
|
|
|
|
effect()
|
|
|
|
}
|
2018-11-14 00:03:35 +08:00
|
|
|
}
|
2020-03-25 00:43:06 +08:00
|
|
|
|
2021-06-24 05:22:21 +08:00
|
|
|
const immutableDeps = [...dep]
|
|
|
|
immutableDeps.forEach(effect => {
|
|
|
|
if (effect !== activeEffect || effect.allowRecurse) {
|
|
|
|
run(effect)
|
|
|
|
}
|
|
|
|
})
|
2018-11-14 00:03:35 +08:00
|
|
|
}
|