Maison  >  Article  >  interface Web  >  Fonction d'effet secondaire Vue3 et exemple d'analyse de collection de dépendances

Fonction d'effet secondaire Vue3 et exemple d'analyse de collection de dépendances

WBOY
WBOYavant
2023-05-11 20:13:101261parcourir

Fonction d'effet secondaire

Une fonction d'effet secondaire fait référence à une fonction qui produit des effets secondaires, comme indiqué dans le code suivant :

function effect(){
  document.body.innerText = 'hello vue3'
}

Lorsque la fonction d'effet est exécutée, elle définira le contenu du texte du corps, mais toute autre fonction que la fonction d'effet ne fera Lire ou définir le contenu du texte du corps. En d'autres termes, l'exécution de la fonction d'effet affectera directement ou indirectement l'exécution d'autres fonctions. À l'heure actuelle, nous disons que la fonction d'effet a des effets secondaires. Les effets secondaires se produisent facilement. Par exemple, si une fonction modifie une variable globale, il s'agit en fait d'un effet secondaire.

// 全局变量
let val = 1
function effect() {
  val = 2 // 修改全局变量,产生副作用
}

Variables globales des fonctions d'effets secondaires

Dans le module d'effets secondaires, plusieurs variables globales sont définies. Connaître ces variables à l'avance nous aidera à comprendre le processus de génération et d'appel des fonctions d'effets secondaires.

// packages/reactivity/src/effect.ts
export type Dep = Set<ReactiveEffect> & TrackedMarkers
type KeyToDepMap = Map<any, Dep>
// WeakMap 集合存储副作用函数
const targetMap = new WeakMap<any, KeyToDepMap>()

// 用一个全局变量存储当前激活的 effect 函数
export let activeEffect: ReactiveEffect | undefined

// 标识是否开启了依赖收集
export let shouldTrack = true
const trackStack: boolean[] = []

targetMap

targetMap est une collection de type WeakMap, utilisée pour stocker les fonctions d'effets secondaires. À partir de la définition du type, nous pouvons voir la structure des données de targetMap :

  • WeakMap est généré par target -->. Map Constitute target --> Map 构成

  • Map 由 key --> Set

Map est composé de key --> Set

Fonction deffet secondaire Vue3 et exemple danalyse de collection de dépendancesoù la clé de WeakMap est la cible de l'objet d'origine, la valeur de WeakMap est une instance de Map, et la clé de Map est la cible de l'objet d'origine. La clé et la valeur de Map sont un ensemble composé de fonctions d'effets secondaires. Leur relation est la suivante :

targetMap Pourquoi utiliser WeakMap

Regardons le code suivant :

const map = new Map();
const weakMap = new WeakMap();

(function() {
  const foo = {foo: 1};
  const bar = {bar: 2};
  
  map.set(foo, 1); // foo 对象是 map 的key
  weakMap.set(bar, 2); // bar 对象是 weakMap 的 key
})
Dans le code ci-dessus, les constantes map et lowMap sont définies, correspondant respectivement aux instances de Map et WeakMap. Deux objets sont définis dans l'expression de fonction immédiatement exécutée : foo et bar, qui servent respectivement de clés de map et lowMap.

Une fois l'expression de fonction exécutée, pour l'objet foo, il est toujours référencé comme la clé de la carte, donc le

garbage collector

ne le supprimera pas de la mémoire, et nous pouvons toujours l'imprimer via map.keys Out objet foo.

Pour la barre d'objets, puisque la clé de WeakMap est une référence faible, cela n'affecte pas le travail du garbage collector, donc une fois l'expression exécutée, le garbage collector supprimera la barre d'objet de la mémoire, et nous ne pouvons pas obtenir it. La valeur clé de WeakMap signifie que la barre d'objet ne peut pas être obtenue via WeakMap.

En termes simples,

WeakMap a une faible référence à la clé et n'affecte pas le travail du ramasse-miettes

