【源码计划】vue3-effect解读

102 阅读9分钟

前置

本文主要是对vue3中响应式effect源码的解读。

1、介绍effect

把对象转换成响应式对象,还需要依赖收集触发更新,组成一个响应式系统。effect是响应式的核心,主要功能就是依赖收集和触发更新。被传入effect的函数称为副作用函数,这个函数就会在触发更新时执行。

什么是副作用函数:

函数内部使用到的响应式对象与effect建立映射关系。当这个响应式对象发生变化,这个effect能找到这个函数并执行。computed和watch的底层核心就是effect,所以传入这个两个api的函数也是副作用函数。

2、使用

const state = reactive({ age: 18, count:0 })
effect(() => {
    console.log('age:', state.age)
})
setTimeout(() => {
    state.age++
}, 1000)
setTimeout(() => {
    state.count++
}, 2000)

// 打印结果:
// age: 18
// age: 19

结果:

1、第一次打印age: 18,因为effect默认执行一次

2、一秒后修改第二次打印age: 19,因为state.ageeffect建立映射,age这个属性被收集了。当age变化了触发更新,副作用函数执行。

3、两秒后没有打印,因为effect内没有使用count这个属性,所以没有依赖收集也就不会触发更新。

简化代码

创建effect

export let activeEffect: ReactiveEffect | undefined // 当前正在执行的effectclass ReactiveEffect<T = any> {
  deps: Dep[] = []  // 收集effect中使用到的属性,依赖收集,记录哪些属性是在effect中使用的
  parent: ReactiveEffect | undefined = undefined // 父级的effect
​
  constructor(
    public fn: () => T,
  ) {}
​
  run() {
    let parent: ReactiveEffect | undefined = activeEffect
    
    try {
      this.parent = activeEffect    // 缓存父级的effect
      activeEffect = this   // 设置成正在执行的是当前effect
    
      // 执行用户传入的副作用函数
      return this.fn()
    } finally {
      // 执行完毕后还原activeEffect
      activeEffect = this.parent
      this.parent = undefined
    }
  }
}
​
// 1.effect
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  // 将用户的函数变成一个响应式的函数
  const _effect = new ReactiveEffect(fn) ///2.创建响应式effect
​
  // 3.执行run方法
  if (!options || !options.lazy) {
    // 是不是默认执行
    _effect.run()
  }
  // 4.确保在外面调用runner时this指向的正确性   
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

解读过程:

1、effect接收两个参数,用户传入的副作用函数和配置项

// 配置项
interface ReactiveEffectOptions {
  lazy?: boolean    // effecct不会立刻执行
  scheduler?: EffectScheduler   // 调度函数
  scope?: EffectScope   // effect作用域
  allowRecurse?: boolean    // 允许更新触发effect执行
  onStop?: () => void   // 终止函数stop触发
  onTrack?: (event: DebuggerEvent) => void  // 收集依赖触发
  onTrigger?: (event: DebuggerEvent) => void    // 触发更新触发
}

2、创建响应式effect

3、执行effect,run方法中的activeEffect是个全局变量,保存当前的effect,在响应式对象get或者set触发的时候,能从activeEffect拿到当前的effect

parent是用在嵌套effect中,保留父级的effect,实例如下

// 全局变量activeEffect 
// 这是effect1
effect(()=>{    
    // 全局变量activeEffect = effect1
    state.name;
    // 这是effect2
    effect(()=>{    
        // effect2缓存了上一个effect也就是effect1
        // effect2.parent = activeEffect = effect1
        // activeEffect = effect2
        state.age
    }) 
    // effect2执行结束,后还原activeEffect为effec1
    // activeEffect = effect2.parent = effect1
    state.address;
})

收集依赖

