Vue3 源码学习 - Ref

164 阅读15分钟

本文是本人阅读和学习 Vue 源码的笔记,如有错误欢迎指正

响应式源码位于 packages\reactivity\src

vue3 中最核心的响应式其实就是 ref。

ref 分为两种:普通 ref 和 shallow ref,可以通过 createRef()​​ 传入的第二个参数来判断是否为 shallowRef。

// in ref.ts
export function ref(value?: unknown) {
  return createRef(value, false)
}

export function shallowRef(value?: unknown) {
  return createRef(value, true)
}

shallowRef 的作用是什么呢?看看官方文档的解释:

和 ref()​​ 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value​​ 的访问是响应式的。

​shallowRef()​​ 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。

createRef

接下来看看 createRef

function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

可以看到,会先对传入的 rawValue 进行判断,如果它是之前创建过的 ref,则直接返回已经存在的 ref 即可。如果不是,则会创建 RefImpl 的实例(透传 rawValue 和 shallow 参数)。

RefImpl

再看看 RefImpl 类的实现

class RefImpl<T> {
  private _value: T // 响应式的value值
  private _rawValue: T // 不具备响应式的value原始值

  public dep?: Dep = undefined // 每个ref实例的依赖WeakMap
  public readonly __v_isRef = true // 标识是否为ref

  constructor(
    value: T,
    public readonly __v_isShallow: boolean,
  ) {
    // 对于shallowRef,直接返回value,否则调用对应的工具函数进行处理
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }

  // 暂时先不看get和set的逻辑
  get value() {
    // ...
  }

  set value(newVal) {
    // ...
  }
}

toRaw 和 toReactive

这里插入看一下构造函数里面的 toRaw 和 toReactive

toRaw 根据一个 Vue 创建的 Proxy 代理返回其原始对象。

​toRaw()​​ 可以返回由 reactive()​​、readonly()​​、shallowReactive()​​ 或者 shallowReadonly()​​ 创建的代理对应的原始对象。

这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。

源码中的示例:

const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true

具体实现:

// in reactive.ts
export function toRaw<T>(observed: T): T {
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  // 判断一下对象上是否有RAW标识的原始值(见下文解释),如果有则递归使用toRaw,没有就直接返回
  return raw ? toRaw(raw) : observed
}

ReactiveFlags 有哪些呢?

export interface Target {
  [ReactiveFlags.SKIP]?: boolean
  [ReactiveFlags.IS_REACTIVE]?: boolean
  [ReactiveFlags.IS_READONLY]?: boolean
  [ReactiveFlags.IS_SHALLOW]?: boolean
  [ReactiveFlags.RAW]?: any  // 被代理的初始对象
}

// 如果一个target有这个值,说明它是被Proxy代理了
export function isProxy(value: any): boolean {
  return value ? !!value[ReactiveFlags.RAW] : false
}

那么在什么时候会给对象加上 ReactiveFlags 的 RAW 呢?实际上并不是真的给对象加上了一个属性,而是在 Proxy 的 get 中劫持了对 ReactiveFlags.RAW 的读取,转而从对应的 Map 中获取原始的 target。

class BaseReactiveHandler implements ProxyHandler<Target> {
  constructor(
    protected readonly _isReadonly = false,
    protected readonly _isShallow = false,
  ) {}