**. Selon cette fonctionnalité, une fois la clé recyclée par le garbage collector, la clé et la valeur correspondantes ne seront plus accessibles. Par conséquent, WeakMap est souvent utilisé pour stocker des informations qui n'ont de valeur que lorsque l'objet référencé par clé existe (n'a pas été recyclé)**.

Par exemple, dans le scénario ci-dessus, si l'objet cible n'a aucune référence, cela signifie que le côté utilisateur n'en a plus besoin et le garbage collector terminera la tâche de recyclage. Mais si vous utilisez Map au lieu de WeakMap, même si le code côté utilisateur n'a aucune référence à la cible, la cible ne sera pas recyclée, ce qui peut éventuellement conduire à un débordement de mémoire. La variable

activeEffect

activeEffect est utilisée pour maintenir les effets secondaires en cours d'exécution. La variable

shouldTrack

shouldTrack est utilisée pour identifier si la collecte de dépendances est activée uniquement lorsque la valeur de ShouldTrack est vraie, c'est-à-dire. , la fonction d'effet secondaire est ajoutée à la collection de dépendances.

Implémentation des effets secondaires

fonction d'effet

L'API d'effet est utilisée pour créer une fonction d'effet secondaire, acceptant deux paramètres, qui sont une fonction fn définie par l'utilisateur et des options d'options. Le code source est le suivant :

// packages/reactivity/src/effect.ts

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  // 当传入的 fn 中存在 effect 副作用时,将这个副作用的原始函数赋值给 fn
  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()
  }
  // 通过 bind 函数返回一个新的副作用函数   
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  // 将副作用添加到新的副作用函数上   
  runner.effect = _effect
  // 返回这个新的副作用函数   
  return runner
}

À partir du code ci-dessus, nous pouvons savoir que lorsqu'il y a un effet secondaire dans le paramètre fn passé, la fonction d'origine de cet effet secondaire est attribuée à fn. Appelez ensuite la classe ReactiveEffect pour créer une fonction d’effet secondaire encapsulée.

Dans certains scénarios, nous ne voulons pas que l'effet soit exécuté immédiatement, mais nous voulons qu'il soit exécuté en cas de besoin. Nous pouvons y parvenir en ajoutant l'attribut paresseux aux options. Dans le code source de la fonction d'effet, déterminez la valeur de l'option options.lazy. Lorsque la valeur est vraie, la fonction d'effet secondaire ne sera pas exécutée immédiatement, obtenant ainsi un effet d'exécution paresseuse.

Ensuite, utilisez la fonction de liaison pour renvoyer un nouveau coureur de fonction d'effet secondaire, le this de cette nouvelle fonction est spécifié comme _effect, et ajoutez _effect à l'attribut d'effet de cette nouvelle fonction d'effet secondaire, et enfin renvoyez cette nouvelle fonction d'effet secondaire.

Étant donné que l'API d'effet renvoie la fonction d'effet secondaire encapsulée, la fonction d'effet secondaire d'origine est stockée dans l'attribut d'effet de la fonction d'effet secondaire encapsulée. Par conséquent, si vous souhaitez que la fonction d'effet secondaire soit transmise par l'utilisateur, vous en avez besoin. à passer

fn.effect.fn

Venez le chercher.

La classe ReactiveEffect est appelée dans la fonction d'effet pour créer des effets secondaires. Examinons ensuite l'implémentation de la classe ReactiveEffect.

Classe ReactiveEffect🎜
// packages/reactivity/src/effect.ts