//1.
function get(target: Target, key: string | symbol, receiver: object) {
    // ...省略无关代码
    // 取值
    const res = Reflect.get(target, key, receiver)
    // 如果是仅读的不需要做依赖收集
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)  // 收集依赖
    }
    // ...省略无关代码
    return res
}
​
// 记录依赖关系
const targetMap = new WeakMap<any, KeyToDepMap>()
function track(target: object, type: TrackOpTypes, key: unknown) {
  // 否开启依赖搜集 以及 当前这个属性是在effect中使用才收集
  if (shouldTrack && activeEffect) {
    // 2.
    // 第一次用源对象去找一下有没有映射表
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      // 没有值说明没有维护过依赖,给源对象创建一个空的map映射表
      targetMap.set(target, (depsMap = new Map()))
    }
    // 属性值查一下有没有映射表
    let dep = depsMap.get(key)
    if (!dep) {
      // 没有key对应的映射,给属性值创建一个空的set映射表
      depsMap.set(key, (dep = new Set()))
    }
​
    // 不用重复收集同个key
    let shouldTrack = !dep.has(activeEffect)
    // 没有重复的
    if (shouldTrack) {
       // 依赖记录effect        这里实现n对n
       dep.add(activeEffect);
       // dep = [effect, effect]
       activeEffect.deps.push(dep); // 让effect记住dep,这样后续可以用于清理
    }
  }
}
​

解读过程:

1、这里参考reactiveget。即响应式对象获取值的时候,会判断是否依赖收集以及是否在effect中

2、依赖收集过程:记录依赖关系以及依赖收集过程,通过Map建立关联。

  • 首先关联目标对象被触发的属性,然后被触发的属性再记录当前的activeEffect,这里记录的属性可以有多个Effect
  • 最后给当前的Effect也就是activeEffect添加deps属性来记录有哪些属性(一个对象对应一个Map,Map里面一个key对应一个Set)。

过程如下:

const state = reactive({age: 18})
effect(()=>{
    // effect1
    state.age
})
effect(()=>{
    // effect2
    state.age
})

targetMap = {({ age: 18}): Map2 } 用一个Map来描述对象和key的关系

Map2 = {age: [effect1, effect2]} 描述key和effect的关系

结果如下

targetMap = {({ age: 18}): {age: [effect1, effect2]} }

总结:将属性和对应的effect维护成映射关系,后续属性变化可以触发对应的effect函数重新run

触发更新

// 1.
function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // ...省略无关代码
    // 查看是否有过这个key
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length // 数组访问索引在长度中
        : hasOwn(target, key) // 对象有这个属性
    const result = Reflect.set(target, key, value, receiver)
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 添加
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        // 如果前后置有变化触发修改逻辑
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
}
​
// 2.触发更新过程
export function trigger(  
    target: object,
    type: TriggerOpTypes,
    key?: unknown,
    newValue?: unknown,
    oldValue?: unknown,
    oldTarget?: Map<unknown, unknown> | Set<unknown>
  ) {
    const depsMap = targetMap.get(target)
    if (!depsMap) {
        // 从未收集过
        // never been tracked
        return
    }
    const effects = depsMap.get(key);
    effects && effects.forEach(effect => {
        // 当前正在执行和现在要执行的是同一个就屏蔽(比如在effect又修改同个值)
        if (effect !== activeEffect) effect.run(); 
        if (effects) {
        effects = new Set(effects);
        for (const effect of effects) {
            if (effect !== activeEffect) { 
                if(effect.scheduler){ // 如果有调度函数则执行调度函数
                    effect.scheduler()
                }else{
                    effect.run(); 
                }
            }
        }
    })
}

解读过程:

1、这里参考reactiveset。即响应式对象设置值的时候,会判断是新增的属性还是变化的属性

2、触发更新过程

  • 判断目标对象和触发的属性是否存在依赖收集
  • 将属性对应关联的一个或多个effect调用scheduler或者run方法触发更新。

什么是scheduler?

trigger触发时,可以自己决定副作用函数执行方式

const state = reactive({ age: 18 });
const runner = effect(
    () => {
        console.log('runner');
    },
    {
        scheduler() {
            console.log('scheduler');
        },
    }
);
state.age ++;
// 打印结果
// runner   第一次默认运行effect.run()
// scheduler    第二次运行effect.scheduler()

源码解读

effect

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn // 是否已经是effect,是effect获取对应的原函数
  }
​
  const _effect = new ReactiveEffect(fn) // 创建响应式effect
  if (options) {
    extend(_effect, options)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    // 是不是立即执行
    _effect.run()
  }
  // this指向的正确性
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

