Home > Web Front-end > Vue.js > body text

How to use the effect of Vue3 responsive core

WBOY
Release: 2023-05-10 11:19:19
forward
2730 people have browsed it

Normally we will not use effect directly, because effect is a low-level API. When we use Vue3, Vue will call effect for us by default. Effect is translated as effect, which means to make it work. The function that makes it work is the function we pass in, so the function of effect is to make the function we pass in take effect, that is, to execute this function. A simplified diagram of the execution process is as follows:

How to use the effect of Vue3 responsive core

Next, we will first understand the basic usage of effect through examples, and then understand the principle.

1. Effect usage

1. Basic usage

const obj = reactive({count: 1})

const runner = effect(() => {
  console.log(obj.count)
})

obj.count++
Copy after login

The result will print 1 first, and then print 2 after obj.count .

The process diagram is as follows:

How to use the effect of Vue3 responsive core

Run effect(fun)

// 先执行
fun()  // 打印出1

const runner = new ReactiveEffect(fn)

return runner

runner: {
  run() {
    this.fun() //执行fun
  },
  stop() {

  }
}
Copy after login

console.log (obj.count)The track dependency collection structure is as follows:

How to use the effect of Vue3 responsive core

obj.count Trigger dependencies, execute runner.run(), and actually run is

() => {
  console.log(obj.count)
}
Copy after login

, so it prints out 2

2. The lazy attribute is true

When this value is true, only after the first manual call to the runner, the dependent data The callback of the effect will be automatically executed when the change is made. It can be understood that the effect is executed for the first time after manually calling the runner.

const obj = reactive({count: 1})

const runner = effect(() => {
  console.log(obj.count)
}, {
  lazy: true
})
runner()
obj.count++
Copy after login

will only print out 2

. The reason is that the effect source code has the following Logic:

How to use the effect of Vue3 responsive core

3. Options include onTrack

let events = []
const onTrack = (e) => {
  events.push(e)
}
const obj = reactive({ foo: 1, bar: 2 })
const runner = effect(
  () => {
    console.log(obj.foo)
  },
  { onTrack }
)
console.log('runner', runner)
obj.foo++
console.log("events", events)
Copy after login

Look at the printed results of events:

How to use the effect of Vue3 responsive core

[
  {
    effect: runner,  // effect 函数的返回值
    target: toRaw(obj),  // 表示的是哪个响应式数据发生了变化
    type: TrackOpTypes.GET,  // 表示此次记录操作的类型。 get 表示获取值
    key: 'foo'
 }
]
Copy after login

2. Source code analysis

1. Implementation of effect method

// packages/reactivity/src/effect.ts
export interface ReactiveEffectOptions extends DebuggerOptions {
  lazy?: boolean
  scheduler?: EffectScheduler
  scope?: EffectScope
  allowRecurse?: boolean
  onStop?: () => void
}

