您的位置:首页 > 技术中心 > 前端框架 >

Vue3响应式核心之effect怎么使用

时间:2023-05-10 14:56

通常情况下我们是不会直接使用effect的,因为effect是一个底层的API,在我们使用Vue3的时候Vue默认会帮我们调用effect。 effect翻译为作用,意思是使其发生作用,这个使其的其就是我们传入的函数,所以effect的作用就是让我们传入的函数发生作用,也就是执行这个函数。 执行过程简图如下:

Vue3响应式核心之effect怎么使用

接下来先通过例子了解effect的基本用法,然后再去了解原理。

一、effect用法

1、基本用法

const obj = reactive({count: 1})const runner = effect(() => {  console.log(obj.count)})obj.count++

结果会先打印1, 然后在obj.count++之后打印出2。

流程简图如下:

Vue3响应式核心之effect怎么使用

运行effect(fun)

// 先执行fun()  // 打印出1const runner = new ReactiveEffect(fn)return runnerrunner: {  run() {    this.fun() //执行fun  },  stop() {  }}

console.log(obj.count)track依赖收集 结构如下:

Vue3响应式核心之effect怎么使用

obj.count++触发依赖,执行runner.run(), 实际运行的是

() => {  console.log(obj.count)}

所以又打印出2

2、lazy属性为true

此值为 true 时,只有在第一次手动调用 runner 后,依赖数据变更时,才会自动执行 effect 的回调,可以理解为 effect 的是在手动调用 runner 后才首次执行

const obj = reactive({count: 1})const runner = effect(() => {  console.log(obj.count)}, {  lazy: true})runner()obj.count++

只会打印出2

原因是effect源码中有如下逻辑:

Vue3响应式核心之effect怎么使用

3、options中包含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)

看下events的打印结果:

Vue3响应式核心之effect怎么使用

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

二、源码分析

1、effect方法的实现

// packages/reactivity/src/effect.tsexport 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}

核心代码:

创建一个响应式副作用函数const _effect = new ReactiveEffect(fn),其运行结果如下:

Vue3响应式核心之effect怎么使用

非lazy状态执行响应式副作用函数_effect.run()

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

_effect.run作用域绑定到_effect

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

返回副作用函数runner

2、ReactiveEffect函数源码

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    }  }}
  • run方法的作用就是执行副作用函数,并且在执行副作用函数的过程中,会收集依赖;

  • stop方法的作用就是停止当前的ReactiveEffect对象,停止之后,就不会再收集依赖了;

  • activeEffect和this并不是每次都相等的,因为activeEffect会跟着调用栈的深度而变化,而this则是固定的;

三、依赖收集相关

1、如何触发依赖收集

在副作用函数中, obj.count就会触发依赖收集

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

触发的入口在get拦截器里面

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

2、track源码

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

shouldTrack在上面也讲过,它的作用就是控制是否收集依赖;

activeEffect就是我们刚刚讲的ReactiveEffect对象,它指向的就是当前正在执行的副作用函数;

track方法的作用就是收集依赖,它的实现非常简单,就是在targetMap中记录下target和key;

targetMap是一个WeakMap,它的键是target,值是一个Map,这个Map的键是key,值是一个Set;

targetMap的结构伪代码如下:

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

Vue3响应式核心之effect怎么使用

以上是最原始的depMap

dev环境为增加响应式调试会增加eventInfo

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

eventInfo结构如下:

Vue3响应式核心之effect怎么使用

trackEffects(dep, eventInfo)

如果 dep 中没有当前的 ReactiveEffect 对象,就会添加进去, 作用就把对象的属性操作与副作用函数建立关联,接下来看trackEffects

3、trackEffects(dep, eventInfo)源码解读

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

dep.add(activeEffect!) 如果 dep 中没有当前的 ReactiveEffect 对象,就会添加进去

Vue3响应式核心之effect怎么使用

最终生成的depTarget结构如下:

Vue3响应式核心之effect怎么使用

四、触发依赖

比如例子中代码obj.count++就会触发set拦截,触发依赖更新

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'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依赖更新      }    }    //...  }

1、trigger依赖更新

// 路径:packages/reactivity/src/effect.tsexport 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 === 'length' && isArray(target)) {    const newLength = Number(newValue)    depsMap.forEach((dep, key) => {      if (key === 'length' || 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('length'))        }        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))    }  }}

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

Vue3响应式核心之effect怎么使用

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

Vue3响应式核心之effect怎么使用

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

Vue3响应式核心之effect怎么使用

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

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

主要步骤

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

Vue3响应式核心之effect怎么使用

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

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

以上就是Vue3响应式核心之effect怎么使用的详细内容,更多请关注Gxl网其它相关文章!

热门排行

今日推荐

热门手游