  get(target: Target, key: string | symbol, receiver: object) {
    const isReadonly = this._isReadonly,
      isShallow = this._isShallow
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return isShallow
    } else if (key === ReactiveFlags.RAW) {  // 如果读取了ReactiveFlags.RAW
      if (
        // 根据是否readonly/shallow返回四个Map中对应的一个
        receiver ===
          (isReadonly
            ? isShallow
              ? shallowReadonlyMap
              : readonlyMap
            : isShallow
              ? shallowReactiveMap
              : reactiveMap
          ).get(target) || // 从Map中看看之前是否存入了这个target,有就说明被Proxy代理了
        // receiver is not the reactive proxy, but has the same prototype
        // this means the reciever is a user proxy of the reactive proxy
        Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
      ) {
        return target // 直接返回target本身(被Proxy劫持的对象,并不是Proxy对象)
      }
      // early return undefined
      return
    }
// ....

toReactive

如果传入的是一个对象,则使用 reactive 创建一个响应式对象,否则直接返回原始值。

export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

// shared/src/general.ts
export const isObject = (val: unknown): val is Record<any, any> =>
  val !== null && typeof val === 'object'

这里先认为是非对象情形,关于 reactive 的实现原理在下一部分描述,此处先不赘述

对 value 的 get 劫持

接下来回过头来看看 RefImpl 中的对 value 的 get 和 set 劫持

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor() {
    // ...
  }

  get value() {
    trackRefValue(this) // 在使用到当前value的地方进行追踪
    return this._value
  }

  set value(newVal) {
    // ...
  }
}

trackRefValue 追踪 Ref 的使用

接下来看看 trackRefValue 是如何追踪的

// in ref.ts
import {
  activeEffect,
  shouldTrack,
  trackEffect,
  triggerEffects,
} from './effect'

export function trackRefValue(ref: RefBase<any>) {
  // 1. 这里的shouldTrack实际上标识了当前是否要追踪,
  //   可以通过effect中暴露的一些方法来设置shouldTrack(由一个trackStack: boolean[]维护历史记录)
  // 2. activeEffect 表示上一个成功加入deps的effect
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref) // 转化成一个非响应式值
    trackEffect(
      activeEffect,  // 上一个effect
      (ref.dep ??= createDep( // 如果ref没有dep就创建,有就直接返回dep
        () => (ref.dep = undefined), // 传给dep的cleanup函数
        ref instanceof ComputedRefImpl ? ref : undefined, // 传给dep的conputed属性
      )),
      __DEV__ // 这里是宏定义,可以判断环境; 如果是DEV环境就传一个对象作为debuggerEventExtraInfo
        ? {
            target: ref,
            type: TrackOpTypes.GET,
            key: 'value',
          }
        : void 0,
    )
  }
}

// in dep.ts
export const createDep = (
  cleanup: () => void,
  computed?: ComputedRefImpl<any>,
): Dep => {
  const dep = new Map() as Dep
  dep.cleanup = cleanup
  dep.computed = computed
  return dep
}

在早期的版本,dep 的数据结构时 Set,然后后来为了更高的性能,改成了 Map,见这个 PR

从 Set​​ 变成了 Map<Effect, EffectTrackId>​​,但是实际上原理是类似的,只是优化了一些问题(比如通过 EffectTrackId 可以知道在清除 effect 的 dep 时,哪些应该清理,哪些不应该清理,见后文介绍)

总之,都是记录了 dep -> effect 的关联,方便 trigger 触发时,执行 dep 中的所有 effect。

看看 trackEffect 的逻辑

// in effect.ts
export function trackEffect(
  effect: ReactiveEffect,
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
  // 只有当前effect._trackId发生改变时才进行追踪
  if (dep.get(effect) !== effect._trackId) {
    dep.set(effect, effect._trackId) // effect加入到dep中
    const oldDep = effect.deps[effect._depsLength]
    if (oldDep !== dep) {
      if (oldDep) {
        cleanupDepEffect(oldDep, effect)
      }
      effect.deps[effect._depsLength++] = dep // 记录effect被加入到的dep
    } else {
      effect._depsLength++
    }
    if (__DEV__) {
      effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
    }
  }
}

cleanupEffect 清除副作用

cleanupDepEffect 的作用是什么?(以下内容参考霍春阳的 Vue.js 设计与实现)

在同一个 effect 中,可能读取了多个 ref 的值,此时这个 effect 就会被多个 ref 的 dep 收集。但是,读取 ref 的情况可能存在分支判断(也就是每次不一定要触发所有 dep)。

