Home >Web Front-end >Vue.js >Vue3 side effect function and dependency collection example analysis

Vue3 side effect function and dependency collection example analysis

WBOY
WBOYforward
2023-05-11 20:13:101299browse

Side Effect Function

A side effect function refers to a function that produces side effects, as shown in the following code:

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

When the effect function is executed, it will set the text content of the body, but Any function except the effect function can read or set the text content of the body. In other words, the execution of the effect function will directly or indirectly affect the execution of other functions. At this time, we say that the effect function has side effects. Side effects are easy to occur. For example, if a function modifies a global variable, this is actually a side effect.

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

Global variables of side-effect functions

In the side-effect module, several global variables are defined. Knowing these variables in advance will help us understand the generation and calling process of the side-effect function.

// 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 is a collection of WeakMap types, used to store side effect functions. From the type definition, we can see the data structure of targetMap:

  • WeakMap consists of target --> Map

  • Map consists of key --> Set

##The key of WeakMap is the original object target, the value of WeakMap is a Map instance, the key of Map is the key of the original object target, and the value of Map is a Set composed of side effect functions. Their relationship is as follows:

Vue3 side effect function and dependency collection example analysis

targetMap Why use WeakMap

Let’s look at the following code:

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
})

In the above code, The map and weakMap constants are defined, corresponding to instances of Map and WeakMap respectively. Two objects are defined inside the immediately executed function expression: foo and bar, which serve as the keys of map and weakMap respectively.

After the function expression is executed, for the object foo, it is still referenced as the key of the map, so

the garbage collector will not remove it from the memory. We can still print out object foo via map.keys.

For object bar, since the key of WeakMap is a weak reference, it does not affect the work of the garbage collector, so once the expression is executed, the garbage collector will remove the object bar from the memory. And we cannot obtain the key value of weakMap, so we cannot obtain the object bar through weakMap.

Simply put,

WeakMap is a weak reference to key and does not affect the work of the garbage collector**. According to this feature, once the key is recycled by the garbage collector, the corresponding key and value will not be accessible. Therefore, WeakMap is often used to store information that is valuable only when the object referenced by key exists (has not been recycled)**.

For example, in the above scenario, if the target object does not have any references, it means that the user side no longer needs it, and the garbage collector will complete the recycling task. But if you use Map instead of WeakMap, even if the user-side code does not have any reference to the target, the target will not be recycled, which may eventually lead to memory overflow.

activeEffect

activeEffect variable is used to maintain the side effects currently being executed

shouldTrack

shouldTrack variable is used to identify whether dependency collection is enabled, only the value of shouldTrack When it is true, dependency collection is performed, that is, the side-effect function is added to the dependency collection.

Implementation of side effects

effect function

effect API is used to create a side effect function, accepting two parameters, which are user-defined fn function and options options. The source code is as follows:

// 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
}

From the above code, we can know that when there is an effect side effect in the passed parameter fn, the original function of this side effect is assigned to fn. Then call the ReactiveEffect class to create an encapsulated side effect function.

In some scenarios, we do not want the effect to be executed immediately, but we want it to be executed when needed. We can achieve this by adding the lazy attribute to options. In the effect function source code, determine the value of the options.lazy option. When the value is true, the side effect function will not be executed immediately, thereby achieving a lazy execution effect.

Then return a new side effect function runner through the bind function. This of this new function is designated as _effect, and _effect is added to the effect attribute of the new side effect function, and finally the new side effect function is returned. .

Since the effect API returns the encapsulated side effect function, the original side effect function is stored in the effect attribute of the encapsulated side effect function, so if you want to get the side effect function passed in by the user, you need to pass

fn.effect.fn to obtain.

The ReactiveEffect class is called in the effect function to create side effects. Next, let’s look at the implementation of the ReactiveEffect class.

ReactiveEffect class

// 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
    }
  }
}

In the ReactiveEffect class, a run method is defined. This run method is the actual running method when creating side effects. Each time an update is dispatched, this run method will be executed, thereby updating the value.

全局变量 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 方法,执行更新。

The above is the detailed content of Vue3 side effect function and dependency collection example analysis. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:yisu.com. If there is any infringement, please contact admin@php.cn delete