【vue3源码】

【vue3源码】二、vue3的响应系统分析

2022-06-29  本文已影响0人  MAXLZ

vue3的响应系统分析

前言

参考代码版本:vue 3.2.37

官方文档:https://vuejs.org/

vue3的响应式处理主要集中在packages/reactivity/src/effect.ts文件中。

effect

vue3中,会使用一个effect方法注册副作用函数。为什么要注册副作用函数呢?

如果响应式数据更新,我们希望副作用函数中的相关数据也能同步更新。要实现这种效果,就需要我们做两个工作:

那么我们如何在设置响应式数据时,触发相关的副作用函数呢?这就需要我们在收集副作用函数时,使用某种数据结构把他暂存起来,等到需要到他的时候,就可以取出来。

effect的作用就是将我们注册的副作用函数暂存。下面我们来看effect的实现:

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }

  const _effect = new ReactiveEffect(fn)
  if (options) {
    extend(_effect, options)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

effect可以接收两个参数,其中第二个参数为可选参数,可以不传。第一个参数是一个副作用函数fn,第二个参数是个对象,该对象可以有如下属性:

effect中会首先检查fn.effect属性,如果存在fn.effect,那么说明fn已经被effect处理过了,然后使用fn.effect.fn作为fn

if ((fn as ReactiveEffectRunner).effect) {
  fn = (fn as ReactiveEffectRunner).effect.fn
}
const fn = () => {}
const runner1 = effect(fn)
const runner2 = effect(runner1)

runner1.effect.fn === fn // true
runner2.effect.fn === fn // true

然后new了一个ReactiveEffect对象。

const _effect = new ReactiveEffect(fn)

接着如果存在option对象的话,会将options,合并到_effect中。如果存在options.scope,会调用recordEffectScope_effect放入options.scope。如果不存在optionsoptions.lazy === false,那么会执行_effect.run(),进行依赖的收集。

if (options) {
  extend(_effect, options)
  if (options.scope) recordEffectScope(_effect, options.scope)
}
if (!options || !options.lazy) {
  _effect.run()
}

最后,会将_effect.run中的this指向它本身,这样做的目的是用户在主动执行runner时,this指针指向的是_effect对象,然后将_effect作为runnereffect属性,并将runner返回。

const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner

effect中创建了一个ReactiveEffect对象,这个ReactiveEffect是什么呢?接下来继续看ReactiveEffect的实现。

ReactiveEffect

export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined

  computed?: ComputedRefImpl<T>
  allowRecurse?: 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)
  }

  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

      trackOpBit = 1 << ++effectTrackDepth

      if (effectTrackDepth <= maxMarkerBits) {
        initDepMarkers(this)
      } else {
        cleanupEffect(this)
      }
      return this.fn()
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        finalizeDepMarkers(this)
      }

      trackOpBit = 1 << --effectTrackDepth

      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined
    }
  }

  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

ReactiveEffect是使用es6 class定义的一个类。它的构造器可以接受三个参数:fn(副作用函数)、scheduler(调度器)、scope(一个EffectScope作用域对象),在构造器中调用了一个recordEffectScope方法,这个方法会将当前ReactiveEffect对象(this)放入对应的EffectScope作用域(scope)中。

constructor(
  public fn: () => T,
  public scheduler: EffectScheduler | null = null,
  scope?: EffectScope
) {
  recordEffectScope(this, scope)
}

ReactiveEffect中有两个方法:runstop

run

run的执行过程中,会首先判断ReactiveEffect的激活状态(active),如果未激活(this.active === false),那么会立马执行this.fn并返回他的执行结果。

if (!this.active) {
  return this.fn()
}

然后声明了两个变量:parent(默认activeEffect)、lastShouldTrack(默认shouldTrack,一个全局变量,默认为true)。紧接着会使用while循环寻找parent.parent,一旦parentthis相等,立即结束循环。

let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
  if (parent === this) {
    return
  }
  parent = parent.parent
}

紧接着把activeEffect赋值给this.parent,把this赋值给this.parent

try {
  // 设置当前的parent为上一个activeEffect
  this.parent = activeEffect
  // 设置activeEffect为当前ReactiveEffect实例,activeEffect是个全局变量
  activeEffect = this
  shouldTrack = true
  // ...
}
// ...

这样做的目的是,建立一个嵌套effect的关系,来看下面一个例子:

const obj = reactive({a: 1})

effect(() => {
  console.log(obj.a)
  effect(() => {
    console.log(obj.a)
  })
})

当执行第一层_effect.run时,因为默认的activeEffectundefined,所以第一层effect中的_effect.parent=undefined,紧接着把this赋值给activeEffect,这时activeEffect指向的第一层的_effect