effect(() => {
  // ref2.value 是否被读取,取决于 ref1.value 的值
  document.body.innerText = ref1.value ? ref2.value : "not";
})

解决方法是:每次副作用函数执行时,就把它从所有与之关联的依赖集合 dep 中删除。当副作用函数执行完毕后,当前状态下是否需要读取 ref 将决定 effect 是否会被加入到对应的 dep 中。因此可以保证当前所有的 ref 各自的 dep 存的 effect 一定是当前状态需要被 trigger 触发的。

那么如何知道当前的 effect 被哪些 dep 引用了呢?在 effect 中存一个 deps 数组记录即可(每次 effect 被 track 就同步记录)。也就是 dep <- effect,再加上之前的 dep -> effect,实际上两者都关联起来了,形成了 dep <-> effect。

let activeEffect
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn); // 先把当前effect的dep都清除
    activeEffect = effectFn
    fn() // 一旦执行了fn,读取ref触发track时就会重新加到其dep中
  }
  effectFn.deps = []
  effectFn()
}

在更新的源码中,清理实际上放在了 effect.run() 中(见下文)

看看 effect.ts 中的 ReactiveEffect 类

// in effect.ts
export let activeEffect: ReactiveEffect | undefined

export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []  // 记录当前effect曾被加入到哪些dep中
  computed?: ComputedRefImpl<T>
  allowRecurse?: boolean
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void
  _dirtyLevel = DirtyLevels.Dirty
  _trackId = 0 // 标识不同轮次触发的effect(实际上就是触发次数)
  _runnings = 0
  _shouldSchedule = false
  _depsLength = 0 // 某一轮触发effect时新增的dep关联数量

  constructor(
    public fn: () => T,
    public trigger: () => void,
    public scheduler?: EffectScheduler, // 调度函数,在triggerEffects时,如果有scheduler就执行scheduler,否则执行下面的run()
    scope?: EffectScope,
  ) {
    recordEffectScope(this, scope) // 如果传入了作用域scope,则记录
  }

  public get dirty() { /* ... */ }

  public set dirty(v) { /* ... */ }

  run() {
    this._dirtyLevel = DirtyLevels.NotDirty
    if (!this.active) {
      return this.fn()
    }
    let lastShouldTrack = shouldTrack
    let lastEffect = activeEffect
    try {
      // 当前需要追踪,activeEffect变成当前的effect实例,这样trackRefValue才能执行
      shouldTrack = true
      activeEffect = this

      this._runnings++ 
      preCleanupEffect(this) // 被追踪的id增加了,此时就是新的effect
      return this.fn()
    } finally { // 虽然上面return了,但是finally还是会执行的(也就是等待finally执行完毕后才return)
      postCleanupEffect(this) // 每次执行完毕后,清除掉旧的effect
      this._runnings--
      activeEffect = lastEffect
      shouldTrack = lastShouldTrack
    }
  }

  stop() { /* ... */ }
}

effect cleanup 的三个函数:

function preCleanupEffect(effect: ReactiveEffect) {
  effect._trackId++ // 更新了_trackId,在 effect 函数中,
  effect._depsLength = 0 // 置零
}

function postCleanupEffect(effect: ReactiveEffect) {
  // 把超出length的effect清理
  if (effect.deps.length > effect._depsLength) {
    for (let i = effect._depsLength; i < effect.deps.length; i++) {
      cleanupDepEffect(effect.deps[i], effect)
    }
    effect.deps.length = effect._depsLength
  }
}

function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) {
  const trackId = dep.get(effect)
  if (trackId !== undefined && effect._trackId !== trackId) {
    dep.delete(effect)
    if (dep.size === 0) {
      dep.cleanup()
    }
  }
}

effect._depsLength 和 effect.deps.length 是两个概念:

  • effect._depsLength:本轮副作用,effect 新加入到的 dep 的数量
  • effect.deps.length:当前 deps 实际存放数量(包含旧的关联)

小结