ReactiveEffect

export class ReactiveEffect<T = any> {
  active = true     // 激活响应式effect
  deps: Dep[] = []  // 收集effect中使用到的属性
  parent: ReactiveEffect | undefined = undefined
​
  computed?: ComputedRefImpl<T> // 标记computed
  allowRecurse?: boolean    
  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 // 作用域对象
  ) {
    recordEffectScope(this, scope)  // 记录effect的作用域,可以灵活控制effect的执行
  }
​
  run() {
    if (!this.active) {
      return this.fn()
    }
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    while (parent) {
      // 防止死循环
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    try {
      this.parent = activeEffect
      activeEffect = this
      shouldTrack = true
    
      // 每次执行嵌套的effect函数时会递增,通过位操作的方式
      trackOpBit = 1 << ++effectTrackDepth
      if (effectTrackDepth <= maxMarkerBits) {  // const maxMarkerBits = 30
        // effect会记录依赖是否被追踪,这里初始化记录
        initDepMarkers(this)
      } else {
        // 这里会清空当前的effect被之前依赖收集的映射
        // 下面运行的时候会重新进行依赖收集,为了避免不需要的旧的依赖还被收集
        cleanupEffect(this)
      }
      return this.fn()
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        // 处理依赖关系,之前的收集依赖现在没有收集,就需要清除
        finalizeDepMarkers(this)
      }
    
      // 恢复嵌套
      trackOpBit = 1 << --effectTrackDepth
​
      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined
​
      if (this.deferStop) {
        this.stop()
      }
    }
  }
​
  // 停止effect
  stop() {
    // 停止操作发生在当前的effect,会延迟执行
    // stopped while running itself - defer the cleanup
    if (activeEffect === this) {
      this.deferStop = true
    } else if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      // 当前effect设置不激活
      this.active = false
    }
  }
}

dep.ts

// 依赖在之前被收集
export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0// 依赖在现在的effect中被收集
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0// 初始化依赖记录
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].w |= trackOpBit // set was tracked
    }
  }
}
​
// 处理依赖关系
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
  const { deps } = effect
  if (deps.length) {
    let ptr = 0
    for (let i = 0; i < deps.length; i++) {
      const dep = deps[i]
      if (wasTracked(dep) && !newTracked(dep)) {
        // 之前的收集依赖现在没有收集,就需要清除
        dep.delete(effect)
      } else {
        deps[ptr++] = dep
      }
      // clear bits
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    deps.length = ptr
  }
}

track

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
    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()))
    }
​
    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined
​
    trackEffects(dep, eventInfo)
  }
}
​
export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
  dep.w = 0     // 是否被收集
  dep.n = 0     // 是否新收集的依赖
  return dep
}
​
export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
   
    if (!newTracked(dep)) {
      // 这个依赖现在没有被收集,需要设置新依赖标识
      dep.n |= trackOpBit // set newly tracked
      // 这个依赖被收集了true,就不需要再收集shouldTrack=false
      // 这个依赖没有被收集,就需要收集shouldTrack=true
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }
​
  // 依赖收集
  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        extend(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo!
        )
      )
    }
  }
}

trigger

// 迭代行为标识符
export const ITERATE_KEY = Symbol('iterate')
export const MAP_KEY_ITERATE_KEY = Symbol("Map key iterate")
​
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 从未收集过
    // never been tracked
    return
  }
​
  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // 针对集合触发清理数据,所有的dep应该触发
    // 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)) {
          // 添加属性的时候,要触发迭代收集的key
          // track(target, "iterate", isArray(target) ? "length" : ITERATE_KEY);
          // 因为被收集的依赖是迭代操作for in会添加ITERATE_KEY
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
          // 如果是map新增属性触发length更新
            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:
        // Map对象发生更改时,更新依赖
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }
​
  // 开发环境onTrigger回调的信息
  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))
    }
  }
}
​
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)
    }
  }
}
​
function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

小结

  • effect通过副作用函数和响应式数据组成响应式
  • 响应式数据触发get方法,执行track,通过targetMapeffect和收集的targetkey建立映射关系。
  • 响应式数据触发set方法,执行trigger,通过targetkeytargetMap中触发对应的effect