export function effect<T = any>(
  fn: () => T, // 副作用函数
  options?: ReactiveEffectOptions // 结构如上
): ReactiveEffectRunner {
  // 如果 fn 对象上有 effect 属性
  if ((fn as ReactiveEffectRunner).effect) {
    // 那么就将 fn 替换为 fn.effect.fn
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  // 创建一个响应式副作用函数
  const _effect = new ReactiveEffect(fn)
  if (options) {
    // 将配置项合并到响应式副作用函数上
    extend(_effect, options)
    // 如果配置项中有 scope 属性(该属性的作用是指定副作用函数的作用域)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) { // options.lazy 不为true
    _effect.run() // 执行响应式副作用函数 首次执行fn()
  }
  // _effect.run作用域绑定到_effect
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  // 将响应式副作用函数赋值给 runner.effect
  runner.effect = _effect
  return runner
}
Copy after login

Core code:

Create a responsive side effect functionconst _effect = new ReactiveEffect( fn), the running results are as follows:

How to use the effect of Vue3 responsive core

Execute responsive side effect function in non-lazy state_effect.run()

if (!options || !options.lazy) { // options.lazy 不为true
  _effect.run() // 执行响应式副作用函数 首次执行fn()
}
Copy after login

_effect.runScope is bound to _effect

// _effect.run作用域绑定到_effect
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
Copy after login

Returns the side effect function runner

2, ReactiveEffect function source code

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
  /**
   * @internal
   */
  private deferStop?: 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
  ) {
    // 记录当前 ReactiveEffect 对象的作用域
    recordEffectScope(this, scope)
  }

  run() {
    // 如果当前 ReactiveEffect 对象不处于活动状态,直接返回 fn 的执行结果
    if (!this.active) {
      return this.fn()
    }
    // 寻找当前 ReactiveEffect 对象的最顶层的父级作用域
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack // 是否要跟踪
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    try {
      // 记录父级作用域为当前活动的 ReactiveEffect 对象
      this.parent = activeEffect
      activeEffect = this  // 将当前活动的 ReactiveEffect 对象设置为 “自己”
      shouldTrack = true // 将 shouldTrack 设置为 true (表示是否需要收集依赖)
      // effectTrackDepth 用于标识当前的 effect 调用栈的深度,执行一次 effect 就会将 effectTrackDepth 加 1
      trackOpBit = 1 << ++effectTrackDepth

      if (effectTrackDepth <= maxMarkerBits) {
        // 初始依赖追踪标记
        initDepMarkers(this)
      } else {
        // 清除依赖追踪标记
        cleanupEffect(this)
      }
      // 返回副作用函数执行结果
      return this.fn()
    } finally {
      // 如果 effect调用栈的深度 没有超过阈值
      if (effectTrackDepth <= maxMarkerBits) {
        // 确定最终的依赖追踪标记
        finalizeDepMarkers(this)
      }
      // 执行完毕会将 effectTrackDepth 减 1
      trackOpBit = 1 << --effectTrackDepth
      // 执行完毕,将当前活动的 ReactiveEffect 对象设置为 “父级作用域”
      activeEffect = this.parent
      // 将 shouldTrack 设置为上一个值
      shouldTrack = lastShouldTrack
      // 将父级作用域设置为 undefined
      this.parent = undefined
      // 延时停止,这个标志是在 stop 方法中设置的
      if (this.deferStop) {
        this.stop()
      }
    }
  }

  stop() {
    // stopped while running itself - defer the cleanup
    // 如果当前 活动的 ReactiveEffect 对象是 “自己”
    // 延迟停止,需要执行完当前的副作用函数之后再停止
    if (activeEffect === this) {
      // 在 run 方法中会判断 deferStop 的值,如果为 true,就会执行 stop 方法
      this.deferStop = true
    } else if (this.active) {// 如果当前 ReactiveEffect 对象处于活动状态
      cleanupEffect(this) // 清除所有的依赖追踪标记
      if (this.onStop) { 
        this.onStop()
      }
      this.active = false // 将 active 设置为 false
    }
  }
}
Copy after login
  • The function of the run method is to execute the side effect function, and in the process of executing the side effect function, dependencies will be collected;

  • The function of the stop method is to stop the current ReactiveEffect object, after stopping, will no longer collect dependencies;

  • activeEffect and this are not equal every time, because activeEffect will change with the depth of the call stack, and this It is fixed;

3. Dependency collection related

1. How to trigger dependency collection

In the side effect function, obj. count will trigger dependency collection

const runner = effect(() => {
  console.log(obj.count) 
})
Copy after login

The triggered entry is in the get interceptor

function createGetter(isReadonly = false, shallow = false) {
  // 闭包返回 get 拦截器方法
  return function get(target: Target, key: string | symbol, receiver: object) {
    // ...
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    // ...
  }
Copy after login

2. Track source code

const targetMap = new WeakMap();
/**
 * 收集依赖
 * @param target target 触发依赖的对象,例子中的obj
 * @param type 操作类型 比如obj.count就是get
 * @param key 指向对象的key, 比如obj.count就是count
 */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) { // 是否应该依赖收集 & 当前的new ReactiveEffect()即指向的就是当前正在执行的副作用函数

    // 如果 targetMap 中没有 target,就会创建一个 Map
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep())) // createDep 生成dep = { w:0, n: 0}
    }

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

    trackEffects(dep, eventInfo)
  }
}
Copy after login