get 总体的执行步骤其实是这样的(只聚焦 get 相关的):

触发副作用,effect 函数执行,创建 effect 实例,调用了 effect.run()

  1. 执行 preCleanupEffect:当前 effect 的 trackId 更新(加了 1),_depsLength 置零

  2. 执行 effect.fn(),副作用中读取到了 ref 的 value,触发 trackEffect:

    1. 由于 trackId 被更新了,所以当前的 effect 一定是新的 effect,加入到 dep 中

    2. ​const oldDep = effect.deps[effect._depsLength]​​​

      • 如果 oldDep !== dep:

        1. 如果确实有 oldDep:执行 cleanupDepEffect,解除 oldDep <-> effect​​​ 的关联
        1. 把当前 dep 加入到 effect.deps 里,_depsLength++
      • 如果 oldDep === dep:effect 之前就加入到当前的 dep 了,deps 不需要改变,也不需要重新加,直接 _depsLength++ 就行了。(通过这样节省性能)

  3. 执行 postCleanupEffect:把超过 _depsLength 部分的 dep <-> effect​​​ 关联解除(这些关联全都不是本次副作用添加的),即多次调用 cleanupDepEffect,更新 effect.deps.length

对 value 的 set 劫持

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor() {
    // ...
  }

  get value() {
    // ...
  }

  set value(newVal) {
    // 判断一下是否需要用toRaw把newVal转化成原始值(也就是target,而非Proxy对象)
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal) // 这一步确保newVal一定是一个原始值,而不是响应式对象
    if (hasChanged(newVal, this._rawValue)) { // 只有发生了改变才设置新值,避免重复设置带来的性能损耗
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal) // 如果不使用原始值,则需要转回reactive
      triggerRefValue(this, DirtyLevels.Dirty, newVal)
    }
  }
}

triggerRefValue 重新触发副作用

接下来看看 trigger 是如何实现的

export function triggerRefValue(
  ref: RefBase<any>,
  dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
  newVal?: any,
) {
  ref = toRaw(ref)
  const dep = ref.dep
  if (dep) {
    triggerEffects( // 传入当前的 dep 和 dirtyLevel
      dep,
      dirtyLevel, // 新版本源码传入的值
      __DEV__
        ? {
            target: ref,
            type: TriggerOpTypes.SET,
            key: 'value',
            newValue: newVal,
          }
        : void 0,
    )
  }
}

在早期的版本中,triggerEffects 实际上就只是执行当前 ref 的 dep 上的 所有 effect (scheduler 或者 run):

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  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() // 这个地方会去执行
      }
    }
  }
}

在新版本源码中,更加复杂:(先不看遍历)

export function triggerEffects(
  dep: Dep,
  dirtyLevel: DirtyLevels, // 新增 dirtyLevel
  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
  pauseScheduling() // 暂停调度

  // 遍历当前 ref 的 dep 中的 每一个 effect
  for (const effect of dep.keys()) {
    // ...
  }

  resetScheduling() // 重启调度
}

新增加了 dirtyLevel,这个值有什么用呢?

先看看 DirtyLevels 有什么枚举:

export enum DirtyLevels { // 给"脏"的程度分了等级
  NotDirty = 0,
  QueryingDirty = 1,
  MaybeDirty_ComputedSideEffect = 2,
  MaybeDirty = 3,
  Dirty = 4,
}

实际上在 triggerEffects 的参数是传入了枚举 DirtyLevels.Dirty​​ 也就是最“脏”的 4.

scheduler 是怎么回事呢?

实际上是在创建 effect 实例的时候,可以

  1. 直接构造函数传入 scheduler
  2. 在选项 options 中传入 shceduler,并且会将选项上的参数扩展透传给实例