export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined

  /**
   * Can be attached after creation
   * @internal
   */
  computed?: ComputedRefImpl<T>
  /**
   * @internal
   */
  allowRecurse?: boolean

  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
  ) {
    recordEffectScope(this, scope)
  }
  
  run() {
    // 如果 effect 已停用,返回原始副作用函数执行后的结果
    if (!this.active) {
      return this.fn()
    }
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    try {
      // 创建一个新的副作用前将当前正在执行的副作用存储到新建的副作用的 parent 属性上,解决嵌套effect 的情况
      this.parent = activeEffect
      // 将创建的副作用设置为当前正则正在执行的副作用   
      activeEffect = this
      // 将 shouldTrack 设置为 true,表示开启依赖收集
      shouldTrack = true

      trackOpBit = 1 << ++effectTrackDepth

      if (effectTrackDepth <= maxMarkerBits) {
        // 初始化依赖
        initDepMarkers(this)
      } else {
        // 清除依赖
        cleanupEffect(this)
      }
    //   返回原始副作用函数执行后的结果
      return this.fn()
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        finalizeDepMarkers(this)
      }

      trackOpBit = 1 << --effectTrackDepth

      // 重置当前正在执行的副作用   
      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined
    }
  }
  // 停止(清除) effect
  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}
🎜Dans la classe ReactiveEffect, une méthode d'exécution est définie. Cette méthode d'exécution est la méthode d'exécution réelle lors de la création d'effets secondaires. Chaque fois qu'une mise à jour est envoyée, cette méthode d'exécution sera exécutée, mettant ainsi à jour la valeur. 🎜

全局变量 activeEffect 用来维护当前正在执行的副作用,当存在嵌套渲染组件的时候,依赖收集后,副作用函数会被覆盖,即 activeEffect 存储的副作用函数在嵌套 effect 的时候会被内层的副作用函数覆盖。为了解决这个问题,在 run 方法中,将当前正在执行的副作用activeEffect保存到新建的副作用的 parent 属性上,然后再将新建的副作用设置为当前正在执行的副作用。在新建的副作用执行完毕后,再将存储到 parent 属性的副作用重新设置为当前正在执行的副作用。

在 ReactiveEffect 类中,还定义了一个 stop 方法,该方法用来停止并清除当前正在执行的副作用。

track 收集依赖

当使用代理对象访问对象的属性时,就会触发代理对象的 get 拦截函数执行,如下面的代码所示:

const obj = { foo: 1 }

const p = new Proxy(obj, {
  get(target, key, receiver) {
    track(target, key)
    return Reflect.get(target, key, receiver)
  }
}) 

p.foo

在上面的代码中,通过代理对象p 访问 foo 属性,便会触发 get 拦截函数的执行,此时就在 get 拦截函数中调用 track 函数进行依赖收集。源码中 get 拦截函数的解析可阅读《Vue3 源码解读之非原始值的响应式原理》一文中的「访问属性的拦截」小节。

下面,我们来看看 track 函数的实现。

track 函数

// packages/reactivity/src/effect.ts

// 收集依赖
export function track(target: object, type: TrackOpTypes, key: unknown) {
    // 如果开启了依赖收集并且有正在执行的副作用,则收集依赖
  if (shouldTrack && activeEffect) {
    // 在 targetMap 中获取对应的 target 的依赖集合
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      // 如果 target 不在 targetMap 中,则加入,并初始化 value 为 new Map()
      targetMap.set(target, (depsMap = new Map()))
    }
    // 从依赖集合中获取对应的 key 的依赖
    let dep = depsMap.get(key)
    if (!dep) {
      // 如果 key 不存在,将这个 key 作为依赖收集起来,并初始化 value 为 new Set()
      depsMap.set(key, (dep = createDep()))
    }

    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined

    trackEffects(dep, eventInfo)
  }
}

在 track 函数中,通过一个 if 语句判断是否进行依赖收集,只有当 shouldTrack 为 true 并且存在 activeEffect,即开启了依赖收集并且存在正在执行的副作用时,才进行依赖收集。

然后通过 target 对象从 targetMap 中尝试获取对应 target 的依赖集合depsMap,如果 targetMap 中不存在当前target的依赖集合,则将当前 target 添加进 targetMap 中,并将 targetMap 的 value 初始化为 new Map()。

// 在 targetMap 中获取对应的 target 的依赖集合
let depsMap = targetMap.get(target)
if (!depsMap) {
  // 如果 target 不在 targetMap 中,则加入,并初始化 value 为 new Map()
  targetMap.set(target, (depsMap = new Map()))
}