在第一层中的_effect.run执行过程中,最后会执行this.fn(),在执行this.fn()的过程中,会创建第二层effectReactiveEffect对象,然后执行_effect.run,因为在第一层中_effect.run运行过程中,已经将第一层的_effect赋给了activeEffect,所以第二层中的_effect.parent指向了第一层的_effect,紧接着又将第二次的_effect赋给了activeEffect。这样以来第一层effect与第二层effect就建立了联系。

当与父effect建立联系后,有这么一行代码:

trackOpBit = 1 << ++effectTrackDepth

其中effectTrackDepth是个全局变量为effect的深度,层数从1开始计数,trackOpBit使用二进制标记依赖收集的状态(如00000000000000000000000000000010表示所处深度为1)。

紧接着会进行一个条件的判断:如果effectTrackDepth未超出最大标记位(maxMarkerBits = 30),会调用initDepMarkers方法将this.deps中的所有dep标记为已经被track的状态;否则使用cleanupEffect移除deps中的所有dep

if (effectTrackDepth <= maxMarkerBits) {
  initDepMarkers(this)
} else {
  cleanupEffect(this)
}

这里为什么要标记已经被track的状态或直接移除所有dep?我们来看下面一个例子:

const obj = reactive({ str: 'objStr', flag: true })

effect(() => {
  const c = obj.flag ? obj.str : 'no found'
  console.log(c)
})

obj.flag = false

obj.str = 'test'

在首次track时,targetMap结构如下(targetMap在下文中有介绍):

effect-flow1.png

这时targetMap[toRaw(obj)](这里targetMap的键是obj的原始对象)中分别保存着strflag共两份依赖。当执行obj.flag=false后,会触发flag对应的依赖,此时打印not found

obj.flag变为false之后,副作用函数就不会受obj.str的影响了,之后的操作,无论obj.str如何变化,都不应该影响到副作用函数。这里标记dep为已被track或移除dep的作用就是实现这种效果。由于obj.flag的修改,会触发flag对应的副作用函数(执行run函数),此时this.deps中保存着strflag的对应的两份依赖,所以调用initDepMarkers后,会将这两份依赖标记为已收集,当this.fn()执行完毕后,会根据dep某些属性,将str所对应的依赖移除。这样无论修改str为和值,都没有对应的依赖触发。

所以initDepMarkers(在finally移除)/cleanupEffect的作用是移除多余的依赖。

回到run函数中,最后需要执行this.fn(),并将结果返回。这样就可以进行依赖的收集。在return fn()之后继续进入finally,在finally中需要恢复一些状态:finalizeDepMarkers根据一些状态移除多余的依赖、将effectTrackDepth回退一层,activeEffect指向当前ReactiveEffectparentshouldTrack = lastShouldTrackthis.parent置为undefined

try {
  // ...
  
  return this.fn()
} finally {
  if (effectTrackDepth <= maxMarkerBits) {
    finalizeDepMarkers(this)
  }

  trackOpBit = 1 << --effectTrackDepth

  activeEffect = this.parent
  shouldTrack = lastShouldTrack
  this.parent = undefined
}

run函数的作用就是会调用fn,并返回其结果,在执行fn的过程中会命中响应式对象的某些拦截操作,在拦截过程中进行依赖的收集。

stop

当调用stop函数后,会调用cleanupEffectReactiveEffect中所有的依赖删除,然后执行onStop钩子,最后将this.active置为false

stop() {
if (this.active) {
    cleanupEffect(this)
    if (this.onStop) {
      this.onStop()
    }
    this.active = false
  }
}

依赖收集

通过上面对effect的分析,在effect中如果未设置options.lazy = false的话,会直接执行_effect.run(),而在run()方法中最后最终会调用副作用函数fn。在fn的执行过程中,会读取某个响应式数据,而我们的响应式数据是被Proxy代理过的,一旦读取响应式数据的某个属性,就会触发Proxyget操作(不一定是get,这里以get为例进行说明)。在拦截过程中会触发一个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)
  }
}

track函数接收三个参数:target(响应式对象的原始对象)、type(触发依赖操作的方式,有三种取值:TrackOpTypes.GETTrackOpTypes.HASTrackOpTypes.ITERATE)、key(触发依赖收集的key)。

track中一上来就对shouldTrackactiveEffect进行了判断,只有shouldTracktrue且存在activeEffect时才可以进行依赖收集。

