| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391 |
- import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
- import type { ComputedRefImpl } from './computed'
- import { type TrackOpTypes, TriggerOpTypes } from './constants'
- import {
- type DebuggerEventExtraInfo,
- EffectFlags,
- type Subscriber,
- activeSub,
- endBatch,
- shouldTrack,
- startBatch,
- } from './effect'
- /**
- * Incremented every time a reactive change happens
- * This is used to give computed a fast path to avoid re-compute when nothing
- * has changed.
- */
- export let globalVersion = 0
- /**
- * Represents a link between a source (Dep) and a subscriber (Effect or Computed).
- * Deps and subs have a many-to-many relationship - each link between a
- * dep and a sub is represented by a Link instance.
- *
- * A Link is also a node in two doubly-linked lists - one for the associated
- * sub to track all its deps, and one for the associated dep to track all its
- * subs.
- *
- * @internal
- */
- export class Link {
- /**
- * - Before each effect run, all previous dep links' version are reset to -1
- * - During the run, a link's version is synced with the source dep on access
- * - After the run, links with version -1 (that were never used) are cleaned
- * up
- */
- version: number
- /**
- * Pointers for doubly-linked lists
- */
- nextDep?: Link
- prevDep?: Link
- nextSub?: Link
- prevSub?: Link
- prevActiveLink?: Link
- constructor(
- public sub: Subscriber,
- public dep: Dep,
- ) {
- this.version = dep.version
- this.nextDep =
- this.prevDep =
- this.nextSub =
- this.prevSub =
- this.prevActiveLink =
- undefined
- }
- }
- /**
- * @internal
- */
- export class Dep {
- version = 0
- /**
- * Link between this dep and the current active effect
- */
- activeLink?: Link = undefined
- /**
- * Doubly linked list representing the subscribing effects (tail)
- */
- subs?: Link = undefined
- /**
- * Doubly linked list representing the subscribing effects (head)
- * DEV only, for invoking onTrigger hooks in correct order
- */
- subsHead?: Link
- /**
- * For object property deps cleanup
- */
- map?: KeyToDepMap = undefined
- key?: unknown = undefined
- /**
- * Subscriber counter
- */
- sc: number = 0
- constructor(public computed?: ComputedRefImpl | undefined) {
- if (__DEV__) {
- this.subsHead = undefined
- }
- }
- track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
- if (!activeSub || !shouldTrack || activeSub === this.computed) {
- return
- }
- let link = this.activeLink
- if (link === undefined || link.sub !== activeSub) {
- link = this.activeLink = new Link(activeSub, this)
- // add the link to the activeEffect as a dep (as tail)
- if (!activeSub.deps) {
- activeSub.deps = activeSub.depsTail = link
- } else {
- link.prevDep = activeSub.depsTail
- activeSub.depsTail!.nextDep = link
- activeSub.depsTail = link
- }
- addSub(link)
- } else if (link.version === -1) {
- // reused from last run - already a sub, just sync version
- link.version = this.version
- // If this dep has a next, it means it's not at the tail - move it to the
- // tail. This ensures the effect's dep list is in the order they are
- // accessed during evaluation.
- if (link.nextDep) {
- const next = link.nextDep
- next.prevDep = link.prevDep
- if (link.prevDep) {
- link.prevDep.nextDep = next
- }
- link.prevDep = activeSub.depsTail
- link.nextDep = undefined
- activeSub.depsTail!.nextDep = link
- activeSub.depsTail = link
- // this was the head - point to the new head
- if (activeSub.deps === link) {
- activeSub.deps = next
- }
- }
- }
- if (__DEV__ && activeSub.onTrack) {
- activeSub.onTrack(
- extend(
- {
- effect: activeSub,
- },
- debugInfo,
- ),
- )
- }
- return link
- }
- trigger(debugInfo?: DebuggerEventExtraInfo): void {
- this.version++
- globalVersion++
- this.notify(debugInfo)
- }
- notify(debugInfo?: DebuggerEventExtraInfo): void {
- startBatch()
- try {
- if (__DEV__) {
- // subs are notified and batched in reverse-order and then invoked in
- // original order at the end of the batch, but onTrigger hooks should
- // be invoked in original order here.
- for (let head = this.subsHead; head; head = head.nextSub) {
- if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
- head.sub.onTrigger(
- extend(
- {
- effect: head.sub,
- },
- debugInfo,
- ),
- )
- }
- }
- }
- for (let link = this.subs; link; link = link.prevSub) {
- if (link.sub.notify()) {
- // if notify() returns `true`, this is a computed. Also call notify
- // on its dep - it's called here instead of inside computed's notify
- // in order to reduce call stack depth.
- ;(link.sub as ComputedRefImpl).dep.notify()
- }
- }
- } finally {
- endBatch()
- }
- }
- }
- function addSub(link: Link) {
- link.dep.sc++
- if (link.sub.flags & EffectFlags.TRACKING) {
- const computed = link.dep.computed
- // computed getting its first subscriber
- // enable tracking + lazily subscribe to all its deps
- if (computed && !link.dep.subs) {
- computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
- for (let l = computed.deps; l; l = l.nextDep) {
- addSub(l)
- }
- }
- const currentTail = link.dep.subs
- if (currentTail !== link) {
- link.prevSub = currentTail
- if (currentTail) currentTail.nextSub = link
- }
- if (__DEV__ && link.dep.subsHead === undefined) {
- link.dep.subsHead = link
- }
- link.dep.subs = link
- }
- }
- // 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 Maps to reduce memory overhead.
- type KeyToDepMap = Map<any, Dep>
- export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap()
- export const ITERATE_KEY: unique symbol = Symbol(
- __DEV__ ? 'Object iterate' : '',
- )
- export const MAP_KEY_ITERATE_KEY: unique symbol = Symbol(
- __DEV__ ? 'Map keys iterate' : '',
- )
- export const ARRAY_ITERATE_KEY: unique symbol = Symbol(
- __DEV__ ? 'Array iterate' : '',
- )
- /**
- * Tracks access to a reactive property.
- *
- * This will check which effect is running at the moment and record it as dep
- * which records all effects that depend on the reactive property.
- *
- * @param target - Object holding the reactive property.
- * @param type - Defines the type of access to the reactive property.
- * @param key - Identifier of the reactive property to track.
- */
- export function track(target: object, type: TrackOpTypes, key: unknown): void {
- if (shouldTrack && activeSub) {
- 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 Dep()))
- dep.map = depsMap
- dep.key = key
- }
- if (__DEV__) {
- dep.track({
- target,
- type,
- key,
- })
- } else {
- dep.track()
- }
- }
- }
- /**
- * Finds all deps associated with the target (or a specific property) and
- * triggers the effects stored within.
- *
- * @param target - The reactive object.
- * @param type - Defines the type of the operation that needs to trigger effects.
- * @param key - Can be used to target a specific reactive property in the target object.
- */
- export function trigger(
- target: object,
- type: TriggerOpTypes,
- key?: unknown,
- newValue?: unknown,
- oldValue?: unknown,
- oldTarget?: Map<unknown, unknown> | Set<unknown>,
- ): void {
- const depsMap = targetMap.get(target)
- if (!depsMap) {
- // never been tracked
- globalVersion++
- return
- }
- const run = (dep: Dep | undefined) => {
- if (dep) {
- if (__DEV__) {
- dep.trigger({
- target,
- type,
- key,
- newValue,
- oldValue,
- oldTarget,
- })
- } else {
- dep.trigger()
- }
- }
- }
- startBatch()
- if (type === TriggerOpTypes.CLEAR) {
- // collection being cleared
- // trigger all effects for target
- depsMap.forEach(run)
- } else {
- const targetIsArray = isArray(target)
- const isArrayIndex = targetIsArray && isIntegerKey(key)
- if (targetIsArray && key === 'length') {
- const newLength = Number(newValue)
- depsMap.forEach((dep, key) => {
- if (
- key === 'length' ||
- key === ARRAY_ITERATE_KEY ||
- (!isSymbol(key) && key >= newLength)
- ) {
- run(dep)
- }
- })
- } else {
- // schedule runs for SET | ADD | DELETE
- if (key !== void 0 || depsMap.has(void 0)) {
- run(depsMap.get(key))
- }
- // schedule ARRAY_ITERATE for any numeric key change (length is handled above)
- if (isArrayIndex) {
- run(depsMap.get(ARRAY_ITERATE_KEY))
- }
- // also run for iteration key on ADD | DELETE | Map.SET
- switch (type) {
- case TriggerOpTypes.ADD:
- if (!targetIsArray) {
- run(depsMap.get(ITERATE_KEY))
- if (isMap(target)) {
- run(depsMap.get(MAP_KEY_ITERATE_KEY))
- }
- } else if (isArrayIndex) {
- // new index added to array -> length changes
- run(depsMap.get('length'))
- }
- break
- case TriggerOpTypes.DELETE:
- if (!targetIsArray) {
- run(depsMap.get(ITERATE_KEY))
- if (isMap(target)) {
- run(depsMap.get(MAP_KEY_ITERATE_KEY))
- }
- }
- break
- case TriggerOpTypes.SET:
- if (isMap(target)) {
- run(depsMap.get(ITERATE_KEY))
- }
- break
- }
- }
- }
- endBatch()
- }
- export function getDepFromReactive(
- object: any,
- key: string | number | symbol,
- ): Dep | undefined {
- const depMap = targetMap.get(object)
- return depMap && depMap.get(key)
- }
|