接着根据target中被读取的 key,从依赖集合depsMap中获取对应 key 的依赖,如果依赖不存在,则将这个 key 的依赖收集到依赖集合depsMap中,并将依赖初始化为 new Set()。

// 从依赖集合中获取对应的 key 的依赖
let dep = depsMap.get(key)
if (!dep) {
  // 如果 key 不存在,将这个 key 作为依赖收集起来,并初始化 value 为 new Set()
  depsMap.set(key, (dep = createDep()))
}

最后调用 trackEffects 函数,将副作用函数收集到依赖集合depsMap中。

const eventInfo = __DEV__
  ? { effect: activeEffect, target, type, key }
  : undefined

trackEffects(dep, eventInfo)

trackEffects 函数

// 收集副作用函数
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.
    // 如果依赖中并不存当前的 effect 副作用函数
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    // 将当前的副作用函数收集进依赖中
    dep.add(activeEffect!)
    // 并在当前副作用函数的 deps 属性中记录该依赖
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        Object.assign(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo
        )
      )
    }
  }
}

在 trackEffects 函数中,检查当前正在执行的副作用函数 activeEffect 是否已经被收集到依赖集合中,如果没有,就将当前的副作用函数收集到依赖集合中。同时在当前副作用函数的 deps 属性中记录该依赖。

trigger 派发更新

当对属性进行赋值时,会触发代理对象的 set 拦截函数执行,如下面的代码所示:

const obj = { foo: 1 }

const p = new Proxy(obj, {
  // 拦截设置操作
  set(target, key, newVal, receiver){
    // 如果属性不存在,则说明是在添加新属性,否则设置已有属性
    const type = Object.prototype.hasOwnProperty.call(target,key) ?  &#39;SET&#39; : &#39;ADD&#39;
    
    // 设置属性值
    const res = Reflect.set(target, key, newVal, receiver)
    // 把副作用函数从桶里取出并执行,将 type 作为第三个参数传递给 trigger 函数
    trigger(target,key,type)
    
    return res
  }
  
  // 省略其他拦截函数
})

p.foo = 2

在上面的代码中,通过代理对象p 访问 foo 属性,便会触发 set 拦截函数的执行,此时就在 set 拦截函数中调用 trigger 函数中派发更新。源码中 set 拦截函数的解析可阅读《Vue3 源码解读之非原始值的响应式原理》一文中的「设置属性操作的拦截」小节。

下面,我们来看看 track 函数的实现。

trigger 函数