如果可以进行依赖收集的话,会从targetMap中获取target对应的值,这里targetMap保存着所有响应式数据所对应的副作用函数,它是个WeakMap类型的全局变量,WeakMap的键是响应式数据的原始对象target,值是个Map,而Map的键是原始对象的keyMap的值时一个由副作用函数(一个ReactiveEffect实例)组成的Set集合。

为什么target要使用WeakMap,而不是Map?因为WeakMap的键是弱引用,如果target被销毁后,那么它对应的值Map也会被回收。如果你不了解WeakMap的使用,请参考:MDN

type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
targetMap.png

如果从targetMap找不到target对应的值,则创建一个Map对象,存入targetMap中。

let depsMap = targetMap.get(target)
if (!depsMap) {
  targetMap.set(target, (depsMap = new Map()))
}

然后从depsMap中获取key对应的副作用集合,如果不存在,则创建一个Set,存入depsMap中。这里创建Set的过程中,会为Set实例添加两个属性:nww表示在副作用函数执行前dep是否已经被收集过了,n表示在当前收集(本次run执行)过程中dep是新收集的。

let dep = depsMap.get(key)
if (!dep) {
  depsMap.set(key, (dep = createDep()))
}

最后调用trackEffects方法。

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

trackEffects(dep, eventInfo)
export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // set newly tracked
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // 直接判断dep中是否含有activeEffect
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        Object.assign(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo
        )
      )
    }
  }
}

trackEffects接收两个参数:depReactiveEffect集合),debuggerEventExtraInfo(开发环境下activeEffect.onTrack钩子所需的参数)。

trackEffects中说先声明了一个默认值为falseshouldTrack变量,它代表我们需不需要收集activeEffect

如果shouldTracktrue的话,则将activeEffect添加到dep中,同时将dep放入activeEffect.deps中。

shouldTrack的确定和depnw属性密切相关。如果newTracked(dep) === true,说明在本次run方法执行过程中,dep已经被收集过了,shouldTrack不变;如果newTracked(dep) === false,要把dep标记为新收集的,虽然dep在本次收集过程中是新收集的,但它可能在之前的收集过程中已经被收集了,所以shouldTrack的值取决于dep是否在之前已经被收集过了。

// wasTracked(dep)返回true,意味着dep在之前的依赖收集过程中已经被收集过,或者说在之前run执行过程中已经被收集
export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0

// newTracked(dep)返回true,意味着dep是在本次依赖收集过程中新收集到的,或者说在本次run执行过程中新收集到的
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0

这里使用以下例子来说明shouldTrack的确认过程:

let sum
const counter = reactive({ num1: 0, num2: 0 })
effect(() => {
  sum = counter.num1 + counter.num1 + counter.num2
})

在上面例子中共经历3次依赖收集的过程。

  1. 第一次因为访问到counter.num1,被counterget拦截器拦截,因为最开始targetMap是空的,所以在第一次收集过程中会进行初始化,此时targetMap[toRaw(counter)].num1.n/w=0,当决定shouldTrack的值时,因为newTracked(dep)===false,所以shouldTrack=!wasTracked,显然wasTracked(dep)===falseshouldTrack值被确定为true,意味着依赖应该被收集,track执行完成后的targetMap结构为

    track-flow1.png
  2. 第二次同样访问到counter.num1,被counterget拦截器拦截,并开始收集依赖,但在这次收集过程中,因为newTracked(dep) === true,所以shouldTrackfalse,本次不会进行依赖的收集

  3. 第三次访问到counter.num2,过程与第一次相同,当本次track执行完毕后,targetMap结构为

    track-flow2.png
  4. 3次依赖收集完毕,意味着fn执行完毕,进入finally中,执行finalizeDepMarkers,此时会将_effect.deps中的dep.n恢复至0

触发依赖

在依赖被收集完成后,一旦响应式数据的某些属性改变后,就会触发对应的依赖。这个触发的过程发生在proxysetdeleteProperty拦截器、,或集合的get拦截器(拦截clearaddset等操作)。

在触发依赖时,会执行一个trigger函数:

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
   // 获取target对相应的所有依赖,一个map对象 
   const depsMap = targetMap.get(target)
   // 如果没有,说明没有依赖,直接return
   if (!depsMap) {
      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)) {
      depsMap.forEach((dep, key) => {
         if (key === 'length' || key >= (newValue as number)) {
            deps.push(dep)
         }
      })
   } else {
      // schedule runs for SET | ADD | DELETE
      if (key !== void 0) {
         deps.push(depsMap.get(key))
      }

      // 获取一些迭代的依赖,如map.keys、map.values、map.entries等
      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))
      }
   }
}

trigger可接收六个参数:

trigger中首先要获取target对应的所有依赖depsMap,如果没有的直接return

const depsMap = targetMap.get(target)
if (!depsMap) {
 return
}

接下来需要根据keytype获取触发的依赖(使用deps存放需要触发的依赖),这里分为如下几个分支:

deps = [...depsMap.values()]
depsMap.forEach((dep, key) => {
   if (key === 'length' || key >= (newValue as number)) {
     deps.push(dep)
   }
})

这里简单介绍下ITERATE_KEYMAP_KEY_ITERATE_KEY存储的依赖是由什么操作引起的

ITERATE_KEY中的依赖是由这些操作触发进行收集:获取集合的size、集合的forEach操作、集合的迭代操作(包括keys(非Map)、valuesentriesSymbol.iteratorfor...of))

MAP_KEY_ITERATE_KEY中的依赖的由map.keys()触发进行收集。

对应上面其他情况中的几个分支:

当收集完需要触发的依赖,下一步就是要触发依赖:

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

这里有两个分支:

triggerEffects函数可以接收两个参数:dep一个数组或Set集合,保存着需要触发的依赖、debuggerEventExtraInfo在开发环境下,effect.onTrigger所需的一些信息。

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || effect.allowRecurse) {
      if (__DEV__ && effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        effect.run()
      }
    }
  }
}

triggerEffects中,会遍历dep,如果dep中的effect不是当前活跃的effectactiveEffect)或effect.allowRecursetrue,则会根据是否有effect.scheduler,执行effect.schedulereffect.run。 至此,依赖触发过程结束。

接下来详细看下在this.fn()执行完毕后,多余的依赖是如何根据nw属性移除的(此处值只考虑深度在31层以内的,超出31(包含31)层会直接调用cleanupEffect方法删除,比较简单,此处不进行详细说明):
我们还是以前面的例子来分析:

const obj = reactive({ str: 'objStr', flag: true })

effect(() => {
  const c = obj.flag ? obj.str : 'no found'
  console.log(c)
})

obj.flag = false

obj.str = 'test'
  1. effect执行过程中,创建ReactiveEffect实例,这里以_effect表示,因为未指定lazy,所以会执行_effect.run()
  2. 执行this.fn(),在fn执行过程中会访问到objflagstr属性,从而被objget拦截器进行拦截,在拦截过程中会调用track进行依赖的收集,this.fn()执行完毕后targetMap结构如下
    effect-flow1.png
  3. 然后进入finally,执行finalizeDepMarkers,因为wasTracked(dep)false,所以不会删除依赖,但会执行dep.n &= ~trackOpBit,清除比特位。最终targetMap结构为:
    effect-flow2.png
  4. 当执行obj.flag = false时,会触发flag属性对应的依赖,执行trigger,在trigger中获取flag对应的依赖set1,然后调用triggerEffects,在triggerEffects中,执行_effect.run
  5. 在这次run执行过程中,会将_effect.deps中的依赖集合都标记为已收集状态:
    effect-flow3.png
  6. 然后执行this.fn(),同样执行fn的过程中,被objget拦截器拦截,不过这次只拦截了flag属性。在trackEffects中检测到newTracked(dep) === false(此处dep就是set1),所以执行dep.n |= trackOpBit操作,将set1标记为本轮收集过程中新的依赖,又因为wasTracked(dep) === true,所以shouldTrackfalse,本次不会收集依赖。至此,targetMap结构为:
    effect-flow4.png
  7. this.fn()执行完毕,进入finally,执行finalizeDepMarkers。在finalizeDepMarkers中会遍历effect.deps,根据nw属性移除依赖。
  8. 首先判断set1,因为wasTracked(dep) === truenewTracked(dep) === true,所以执行deps[ptr++] = dep,将set1放在deps索引为0的位置,同时ptr自增1,然后执行dep.w &= ~trackOpBitdep.n &= ~trackOpBit。最终set1.n/w = 0
  9. 接着判断set2,因为wasTracked(dep) === truenewTracked(dep) === false,所以执行dep.delete(effect),将_effectset2中删除,然后执行dep.w &= ~trackOpBitdep.n &= ~trackOpBit。最终set2.n/w = 0set2中无依赖。
  10. 遍历完毕,执行deps.length = ptrptr此时为1)。也就是说把set2deps中移除了。
  11. finally执行完毕后,targetMap结构为:
    effect-flow5.png
    可以看到str对应的依赖已经没有了。
  12. 当执行obj.str = 'test'时,触发trigger函数,但此时在targetMap中已经没有str对应的依赖了,所以在trigger中直接return,结束。
上一篇下一篇

猜你喜欢

热点阅读