export interface ReactiveEffectOptions extends DebuggerOptions {
  lazy?: boolean
  scheduler?: EffectScheduler
  scope?: EffectScope
  allowRecurse?: boolean
  onStop?: () => void
}

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

  // 第一个参数是fn,第二个参数是trigger,第三个参数是scheduler
  // NOOP就是 `() => {}`,表示什么也不做
  const _effect = new ReactiveEffect(fn, NOOP, () => {  // 这里传入了 scheduler
    // shceduler:当effect是脏的时候,执行一次run
    if (_effect.dirty) {
      _effect.run()
    }
  })

  // 把options的属性拓展透传给effect实例
  if (options) {
    extend(_effect, options) // 这里的extend实际上就是Object.assign
    if (options.scope) recordEffectScope(_effect, options.scope)
  }

  // ...
}

再看看调度栈的相关逻辑:

export let pauseScheduleStack = 0 // 一个flag,表示当前是否停止调度
const queueEffectSchedulers: EffectScheduler[] = []
export function pauseScheduling() {
  pauseScheduleStack++
}

export function resetScheduling() {
  pauseScheduleStack--
  // 重启调用栈,执行调用栈中存在的 scheduler
  while (!pauseScheduleStack && queueEffectSchedulers.length) {
    queueEffectSchedulers.shift()!()
  }
}

那么什么时候会给调度栈添加 effect 呢?其实就在 triggerEffects 的遍历中。

回过头来看看遍历

export function triggerEffects(
  dep: Dep,
  dirtyLevel: DirtyLevels,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
  pauseScheduling() // 暂停调度

  // 遍历当前 ref 的 dep 中的 每一个 effect
  for (const effect of dep.keys()) {
    // dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
    let tracking: boolean | undefined // 由于map的读取比较耗费性能,把能否读取结果缓存一下


    if (
      effect._dirtyLevel < dirtyLevel &&  // 当前更"脏"
      (tracking ??= dep.get(effect) === effect._trackId) // dep有最新的effect
    ) {
      effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
      // 上面的代码相当于 
      // effect._shouldSchedule = effect._shouldSchedule || (effect._shouldSchedule = effect._dirtyLevel === DirtyLevels.NotDirty)
      // 也就是只有 1.本来就需要调度 或者 2.本来不需要调度,且上一个状态不脏 的时候才应该执行调度scheduler
      effect._dirtyLevel = dirtyLevel // 更新effect实例上的脏等级
    }


    if (
      effect._shouldSchedule && // 应该调度
      (tracking ??= dep.get(effect) === effect._trackId) // dep有最新的effect
    ) {
      if (__DEV__) {
        // ...
      }
      effect.trigger() // 触发 trigger
  
      if (
        (!effect._runnings || effect.allowRecurse) &&
        effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
      ) {
        effect._shouldSchedule = false // 设置为false,因为后面scheduler会加到调度栈中
        if (effect.scheduler) {
          queueEffectSchedulers.push(effect.scheduler) // 给调度栈添加scheduler
        }
      }
    }
  }

  resetScheduling() // 重启调度,执行调度栈中的所有scheduler
}

在清空 schedulerStack 的时候,就执行了 effect 的 scheduler,也就是检查 effect 是否为脏,是就执行 run

// 之前在effect实例化时,传入的scheduler
() => {
  if (_effect.dirty) {
    _effect.run()
  }
}

执行 run 的步骤实际上在上一个小结有提到,但是这里需要补充 set 会涉及的操作的注释:

run() {
    this._dirtyLevel = DirtyLevels.NotDirty // 把脏等级设置为不脏,因为后面会执行fn
    if (!this.active) {
      return this.fn()
    }
    let lastShouldTrack = shouldTrack // 记录上一个shouldTrack,相当于栈暂存,后续会恢复
    let lastEffect = activeEffect // 记录上一个activeEffect,相当于栈暂存,后续会恢复
    try {
      // 当前需要追踪,activeEffect变成当前的effect实例,这样trackRefValue才能执行
      shouldTrack = true 
      activeEffect = this

      this._runnings++ // 表示当前正在执行
      preCleanupEffect(this)
      return this.fn()
    } finally {
      postCleanupEffect(this)
      this._runnings-- // 当前已经执行完毕
      activeEffect = lastEffect // 恢复成上一个activeEffect
      shouldTrack = lastShouldTrack // 恢复成上一个shouldTrack
    }
}