trigger 函数的源码如下:

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  // 该 target 从未被追踪,则不继续执行
  if (!depsMap) {
    // never been tracked
    return
  }

  // 存放所有需要派发更新的副作用函数  
  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    // 当需要清除依赖时,将当前 target 的依赖全部传入
    deps = [...depsMap.values()]
  } else if (key === &#39;length&#39; && isArray(target)) {
    // 处理数组的特殊情况
    depsMap.forEach((dep, key) => {
      // 如果对应的长度, 有依赖收集需要更新
      if (key === &#39;length&#39; || key >= (newValue as number)) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    // 在 SET | ADD | DELETE 的情况,添加当前 key 的依赖
    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)) {
            // 操作类型为 ADD 时触发Map 数据结构的 keys 方法的副作用函数重新执行
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          deps.push(depsMap.get(&#39;length&#39;))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            // 操作类型为 DELETE 时触发Map 数据结构的 keys 方法的副作用函数重新执行
            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]) {
      if (__DEV__) {
        triggerEffects(deps[0], eventInfo)
      } else {
        triggerEffects(deps[0])
      }
    }
  } else {
    const effects: ReactiveEffect[] = []
    // 将需要执行的副作用函数收集到 effects 数组中
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

在 trigger 函数中,首先检查当前 target 是否有被追踪,如果从未被追踪过,即target的依赖未被收集,则不需要执行派发更新,直接返回即可。

const depsMap = targetMap.get(target)
// 该 target 从未被追踪,则不继续执行
if (!depsMap) {
  // never been tracked
  return
}

接着创建一个 Set 类型的 deps 集合,用来存储当前target的这个 key 所有需要执行派发更新的副作用函数。

// 存放所有需要派发更新的副作用函数  
let deps: (Dep | undefined)[] = []

接下来就根据操作类型type 和 key 来收集需要执行派发更新的副作用函数。

如果操作类型是 TriggerOpTypes.CLEAR ,那么表示需要清除所有依赖,将当前target的所有副作用函数添加到 deps 集合中。

if (type === TriggerOpTypes.CLEAR) {
  // collection being cleared
  // trigger all effects for target
  // 当需要清除依赖时,将当前 target 的依赖全部传入
  deps = [...depsMap.values()]
}

如果操作目标是数组,并且修改了数组的 length 属性,需要把与 length 属性相关联的副作用函数以及索引值大于或等于新的 length 值元素的相关联的副作用函数从 depsMap 中取出并添加到 deps 集合中。

else if (key === &#39;length&#39; && isArray(target)) {
  // 如果操作目标是数组,并且修改了数组的 length 属性
  depsMap.forEach((dep, key) => {
    // 对于索引大于或等于新的 length 值的元素,
    // 需要把所有相关联的副作用函数取出并添加到 deps 中执行
    if (key === &#39;length&#39; || key >= (newValue as number)) {
      deps.push(dep)
    }
  })
}

如果当前的 key 不为 undefined,则将与当前key相关联的副作用函数添加到 deps 集合中。注意这里的判断条件 void 0,是通过 void 运算符的形式表示 undefined 。

if (key !== void 0) {
  deps.push(depsMap.get(key))
}

接下来通过 Switch 语句来收集操作类型为 ADD、DELETE、SET 时与 ITERATE_KEY 和 MAP_KEY_ITERATE_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)) {
        // 操作类型为 ADD 时触发Map 数据结构的 keys 方法的副作用函数重新执行
        deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
      }
    } else if (isIntegerKey(key)) {
      // new index added to array -> length changes
      deps.push(depsMap.get(&#39;length&#39;))
    }
    break
  case TriggerOpTypes.DELETE:
    if (!isArray(target)) {
      deps.push(depsMap.get(ITERATE_KEY))
      if (isMap(target)) {
        // 操作类型为 DELETE 时触发Map 数据结构的 keys 方法的副作用函数重新执行
        deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
      }
    }
    break
  case TriggerOpTypes.SET:
    if (isMap(target)) {
      deps.push(depsMap.get(ITERATE_KEY))
    }
    break
}

最后调用 triggerEffects 函数,传入收集的副作用函数,执行派发更新。

const eventInfo = __DEV__
  ? { target, type, key, newValue, oldValue, oldTarget }
  : undefined

if (deps.length === 1) {
  if (deps[0]) {
    if (__DEV__) {
      triggerEffects(deps[0], eventInfo)
    } else {
      triggerEffects(deps[0])
    }
  }
} else {
  const effects: ReactiveEffect[] = []
  // 将需要执行的副作用函数收集到 effects 数组中
  for (const dep of deps) {
    if (dep) {
      effects.push(...dep)
    }
  }
  if (__DEV__) {
    triggerEffects(createDep(effects), eventInfo)
  } else {
    triggerEffects(createDep(effects))
  }
}

triggerEffects 函数

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  // 遍历需要执行的副作用函数集合   
  for (const effect of isArray(dep) ? dep : [...dep]) {
    // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
    if (effect !== activeEffect || effect.allowRecurse) {
      if (__DEV__ && effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      if (effect.scheduler) {
        // 如果一个副作用函数存在调度器,则调用该调度器
        effect.scheduler()
      } else {
        // 否则直接执行副作用函数
        effect.run()
      }
    }
  }
}

在 triggerEffects 函数中,遍历需要执行的副作用函数集合,如果当前副作用函数存在调度器,则执行该调度器,否则直接执行该副作用函数的 run 方法,执行更新。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer