Vue3源码 | 深入理解响应式系统下篇-effect

457 阅读6分钟

「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

上一篇阅读了响应式系统中关于如何创建响应式对象相关的源码,即通过Proxy对target目标对象进行代理,并通过Proxy的馅饼函数对对象的操作进行劫持。在get函数中会根据入参决定是创建reactive、shallowReactive,还是readonly代理等,并且满足条件下会触发track函数追踪收集缓存数据到targetMap,这里targetMap是WeakMap的数据类型。。。

OK,更详细的内容,可通过如下链接点击阅读,当然targetMap为啥是WeakMap类型,而不是Map呢?

Vue3源码 | 深入理解响应式系统上篇-reactive

简单说明下:首先WeakMap的键名只能是对象,并且是对对象的弱引用,即垃圾回收机制不会将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

下面进入主题。

track

track追踪收集,是在执行代理对象的get陷阱函数进行的操作,它是将数据缓存到targetMap。下面看看具体的代码实现:

export function track(target: object, type: TrackOpTypes, key: unknown) {
  // shouldTrack:是否应该收集依赖
  // activeEffect:当前激活的efftct,也就是effect的回调函数,数据变化后执行的副作用函数
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // targetMap就是缓存数据的对象,weakMap类型
  // 每个target对应一个depsMap
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    // 每个key对应一个dep,注意这里是set集合类型
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    // 收集当前激活的effct作为依赖
    dep.add(activeEffect)
    // 激活的activeEffect收集dep作为依赖
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

框架是实现响应式系统,也就是当数据变化时能够做出一些响应。所以,可以猜出,这里收集的依赖,应该就是数据变化后执行的副作用函数。

// 其他代码省略

effect(() => {
  // 副作用函数,收集的应该是这玩意
  patch()
})

通过track我们也可以看出targetMap数据结构是这样的:

// targetMap简易描述
WeakMap -> {
  [target:代理对象]:{
  	[key:代理对象的key]:new Set(effect)
	}
}

接着上面的例子,再来看看,这里activeEffect具体是如何收集的。

effect

上面例子,我们猜测track收集追踪的是effect的内容,这里看下代码。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    // 如果已经是effect函数,则指向原始函数
    fn = fn.raw
  }
  // 创建响应式的副作用函数
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // 非lazy,则直接执行一次,这里lazy属性,computed计算属性会用到
    effect()
  }
  return effect
}
function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {// 未激活状态
      // 如果是非调度执行,则直接执行fn,这里是原始函数,effect入参时已做了转换
      return options.scheduler ? undefined : fn()
    }
    // effectStack:effect全局栈
    if (!effectStack.includes(effect)) {
      // 清空effect引用的依赖
      // 通过遍历effect.deps保存的effect,清空effect
      cleanup(effect)
      try {
        // 设置 sholdTrack = true,允许收集依赖
        enableTracking()
        // effect压入全局栈
        effectStack.push(effect)
        // 设置effect为当前激活effect
        activeEffect = effect
        // 执行原始函数
        return fn()
      } finally {
        // 出栈
        effectStack.pop()
        // 重置 sholdTrack = false
        resetTracking()
        // 指向最后一个effect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  // effect函数表示
  effect._isEffect = true
  // 是否激活状态
  effect.active = true
  // 包装的原始函数
  effect.raw = fn
  // effect对应的依赖
  effect.deps = []
  // effect相关配置
  effect.options = options
  return effect
}

不出意料,activeEffect 确实是通过 effect 函数进行的赋值初始化。这里的createReactiveEffect 函数看着是不是头大,但我们知道它是为了创建一个新的effect函数。抛开框架业务上的实现,我们看看函数的本质:


// 这里是给fn函数包裹了一层wrapper,并赋值给activeEffect
var activeEffect;
function wrapper(fn){
	var effect = function(...args){
    activeEffect = fn;
  	fn(...args)
  }
  return effect
}

看到这里,我们知道了effect主要是将全局的activeEffect变量指向当前effect,然后执行被包裹的原始函数fn。 从track追踪收集数据,以及副作用函数是如何被处理收集入缓存的,现在看看如何触发执行。

trigger

trigger函数是在代理对象触发set陷阱函数执行的,用于执行副作用函数。看看代码:

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 从依赖收集缓存对象targetMap获取target的依赖集合
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 不存在,则返回,也就是未被追踪track
    return
  }
	
  // 创建运行的effects集合
  const effects = new Set<ReactiveEffect>()
  // 用于添加effects的函数
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => effects.add(effect))
    }
  }

  if (type === TriggerOpTypes.CLEAR) {
    // 清楚集合类型,添加effect
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    //SET | ADD | DELETE 类型之一,添加effect
    if (key !== void 0) {
      add(depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    const isAddOrDelete =
      type === TriggerOpTypes.ADD ||
      (type === TriggerOpTypes.DELETE && !isArray(target))
    if (
      isAddOrDelete ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
      add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
    }
    if (isAddOrDelete && target instanceof Map) {
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
  }
	
  // 用于执行effect
  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    // 执行调度
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      // 直接执行
      effect()
    }
  }
	// 遍历执行effect
  effects.forEach(run)
}

简而言之,trigger 主要实现了从 targetMap 拿到 target 对应的依赖集合 depsMap ,根据 keydepsMap 找对对应的 effects 并添加到运行的 effects 集合中,然后遍历运行时的 effects 执行相关的副作用函数。

到这里我们大致讲解了reactive api的大致实现思路,当然关于具体的实现细节并没有深究,比如上面的添加 effect,为啥要先执行一次 cleanup 呢?

Ref API

前面的例子,都是围绕reactive api来分析响应式系统的实现,我们知道这个api只能处理对象或者数据类型,对基础类型(比如:Number、String、Boolean)是不支持的。因此Vue3提供了ref API。看下代码

export function ref(value?: unknown) {
  return createRef(value)
}

function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    // 已经是ref类型,则返回自身
    return rawValue
  }
  // 传入的是对象或数据,转换为reactive对象
  let value = shallow ? rawValue : convert(rawValue)
  const r = {
    // ref对象标识
    __v_isRef: true,
    get value() {
      // 收集依赖,key固定为value
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal) {
      // 判断value值是否存在修改
      if (hasChanged(toRaw(newVal), rawValue)) {
        rawValue = newVal
        value = shallow ? newVal : convert(newVal)
        // 派发通知,执行副作用函数
        trigger(r, TriggerOpTypes.SET, 'value', newVal)
      }
    }
  }
  return r
}

ref api实现通过创建具备value属性的对象,并对其set、get函数进行劫持,get函数进行收集数据依赖,set进行触发操作,并返回该对象来实现响应式。

总结

至此我们便分析了Vue3响应式系统的主体实现思路,感兴趣,你也可以根据该流程自主实现一个最小响应式系统。(如有不准确地方,欢迎留言指正)