vue3源码之旅-effect

1,028 阅读4分钟

更多文章

前言

之前介绍了reactiveref,接下来就是effect了,effect可以理解为依赖收集的过程,还是通过代码来看一下(简化代码中可以结合index1.html看一下)

简化代码

vue3-effect源码位置

依赖收集

reactiveref中均有对依赖的收集,get阶段和set阶段分别会触发track/trackEffectstrigger/triggerEffects

reactive相关源码如下:

// get
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
  }
}
// set
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
  }
}

ref相关源码如下:

export function trackRefValue(ref: RefBase<any>) {
  if (isTracking()) {
    ref = toRaw(ref)
    // 没有dep添加dep属性,Set类型
    if (!ref.dep) {
      ref.dep = createDep()
    }
    trackEffects(ref.dep)
  }
}

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  // 存在dep属性,通知依赖
  if (ref.dep) {
    triggerEffects(ref.dep)
  }
}
class RefImpl<T> {

  get value() {
    trackRefValue(this)
  }

  set value(newVal) {
    if (hasChanged(newVal, this._rawValue)) {
      triggerRefValue(this, newVal)
    }
  }
}

从源码中挑出了触发依赖收集的相关代码,这部分并不难理解,get阶段触发tracktrackEffects收集依赖(收集观察者),set阶段触发triggertriggerEffects触发依赖(通知观察者),而在effecttrack最终会触发trackEffectstrigger最终会触发triggerEffects,所以接下来从track开始对effect部分的解读,基本可以将整个过程分析完毕,不从trigger开始的原因是没有依赖何来的触发依赖

track

track代码如下(源码简化后):

const targetMap = new WeakMap()
let activeEffect

const createDep = (effects) => {
  const dep = new Set(effects)
  return dep
}

function isTracking() {
  return activeEffect !== undefined
}

function track(target, type, key) {
  if (!isTracking()) return
  // 是否有缓存
  let depsMap = targetMap.get(target)
  // 无缓存存储,初始化为map
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 获取当前属性是否有缓存
  let dep = depsMap.get(key)
  // 无缓存存储,初始化为set
  if(!dep) {
    depsMap.set(key, (dep = createDep()))
  }
  trackEffects(dep);
}

function trackEffects(dep) {
  // 是否存在
  let shouldTrack = !dep.has(activeEffect)
  // 不存在收集依赖
  if(shouldTrack) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}

代码中已经有部分注释,可以看出经过一些优化后,最终将activeEffect加入到dep中,数据存储关系如下: targetMap<weakMap> -> depsMap<Map> -> dep<Set> -> activeEffect<ReactiveEffect>,接下来看一下activeEffect是如何定义的,首先看一下effect干了些什么

function effect(fn, options = {}) {
  const _effect = new ReactiveEffect(fn)
  // 存在合并入_effect
  if(options) extend(_effect, options)
  // 初始执行run
  _effect.run()

  const runner = _effect.run.bind(_effect)
  runner.effect = _effect
  // 将runner返回出去,让外部可以主动调用run
  return runner
}

初始化(或合适时机)调用effect时会将ReactiveEffect实例化并执行run,执行run时会将当前ReactiveEffect实例赋值给activeEffect(代码在ReactiveEffect部分),最终将runner返回出去,不仅在数据更新时可以调用,同样可以选择在合适的时机主动触发,这里涉及到了ReactiveEffect,看一下实现:

class ReactiveEffect {
  active = true
  deps = []

  constructor(fn, scheduler) {
    this.fn = fn
    // 自定义scheduler,若存在触发triggerEffects时执行,否则执行fn
    this.scheduler = scheduler
  }

  run() {
    // 将当前实例赋值给activeEffect
    activeEffect = this
    return this.fn()
  }

  stop() {
    if(this.active) {
      cleanupEffect(this)
      this.active = false
    }
  }
}
// 清空effect
function cleanupEffect(effect) {
  effect.deps.forEach((dep) => dep.delete(effect))
  effect.deps.length = 0
}
function stop(runner) {
  runner.effect.stop();
}

抛去其他考虑,track的最简流程如下:

const targetMap = new WeakMap()

class ReactiveEffect {
  active = true
  deps = []

  constructor(fn) {
    this.fn = fn
  }

  run() {
    activeEffect = this
    return this.fn()
  }
}

function effect(fn) {
  const _effect = new ReactiveEffect(fn)
  _effect.run()
  const runner = _effect.run.bind(_effect)
  return runner
}

function track(target, key) {
  let depsMap = targetMap.get(target)
  let dep = depsMap.get(key)
  trackEffects(dep);
}

function trackEffects(dep) {
  let shouldTrack = !dep.has(activeEffect)
  if(shouldTrack) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}

场景假设

假设一个最简单的场景,更新id=appdom:

const state = reactive({ a: 100 })

effect(
  () => document.getElementById('app').innerText = state.a
)

当调用effect时,fn即为() => document.getElementById('app').innerText = state.aactiveEffect被初始化。当调用state.a时会将当前activeEffect添加到dep

现在有这么一个操作state.a = 0,则触发trigger,如果trigger能够触发当前activeEffect中的run就可已更新dom,关系如下:

get -> track -> 收集activeEffect

set -> trigger -> 告知activeEffect

接下来看一下trigger

trigger

function trigger(
  target,
  type,
  key,
  newValue,
  oldValue
) {
  // 获取缓存
  const depsMap = targetMap.get(target)
  // 无缓存表示从来没有触发过track,直接return
  if (!depsMap) return

  let deps = [];
  
  if (key !== void 0) deps.push(depsMap.get(key))
  const effects = []
  // 取出set数据
  for (const dep of deps) {
    if (dep) {
      effects.push(...dep)
    }
  }
  triggerEffects(createDep(effects))
}

function triggerEffects(dep) {
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect.scheduler) {
      effect.scheduler();
    } else {
      effect.run();
    }
  }
}

if (!depsMap) return这个动作还是比较重要的,targetMap中如果没有该值则表示没有触发过track,也就是没有观察者(没有通知对象),trigger最后会取出所有effect然后触发triggerEffects,当开发者传入了scheduler触发scheduler,否则触发run -> fn

ref

ref模块是添加了一个dep属性然后直接调用trackEffectstriggerEffects,基本是上边的搞清楚这个很容易就搞懂了,不再过多解释

get -> trackEffects -> 收集activeEffect

set -> triggerEffects -> 告知activeEffect

结语

配合着简化代码操作一下方便理解