小结

set 触发的逻辑顺序总结一下就是:

设置值的时候,triggerRefValue 会将 effect 变成脏,并执行 effect.scheduler,而 scheduler 会检查 effect 是否脏,如果脏就执行 effect.run():

  1. 把 dirty_level 设置为不脏,这样后续如果还触发了同一个 trigger,再执行 run 则会直接退出(scheduler 中的逻辑)

  2. 保存上次的 shouldTrack 和 activeEffect,并且设置当前的 shouldTrack 和 activeEffect

  3. 执行 preCleanupEffect:当前 effect 的 trackId 更新(加了 1),_depsLength 置零

  4. 执行 effect.fn(),副作用中如果读取到了 ref 的 value,则触发 trackEffect:

    1. 由于 trackId 被更新了,所以当前的 effect 一定是新的 effect,加入到 dep 中

    2. ​const oldDep = effect.deps[effect._depsLength]​​

      • 如果 oldDep !== dep:

        1. 如果确实有 oldDep:执行 cleanupDepEffect,解除 oldDep <-> effect​​ 的关联
        1. 把当前 dep 加入到 effect.deps 里,_depsLength++
      • 如果 oldDep === dep:effect 之前就加入到当前的 dep 了,deps 不需要改变,也不需要重新加,直接 _depsLength++ 就行了。(通过这样节省性能)

  5. 执行 postCleanupEffect:把超过 _depsLength 部分的 dep <-> effect​​ 关联解除(这些关联全都不是本次副作用添加的),即多次调用 cleanupDepEffect,更新 effect.deps.length

  6. 恢复上次的 shouldTrack 和 activeEffect

总结

effect.run 的执行

  • get 的触发实际上总是因为声明一个副作用,导致 effect 函数的执行,然后在函数中会创建 effect 实例,并执行 effect.run()(只要 effect 的 options 不设置为 lazy)

    • 并且在执行 effect.run() 副作用的过程中可能读取一些 ref 的值,随后就会执行对应的 ref 的 track
  • set 实际上是因为设置了某个 ref 的值,从而触发了 ref 的 trigger,trigger 会将 effect 变成脏,并执行 effect.scheduler,而 scheduler 会检查 effect 是否脏,如果脏就执行 effect.run()。

get 和 set 的总体步骤

get 总体的执行步骤其实是这样的(只聚焦 get 相关的):

触发副作用,effect 函数执行,创建 effect 实例,执行 effect.run()

  1. 执行 preCleanupEffect:当前 effect 的 trackId 更新(加了 1),_depsLength 置零

  2. 执行 effect.fn(),副作用中读取到了 ref 的 value,触发 trackEffect:

    1. 由于 trackId 被更新了,所以当前的 effect 一定是新的 effect,加入到 dep 中

    2. ​const oldDep = effect.deps[effect._depsLength]​​

      • 如果 oldDep !== dep:

        1. 如果确实有 oldDep:执行 cleanupDepEffect,解除 oldDep <-> effect​​ 的关联
        1. 把当前 dep 加入到 effect.deps 里,_depsLength++
      • 如果 oldDep === dep:effect 之前就加入到当前的 dep 了,deps 不需要改变,也不需要重新加,直接 _depsLength++ 就行了。(通过这样节省性能)

  3. 执行 postCleanupEffect:把超过 _depsLength 部分的 dep <-> effect​​ 关联解除(这些关联全都不是本次副作用添加的),即多次调用 cleanupDepEffect,更新 effect.deps.length

set 触发的逻辑顺序总结一下就是:

