写点什么

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

用户头像
梁龙先森
关注
发布于: 2021 年 03 月 14 日
Vue3源码 | 深入理解响应式系统下篇-effect

上一篇阅读了响应式系统中关于如何创建响应式对象相关的源码,即通过 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,并赋值给activeEffectvar 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 响应式系统的主体实现思路,感兴趣,你也可以根据该流程自主实现一个最小响应式系统。(如有不准确地方,欢迎留言指正)


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


发布于: 2021 年 03 月 14 日阅读数: 35
用户头像

梁龙先森

关注

脚踏V8引擎的无情写作机器 2018.03.17 加入

还未添加个人简介

评论

发布
暂无评论
Vue3源码 | 深入理解响应式系统下篇-effect