shouldTrack has also been mentioned above. The function is to control whether to collect dependencies;

activeEffect is the ReactiveEffect object we just talked about, which points to the side effect function currently being executed;

The function of the track method is to collect dependencies, and its The implementation is very simple, just record the target and key in targetMap;

targetMap is a WeakMap, its key is target, its value is a Map, the key of this Map is key, and its value is a Set;

The structural pseudocode of targetMap is as follows:

targetMap = {
  target: { 
    key: dep
  },
  // 比如:
  obj: { 
    count: {
       w: 0, 
       n: 0
    }
  }
}
Copy after login

How to use the effect of Vue3 responsive core

The above is the original depMap

The dev environment will increase ## to increase responsive debugging #eventInfo

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

The eventInfo structure is as follows:

How to use the effect of Vue3 responsive core

trackEffects(dep, eventInfo)

if If there is no current ReactiveEffect object in dep, it will be added. The effect is to associate the object's property operation with the side effect function. Next, see

trackEffects

3, trackEffects(dep, eventInfo) Source code interpretation

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      // 执行之前 dep = Set(0) {w: 0, n: 0}

      // 执行之后 dep = Set(0) {w: 0, n: 2}
      dep.n |= trackOpBit // set newly tracked
  
      shouldTrack = !wasTracked(dep) 
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    // 将activeEffect添加到dep
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) { // onTrack逻辑
      activeEffect!.onTrack(
        extend(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo!
        )
      )
    }
  }
}
Copy after login

dep.add(activeEffect!) If there is no current ReactiveEffect object in dep, it will be added

How to use the effect of Vue3 responsive core

The final generated depTarget structure is as follows:

How to use the effect of Vue3 responsive core

4. Trigger dependencies

For example, the code

obj.count in the example will trigger set interception , trigger dependency update

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    //...
    const result = Reflect.set(target, key, value, receiver)
    // don&#39;t trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value) // 触发ADD依赖更新
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue) //触发SET依赖更新
      }
    }
    //...
  }
Copy after login

1、trigger依赖更新

// 路径:packages/reactivity/src/effect.ts
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target) // 获取depsMap, targetMap是在track中创建的依赖
  if (!depsMap) {
    // never been tracked
    return
  }

  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()]
  } else if (key === &#39;length&#39; && isArray(target)) {
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === &#39;length&#39; || key >= newLength) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    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)) {
            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)) {
            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[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}
Copy after login

const depsMap = targetMap.get(target) 获取 targetMap 中的 depsMap targetMap结构如下:

How to use the effect of Vue3 responsive core

执行以上语句之后的depsMap结构如下:

How to use the effect of Vue3 responsive core

将 depsMap 中 key 对应的 ReactiveEffect 对象添加到 deps 中deps.push(depsMap.get(key))之后的deps结构如下:

How to use the effect of Vue3 responsive core

triggerEffects(deps[0], eventInfo)

  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])
      }
    }
  }
Copy after login

trigger函数的作用就是触发依赖,当我们修改数据的时候,就会触发依赖,然后执行依赖中的副作用函数。

在这里的实现其实并没有执行,主要是收集一些需要执行的副作用函数,然后在丢给triggerEffects函数去执行,接下来看看triggerEffects函数。

2、triggerEffects(deps[0], eventInfo)

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}
Copy after login

主要步骤

const effects = isArray(dep) ? dep : [...dep]获取effects

How to use the effect of Vue3 responsive core

triggerEffect(effect, debuggerEventExtraInfo)执行effect,接下来看看源码

3、triggerEffect(effect, debuggerEventExtraInfo)

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
     // 如果 effect.onTrigger 存在,就会执行,只有开发模式下才会执行
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    // 如果 effect 是一个调度器,就会执行 scheduler
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      // 其它情况执行 effect.run()
      effect.run()
    }
  }
}
Copy after login

effect.run()就是执行副作用函数

The above is the detailed content of How to use the effect of Vue3 responsive core. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:yisu.com
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template