设置值的时候,triggerRefValue 会将 effect 变成脏,并执行 effect.scheduler,而 scheduler 会检查 effect 是否脏,如果脏就执行 effect.run():

  1. 把 dirty_level 设置为不脏,这样后续如果还触发了同一个 trigger,再执行 run 则会直接退出(scheduler 中的逻辑)

  2. 保存上次的 shouldTrack 和 activeEffect,并且设置当前的 shouldTrack 和 activeEffect

  3. 执行 preCleanupEffect:当前 effect 的 trackId 更新(加了 1),_depsLength 置零

  4. 执行 effect.fn(),副作用中如果读取到了 ref 的 value,则触发 trackEffect:

    1. 由于 trackId 被更新了,所以当前的 effect 一定是新的 effect,加入到 dep 中

    2. ​const oldDep = effect.deps[effect._depsLength]​​

      • 如果 oldDep !== dep:

        1. 如果确实有 oldDep:执行 cleanupDepEffect,解除 oldDep <-> effect​​ 的关联
        1. 把当前 dep 加入到 effect.deps 里,_depsLength++
      • 如果 oldDep === dep:effect 之前就加入到当前的 dep 了,deps 不需要改变,也不需要重新加,直接 _depsLength++ 就行了。(通过这样节省性能)

  5. 执行 postCleanupEffect:把超过 _depsLength 部分的 dep <-> effect​​ 关联解除(这些关联全都不是本次副作用添加的),即多次调用 cleanupDepEffect,更新 effect.deps.length

  6. 恢复上次的 shouldTrack 和 activeEffect

简单实现一下

简易版 ref.ts

// ref.ts
import { activeEffect } from "./effect"

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

export function shallowRef(value?: unknown) {
  return createRef(value, true);
}

export function createRef(rawValue: any, shallow: boolean) {
  if (rawValue.__v_isRef) {
    return rawValue;
  }
  return new RefImpl(rawValue, shallow);
}

class RefImpl<T> {
  private _value: T
  private _rawValue: T
  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    // this._rawValue = __v_isShallow ? value : toRaw(value);
    // this._value = __v_isShallow ? value : toReactive(value);
    this._rawValue = this._value = value; // TEMP
  }

  get value() {
    trackRefValue(this);
    return this._value;
  }

  set value(newVal) {
    console.log("触发了 set value", newVal);
    // newVal = this.__v_isShallow ? newVal : toRaw(newVal);
    if (!Object.is(newVal, this._rawValue)) {
      this._rawValue = this._value = newVal;
      triggerRefValue(this, newVal);
    }
  }
}

type Dep = Set<any>;
type RefBase<T> = {
  dep?: Dep;
  value: T;
}

function trackRefValue(ref: RefBase<any>) {
  if (!ref.dep) {
    ref.dep = new Set();
  }
  trackEffects(ref.dep);
}

function trackEffects(dep: Dep) {
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    console.log("dep收集了effect", activeEffect, dep);
    // activeEffect.deps.push(dep);
  }
}

function triggerRefValue(ref: RefBase<any>, newVal: any) {
  // ref = toRaw(ref);
  if (ref.dep) {
    triggerEffects(ref.dep)
  }
}

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

简易版 effect.ts

export let activeEffect: any;
export const effect = (fn: Function) => {
  const _effect = function () {
    activeEffect = _effect;
    fn();
  }
  _effect();
}

引入模块

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <div>
      <span id="ref-value">
      </span>
      <span id="ref-value-2"></span>
      <button id="ref">点击增加数值</button>
    </div>
  </div>
  <script type="module">
    import { ref } from "./ref.js";
    import { effect } from "./effect.js";
    const myRef = ref(666);
    effect(() => {
      document.querySelector("#ref-value").innerText = "myRef.value = " + myRef.value;
      document.querySelector("#ref-value-2").innerText = "another:" + (myRef.value % 100 + 10000);
    })

    const refBtn = document.getElementById("ref");
    refBtn.addEventListener("click", () => {
      myRef.value++;
    })
  </script>
</body>
</html>

测试一下

简易版ref.gif