Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【看的见的思考】reactivity #23

Open
cuixiaorui opened this issue Aug 5, 2021 · 0 comments
Open

【看的见的思考】reactivity #23

cuixiaorui opened this issue Aug 5, 2021 · 0 comments

Comments

@cuixiaorui
Copy link
Owner

cuixiaorui commented Aug 5, 2021

看的见的思考

reactive

  test('Object', () => {
    const original = { foo: 1 }
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    expect(isReactive(observed)).toBe(true)
    expect(isReactive(original)).toBe(false)
    // get
    expect(observed.foo).toBe(1)
    // has
    expect('foo' in observed).toBe(true)
    // ownKeys
    expect(Object.keys(observed)).toEqual(['foo'])
  })
 

从这个单元测试的 demo 开始

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

先从 createReactiveObject 开始

这里别的什么都不需要管,先看看他做了什么事

不需要关心 mutableHandlers、mutableCollectionHandlers、reactiveMap 都做了什么

在 createReactiveObject 里面最核心的逻辑是

function createReactiveObject(target){

  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  return proxy
 } 

最核心的逻辑其实就是用 Proxy 给包裹一下,然后这里是需要基于一个 targetType 的类型去选择用不同的 handlers 的

那我们看看 targetType 都有哪几种?

看 getTargetType

  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))

无效的和正常了 type 做了区分

无效的情况是:

有 ReactiveFlags.SKIP 字段 或者 对象是不可以扩展的

那在什么情况下需要对象用到扩展呢?

TODO

剩下的点都在 targetTypeMap 里面声明好了

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

所以总结一下就是:

  • TargetType.COMMON:

    • Array

    • Object

  • TargetType.COLLECTION

    • Map

    • Set

    • WeakMap

    • WeakSet

  • TargetType.INVALID

    • 上面都不符合的话

好 ,类型知道了,那我们的参数是一个对象 {foo:xxx} 所以看看 handlers 应该是 baseHandlers

那么 handlers 就是

TargetType.COMMON → baseHandlers → mutableHandlers

TargetType.COLLECTION → collectionHandlers → mutableCollectionHandlers

mutableHandlers

先看这个方法

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

这里把所有的处理都封装到了具体的函数内了,这次在看要比之前好很多

接着看看 get

get

get 就是调用了 createGetter

function createGetter(isReadonly = false, shallow = false) {
  return function get (target: Target, key: string | symbol, receiver: object)=>{
   const res = Reflect.get(target, key, receiver)
     if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
     if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }
  }



}

这里是使用了闭包的概念,让调用者可以少写几个参数,而且从概念上也做了分离,在create 的时候就可以标注是不是 readonly 的 或者是 shallow 的

这里的 get 调用的时机是在触发 proxy.xxx 的 get 操作的时候

而在调用 get 的时候是需要做依赖收集的,而依赖收集的动作就是在 track 里面做的!

而最终的结果就是返回 Reflect.get 的值就可以了

这里有个点,就是如何处理嵌套的 Object, 就是递归的调用 reactive 即可

track 依赖收集

重点:那是如何做依赖收集的呢???

export function track(target: object, type: TrackOpTypes, key: unknown) {
 
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

这里的重点就是如何存储 dep

这里有两层关系,

target → depsMap

key → dep

例如:

const user = {age: 1}

那 target 就是 user

target 对应的就是 depsMap

age 对应一个 dep

那 activeEffect 是什么呢?其实就是依赖(通过 effect 给到的 function)

effect(()=>{
// 这个 函数就是 activeEffect
})

而把 activeEffect 添加到dep里面的操作就是依赖收集

到这里依赖收集的动作就已经都搞定了

那注意这里的 dep 只是一个 set 数据结构

需要看看 activeEffect 是从哪里过来的

这里的 activeEffect 是在 createReactiveEffect 赋值的

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
        activeEffect = effect
}

所以这个 activeEffect 确实是调用 effect 时候的依赖函数

在来分析分析这几个参数

export function track(target: object, type: TrackOpTypes, key: unknown) {

target 就是对应的对象

type 的作用是后续给 debug 的时候调用的,方便用户知道当前是什么类型

    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }

key 是什么?

key 按照以前的理解是 target 对应的 key ,但是下面的 trigger 的时候发现这个key 可能并不只有一种情况,那么我们看看key 都有什么情况把

啊哦,其实大多数情况下都是有key 的,但是有一些特殊的操作是没有key 的,那怎么办? 自己搞一个被,所以这个就是 ITERATE_KEY 的作用

比如在 ownkeys 的时候,在 size 的时候

function ownKeys(target: object): (string | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

function size(target: IterableCollections, isReadonly = false) {
  target = (target as any)[ReactiveFlags.RAW]
  !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.get(target, 'size', target)
}

Set

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

这里的核心逻辑是 trigger 也就是触发依赖

但是触发依赖的时候是分情况的,一个是 ADD 一个是 SET

TODO 如何区分触发依赖的时候是 ADD 还是 SET?

先看trigger 的逻辑实现

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
    const effects = new Set<ReactiveEffect>()
    ……



}

先看 Add 的逻辑

    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          add(depsMap.get('length'))
        }

这里是基于不同的类型来从获取 depsMap 里面获取值

那看看 add 干了啥?

  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

啊哦, 这里是用 effects 来把 effect 给添加进来的,

那为什么要在这里添加呢? 不是应该在收集依赖的时候添加吗? 看看收集依赖做了啥?

track 的时候确实是收集了 effect 里面的依赖函数,没有问题

那看看 effects 后续是用在哪里了吧

// trigger 
 effects.forEach(run)

在最后一行调用了 effects 执行 run 函数,那这里应该是在 trigger 的时候把之前所有的依赖又重新收集了一遍,那为什么又重新收集了一遍呢?方便后续的统一处理,因为需要在多个地方把所有的 effect 都收集起来

add(depsMap.get(ITERATE_KEY))

这里的 ITERATE_KEY 是什么? 他是在哪里赋值的?是在什么时候存给 depsMap 的呢?

export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')

只是一个唯一标识

那看看在哪里存的 ,存的又是什么, 啊哦

在调用 track 的时候会吧这个值给过去,比如:在 ownKeys 的时候 ,那这里需要看看 track 的几个关键的参数

那么如果我们精简一下 set 做的事的话,那么就是:

  1. 收集所有的 effect (依赖)

  2. 使用的是 add 函数来添加

  3. 调用所有的 effect

  4. 调用的 run 函数来处理

那么继续去看看 run 函数都做了什么吧

  const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

也是非常简单,就是调用一下收集过来的 effect (依赖)

而这里有个特殊处理就是 scheduler 的逻辑实现,这里其实就是调用 scheduler ,让用户自己处理调用的时机(这里先略过)

effect

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  const effect = createReactiveEffect(fn, options)
  return effect
}

fn就是传入的 function ,这里的核心就是创建一个 effect 对象,然后返回即可

接下来看 createReactiveEffect

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

effect 本身就是一个函数,然后给了它很多的属性

没有用对象也没有用 class , 只是用了 function 来表示的 effect 。这是因为 effect 本身是需要执行的

这里是使用 effectStack 来存储所有的 effect ,以及使用 activeEffect 这个全局的变量来保存当前的 effect 这里的 activeEffect 就和 track 依赖收集关联起来了, 因为 effect 这个函数就是响应式对象的依赖

这里需要探索的是

enableTracking

resetTracking

cleanup

先看 enableTracking

export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}

咦,这里的 trackStack 又是一个栈,他是做什么的呢?

看看他都是在哪里调用了,trackStack 只是会影响 shouldTrack ,那重点是 shoudlTrack

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }

最终的使用是会影响到 track 的逻辑

那为什么需要用 stack 来管理一个布尔变量呢?

这里的关键点是 pauseTracking 函数的调用,但是使用它的地方太多了

结合pauseTracking enableTracking resetTracking 来判断的话,应该是需要返回上一个 shouldTrack 的状态

那我们只分析 try 内部的代码的话

 try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
       ……
      }

就是先允许 track 然后把 effect 记录到 effectStack 内,接着执行 fn()

而执行 fn 的时候会触发 track 的逻辑,所以正好把当前的 effect 给收集进去了

那我们在看看 finally 的逻辑

try{
……
}finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }

这里的 finally 逻辑是肯定会执行的

所以也就是在收集完依赖后,就需要把之前的 effect 给弹出去了,然后还 resetTracking,但是这里的 resetTracking 是回到上一个状态。 最后把 activeEffect 制成栈顶的值

但是这里是应对什么场景的呢???

可能是嵌套的。TODO

接着看看 cleanup 的逻辑,因为在跟断点的时候发现每次收集到的 dep 都是会被清理的。也就是执行一次,清理一次。

这个逻辑之前大概听尤大讲过,是为了处理一些边缘case ,必须需要每次都清空,我需要找到这个 边缘case 的 demo

function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

这里的 deps 是 effect 依赖对应的响应式对象的 dep,之前在 track 的时候 收集进来的。

这里的目的就是把响应式对象里面的依赖都清空。至于为什么,只能是找到对应的 demo 才明白。(应该是为了处理 边缘 case)

看单元测试:

  it('should observe multiple properties', () => {
    let dummy
    const counter = reactive({ num1: 0, num2: 0 })
    effect(() => (dummy = counter.num1 + counter.num1 + counter.num2))

    expect(dummy).toBe(0)
    counter.num1 = counter.num2 = 7
    expect(dummy).toBe(21)
  })

这里本来以为赋值2个key 的话,只会触发一次,但是其实是修改一个key就会触发一次 fn的

it('should observe nested properties', () => {
    let dummy
    const counter = reactive({ nested: { num: 0 } })
    effect(() => (dummy = counter.nested.num))

    expect(dummy).toBe(0)
    counter.nested.num = 8
    expect(dummy).toBe(8)
  })

如果是个嵌套的对象的话,这里的 counter.nested 的依赖会是 fn,而 counter.nested.num 的依赖也会是 fn

那这个调用的链路都会收集 fn

 it('should not be triggered by mutating a property, which is used in an inactive branch', () => {
    let dummy
    const obj = reactive({ prop: 'value', run: true })

    const conditionalSpy = jest.fn(() => {
      dummy = obj.run ? obj.prop : 'other'
    })
    effect(conditionalSpy)

    expect(dummy).toBe('value')
    expect(conditionalSpy).toHaveBeenCalledTimes(1)
    obj.run = false
    expect(dummy).toBe('other')
    expect(conditionalSpy).toHaveBeenCalledTimes(2)
    obj.prop = 'value2'
    expect(dummy).toBe('other')
    expect(conditionalSpy).toHaveBeenCalledTimes(2)
  })

看到这个测试的时候,明白了为什么每次都需要 cleanup 了,这里的重点是在 conditionalSpy 内,这里 track 的前提是需要触发 get 之类的操作,而如果说在 effect 的 fn 里面因为有条件逻辑存在的话,就是会有触发不到某个 get 的时候。

比如 conditionalSpy ,在 obj.run 为 true 的时候,这里会触发 obj.run 也会触发 obj.prop。

而 obj.run 变成 false 的时候,这里只会触发 obj.run 。 而只触发 obj.urn 就会意味着 obj.prop 没有收集进来,那当我们去修改了 obj.prop 的值的时候,就不会在执行 effect fn 了。

那如果说我们把代码的执行 path 比作绘画的话,因为这个 path 会改变,所以我们就需要每次都 清空掉,然后重新绘制

哈哈,这里是用游戏逻辑的 reset 来做的比喻

阅读 3.2 版本

变化点在 effect.ts 里面

首先 effect 是用 class 来表示了

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

  // can be attached after creation
  computed?: boolean
  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 | null
  ) {
    recordEffectScope(this, scope)
  }

  run() {
  }

  stop() {
  }
}

只有2个行为,run 和 stop 了

在看 effect 的逻辑

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  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
}

这里的 scope 是做什么用的

执行的时候就是用 _effect.run() 调用即可

这时候的 effect 函数会返回一个新的概念 ReactiveEffectRunner 类型。

那看看这个类型都是做了什么事把

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

这里的 runner 也就是 ReactiveEffectRunner 就是 _effect.run.bind 之后的这个函数

接着看看 effect 里面的 run 都是做了什么

  run() {
    if (!this.active) {
      return this.fn()
    }
    if (!effectStack.includes(this)) {
      try {
        effectStack.push((activeEffect = this))
        enableTracking()

        trackOpBit = 1 << ++effectTrackDepth

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

        trackOpBit = 1 << --effectTrackDepth

        resetTracking()
        effectStack.pop()
        const n = effectStack.length
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }

这里和之前的版本的变化点是控制 track 的布尔值的逻辑变了。别的都是和以前一样的

那重点就看看新增加的逻辑是什么

 try {
        effectStack.push((activeEffect = this))
        enableTracking()

        trackOpBit = 1 << ++effectTrackDepth

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

这里的 trackOpBit 和 initDepMarkers 和 cleanupEffect 方法

对比之前的实现的话,每次都是必须会调用 cleanupEffect 来清理依赖的。而现在不是了

先看看 effectTrackDepth

// The number of effects currently being tracked recursively.
let effectTrackDepth = 0

对effectTrackDepth 的赋值是在 try 里面的时候 effectTrackDepth 会执行 ++effectTrackDepth,

而 effectTrackDepth 会影响到initDepMarkers和cleanupEffect 以及finalizeDepMarkers 的执行

这里还有一个关键的控制变量是maxMarkerBits

感觉是个控制值

/**
 * The bitwise track markers support at most 30 levels op recursion.
 * This value is chosen to enable modern JS engines to use a SMI on all platforms.
 * When recursion depth is greater, fall back to using a full cleanup.
 */
const maxMarkerBits = 30

这里的 SMI ,是什么?TODO

目前猜测是基于v8的某些点做的优化策略

目前是

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

那看看小于 maxMarkerBits 的时候的 initDepMarkers 吧

export const initDepMarkers = ({ deps }: ReactiveEffect) => {
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].w |= trackOpBit // set was tracked
    }
  }
}

这里也是用了二进制的处理方式来做的标记逻辑。

看看这里的 dep.w 都用在了哪里

以上逻辑的关键点是优化了之前每次都调用 cleanup 的点

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 {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

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

逻辑线索在 shouldTrack , 都是为了算出是不是需要 track,那么看看有没有 demo 可以验证这个猜测

  it('should observe multiple properties', () => {
    let dummy
    const counter = reactive({ num1: 0, num2: 0 })
    effect(() => (dummy = counter.num1 + counter.num1 + counter.num2))

    expect(dummy).toBe(0)
    counter.num1 = counter.num2 = 7
    expect(dummy).toBe(21)
  })

在这个单测里面证明了上面的猜测,现在我们知道了解决的是什么问题(why)

接着看看是如何做到的 how

如果从优化角度自己思考的话,是希望

  1. track 收集依赖只收集一次就好了

  2. 但是只收集一次的话,code path 变化了要如何解决?

好了,这里使用二进制的原因是因为会有多层级,而用二进制来表示每一个层级的标识

而对于一个 dep 来讲会有2个标识

  • n → 就是代表在当前的递归层级中是不是初始化过的

    • 我们在简化一下,不考虑递归层级的问题

      • n 就标识为是不是初始化过的
  • w→在当前的递归层级中是不是已经被 track 的

    • 是不是已经被 track 的

接着我们看看 dep.n 和 dep.w 都是分别在什么时候被赋值的

stop 的实现

  it('stop', () => {
    let dummy
    const obj = reactive({ prop: 1 })
    const runner = effect(() => {
      dummy = obj.prop
    })
    obj.prop = 2
    expect(dummy).toBe(2)
    stop(runner)
    obj.prop = 3
    expect(dummy).toBe(2)

    // stopped effect should still be manually callable
    runner()
    expect(dummy).toBe(3)
  })

看看stop 的功能是如何实现的

这里 stop 是停止侦听,而 runner 是重新run起来

看看stop 都做了什么事

export function stop(runner: ReactiveEffectRunner) {
  runner.effect.stop()
}

  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

这里的关键是调用了 cleanupEffect 这个函数,是会把当前的 effect 的 deps 都清空掉

那么也就是说响应式对象里面是没有依赖了,所以当触发 effect 的 trigger 的时候,是没有依赖可以执行的

而再次可以运行的逻辑其实就是重新调用一遍 effect.run

这里的 runner 就是指向的 effect.run 函数

所以后续的操作就是和初始化的逻辑一样了,需要重新的执行 fn ,然后再重新的收集依赖

lazy 的实现

lazy 是为了让用户自己选择调用的时机

  it('lazy', () => {
    const obj = reactive({ foo: 1 })
    let dummy
    const runner = effect(() => (dummy = obj.foo), { lazy: true })
    expect(dummy).toBe(undefined)

    expect(runner()).toBe(1)
    expect(dummy).toBe(1)
    obj.foo = 2
    expect(dummy).toBe(2)
  })

如果 lazy 为true 的话,那么在执行 effect 的时候,是不会主动执行 run 的,(不会执行 run 就意味着不会执行用户给的 fn),然后执行 effect 之后是会返回 runner 的,这里用户可以自己选择在什么时候去执行

实现也比较简单

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

  if (!options || !options.lazy) {
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

EffectScope 是什么

暂时看不出来是在哪里使用的

但是他的职责就是收集所有的 effect

计算属性的实现 computed

先从 test 入手

  it('should return updated value', () => {
    const value = reactive<{ foo?: number }>({})
    const cValue = computed(() => value.foo)
    expect(cValue.value).toBe(undefined)
    value.foo = 1
    expect(cValue.value).toBe(1)
  })

computed 的逻辑

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

// 初始化的逻辑
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  )

  return cRef as any
}

一开始先找到 getter 和 setter

接着看 ComputedRefImpl 的实现

class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  private _value!: T
  private _dirty = true
  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self)
    if (self._dirty) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}/

computed 的核心就是 effect ,而这里是使用了 effect 的 scheduler 的功能

后续执行 effect.run 的时候需要做一些额外的处理

看看 dirty 和 triggerRefValue 是做了什么

因为我们知道 computed 的一个核心点是可以有缓存的,就是没有变化的话,会返回之前的值

主要也是看看 computed 的这个核心功能是如何实现的

先看看 triggerRefValue 的实现

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  if (ref.dep) {
    if (__DEV__) {
      triggerEffects(ref.dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      triggerEffects(ref.dep)
    }
  }
}

这里涉及到了 ref 的代码实现了, 哦 算了 , 计算属性先等等在看,先看 ref ,

这个函数在 ref 的时候都分析完了,可以去看看 ref 的分析

继续

计算属性的话,只有在执行 get 操作的时候才会触发后续的逻辑

构造器里面只是创建了 effect,但是并没有执行

接着就看看 get value 的逻辑吧

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self)
    if (self._dirty) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

这里的重点是搞清楚 _dirty 的逻辑

选择一个单元测试,使用断点

  it('should compute lazily', () => {
    const value = reactive<{ foo?: number }>({})
    const getter = jest.fn(() => value.foo)
    const cValue = computed(getter)

    // lazy
    expect(getter).not.toHaveBeenCalled()

    expect(cValue.value).toBe(undefined)
    expect(getter).toHaveBeenCalledTimes(1)

    // should not compute again
    cValue.value
    expect(getter).toHaveBeenCalledTimes(1)

    // should not compute until needed
    value.foo = 1
    expect(getter).toHaveBeenCalledTimes(1)

首先是调用了 new ComputedRefImpl

然后再初始化的时候创建了 effect ,这里要注意的是

    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this[ReactiveFlags.IS_READONLY] = isReadonly

第二个参数是scheduler 的实现,当执行 effect.run 的时候会执行 fn,当触发了 trigger 逻辑的时候会执行 scheduler 内部的实现

顺序是:

  1. 执行 new ReactiveEffect

  2. 用户执行 get value 的操作

  3. 触发 get

1. _dirty 成false ,锁上了

2. 这里只有在触发了 trigger 的时候才会开锁
  1. 执行 trackRefValue 收集依赖?这里是处理未知情况的逻辑

  2. 触发 effect.run()

  3. 执行用户给的 fn

  4. 触发收集依赖

  5. 用户修改了内部响应式对象的值

  6. 触发 trigger

1. 执行 scheduler 的实现

2. 开锁
  1. 再次调用 get 的时候 dirty 成 ture 所以可以再次执行 effect.run 也就是执行用户给的 fn

ref 的实现

先从这里看起

export function ref(value?: unknown) {
  return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

用户实际使用的就是 RefImpl 的实例

而 refimpl 一共就2个行为, get value 和 set value

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

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

  constructor(value: T, public readonly _shallow = false) {
    this._rawValue = _shallow ? value : toRaw(value)
    this._value = _shallow ? value : convert(value)
  }

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

  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

而基于我们之前看 reactive的经验,知道在 get 的时候是会 track 的,在 set 的时候会 trigger 的

而 ref 这里的实现也是一样的

先看 trackRefValue

export function trackRefValue(ref: RefBase<any>) {
  if (isTracking()) {
    ref = toRaw(ref)
    if (!ref.dep) {
      ref.dep = createDep()
    }
    if (__DEV__) {
      trackEffects(ref.dep, {
        target: ref,
        type: TrackOpTypes.GET,
        key: 'value'
      })
    } else {
      trackEffects(ref.dep)
    }
  }
}

这里的实现就简单了,因为一个 ref 只会对应一个值,所以实现的时候会创建一个 dep 赋值给 ref.dep 上

然后后面的执行和 reactive 一样了。都是执行 trackEffects来处理

这里有个问题,如果说传入的值是 object 或者是 array 的话,那怎么办?

答案就是会用 reactive 包裹一下,这个逻辑是在 构造器里面实现的

  constructor(value: T, public readonly _shallow = false) {
    this._rawValue = _shallow ? value : toRaw(value)
    this._value = _shallow ? value : convert(value)
  }

看 convert 的实现

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val

啊哦,如果是个 object 的话,那么就用 reactive 包裹了

那继续去看 set逻辑

  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      triggerRefValue(this, newVal)
    }
  }

这里处理了2个点

  1. 看看新的值和老的值一样不,如果不一样的话,需要更改之前的值
1. 细节就是会把新的值也 convert 一下
  1. 触发 triggerRefValue
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  if (ref.dep) {
    if (__DEV__) {
      triggerEffects(ref.dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      triggerEffects(ref.dep)
    }
  }
}

这里也和之前是一样的,如果有 dep 的话,那么就 trigger 一下就可以了

DeferredComputed 的实现

先从单元测试看起

  test('should only trigger once on multiple mutations', async () => {
    const src = ref(0)
    const c = deferredComputed(() => src.value)
    const spy = jest.fn()
    effect(() => {
      spy(c.value)
    })
    expect(spy).toHaveBeenCalledTimes(1)
    src.value = 1
    src.value = 2
    src.value = 3
    // not called yet
    expect(spy).toHaveBeenCalledTimes(1)
    await tick
    // should only trigger once
    expect(spy).toHaveBeenCalledTimes(2)
    expect(spy).toHaveBeenCalledWith(c.value)
  })

大概先猜测的话,这里的 deferredComputed 和 computed 大概差不多,一上来就会执行,但是不同的是后续在改变他的值的时候他不会立即执行了。而是等到 await tick 之后才会执行

那看看是如何实现的

程序的入口

export function deferredComputed<T>(getter: () => T): ComputedRef<T> {
  return new DeferredComputedRefImpl(getter) as any
}
class DeferredComputedRefImpl<T> {
  public dep?: Dep = undefined
  private _value!: T
  private _dirty = true
  constructor(getter: ComputedGetter<T>) {
    let compareTarget: any
    let hasCompareTarget = false
    let scheduled = false
    this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
      if (this.dep) {
        if (computedTrigger) {
          compareTarget = this._value
          hasCompareTarget = true
        } else if (!scheduled) {
          const valueToCompare = hasCompareTarget ? compareTarget : this._value
          scheduled = true
          hasCompareTarget = false
          scheduler(() => {
            if (this.effect.active && this._get() !== valueToCompare) {
              triggerRefValue(this)
            }
            scheduled = false
          })
        }
        // chained upstream computeds are notified synchronously to ensure
        // value invalidation in case of sync access; normal effects are
        // deferred to be triggered in scheduler.
        for (const e of this.dep) {
          if (e.computed) {
            e.scheduler!(true /* computedTrigger */)
          }
        }
      }
      this._dirty = true
    })
    this.effect.computed = true
  }

  private _get() {
    if (this._dirty) {
      this._dirty = false
      return (this._value = this.effect.run()!)
    }
    return this._value
  }

  get value() {
    trackRefValue(this)
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    return toRaw(this)._get()
  }
}

这里的几个属性都是和 computed 是一样的,比如有 dep 、 dirty、和 value

看看区别是什么

get 函数的实现和 computed 是差不多的,但是他没有 set 。

  get value() {
    trackRefValue(this)
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    return toRaw(this)._get()
  }
  private _get() {
    if (this._dirty) {
      this._dirty = false
      return (this._value = this.effect.run()!)
    }
    return this._value
  }

同样都是使用 dirty 给锁上

那重点应该就是在 trigger 的实现里面

  constructor(getter: ComputedGetter<T>) {
    let compareTarget: any
    let hasCompareTarget = false
    let scheduled = false
    this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
      if (this.dep) {
        if (computedTrigger) {
          compareTarget = this._value
          hasCompareTarget = true
        } else if (!scheduled) {
          const valueToCompare = hasCompareTarget ? compareTarget : this._value
          scheduled = true
          hasCompareTarget = false
          scheduler(() => {
            if (this.effect.active && this._get() !== valueToCompare) {
              triggerRefValue(this)
            }scheduler
            scheduled = false
          })
        }
        // chained upstream computeds are notified synchronously to ensure
        // value invalidation in case of sync access; normal effects are
        // deferred to be triggered in scheduler.
        for (const e of this.dep) {
          if (e.computed) {
            e.scheduler!(true /* computedTrigger */)
          }
        }
      }
      this._dirty = true
    })
    this.effect.computed = true
  }

这里的 computedTrigger 参数是什么?

这里第二个参数是 scheduler ,我们看看当调用 scheduler 的时候给传入的什么参加就可以了

咦,看了看 effect 里面当调用 scheduler 的时候 并没有什么参数呀?

那执行看看

这个逻辑实在是太绕了。先放弃

这里有个有价值的是,进队列,在异步后执行的实现逻辑

const tick = Promise.resolve()
const queue: any[] = []
let queued = false

const scheduler = (fn: any) => {
  queue.push(fn)
  if (!queued) {
    queued = true
    tick.then(flush)
  }
}

const flush = () => {
  for (let i = 0; i < queue.length; i++) {
    queue[i]()
  }
  queue.length = 0
  queued = false
}

这里有个缺点就是没有看看进来的 fn 是不是已经收集过的。

effectScope 的实现

当 reactivity 这个库单独拿出去使用的时候,就会出现一系列的问题。

比如创建出来的 effect 变多了,如何去统一的 stop 掉。这个 api 就是解决这个问题的

之前在 vue 中使用的话,所有的 effect 都是和组件绑定在一起的。所以当组件销毁的时候,他会自动的把组件内所有的 effect 都 stop 掉。

先看看他的几个功能点

  1. 收集所有的 effect

  2. 包含了 effect

  3. watch

  4. computed

  5. watchEffect

  6. 可以统一的停止所有的 effect

  7. 也就是调用收集起来的 effect.stop

  8. 可以有多个 scope 联合起来使用

  9. 多个 scope 就会涉及到树结构

1. 看看多个 scope 是如何管理的
  1. 当 parent scope 清理的时候所有的 children 都会被清理掉

  2. 也可以设置一下 具体的那个 scope 可以不会被清理

从 api 上来看的话分为以下几个行为:

1. Basic Usage

  1. Nested Scopes

  2. Detached Nested Scopes

4.onScopeDispose

好了,接下来就依次来看看是如何实现的

看看是如何收集所有的 effect 的

  run<T>(fn: () => T): T | undefined {
    if (this.active) {
      try {
        this.on()
        return fn()
      } finally {
        this.off()
      }
    } else if (__DEV__) {
      warn(`cannot run an inactive effect scope.`)
    }
  }

关键函数是 this.on 和 this.off

//this.on
  on() {
    if (this.active) {
      effectScopeStack.push(this)
      activeEffectScope = this
    }
  }

咦,这里只做到了把 effectScope 自己收集起来,那在什么时候收集的 run 里面的 effect 呢?

这个问题的答案是在这里 recordEffectScope

export function recordEffectScope(
  effect: ReactiveEffect,
  scope?: EffectScope | null
) {
  scope = scope || activeEffectScope
  if (scope && scope.active) {
    scope.effects.push(effect)
  }
}

scope 就是 effectScope 了。而里面的逻辑也很简单 就是把 effect 给收集起来,而这个函数是在哪里调用的呢?

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

哈哈,是在 new ReactiveEffect 的时候,所以只要是 effect 创建了,那么就会收集到当前的 scope 里面了,这样就完成了收集 effect 的动作

这里有个发现是,在创建 effect 的时候,你可以指定具体的 scope 。不然的话 就是 activeEffectScope (当前的 scope 来收集 effect 了)

我们继续去看看 this.off 的实现

  off() {
    if (this.active) {
      effectScopeStack.pop()
      activeEffectScope = effectScopeStack[effectScopeStack.length - 1]
    }
  }

可以看到,这里是用 stack 来管理收集递归调用的,这里也和程序执行用 stack 来管理是一样的道理

stop 功能是如何实现的

  stop(fromParent?: boolean) {
    if (this.active) {
      this.effects.forEach(e => e.stop())
      this.cleanups.forEach(cleanup => cleanup())
      if (this.scopes) {
        this.scopes.forEach(e => e.stop(true))
      }
      // nested scope, dereference from parent to avoid memory leaks
      if (this.parent && !fromParent) {
        // optimized O(1) removal
        const last = this.parent.scopes!.pop()
        if (last && last !== this) {
          this.parent.scopes![this.index!] = last
          last.index = this.index!
        }
      }
      this.active = false
    }
  }

如果只忽略其他功能的话,那么核心只是一行代码

 this.effects.forEach(e => e.stop())

把收集起来的所有的 effect 都执行 stop 就完事了

剩下的逻辑是处理其他功能的。我们接着依次去看看

scope 里面会嵌套 scope,当执行 stop 的时候,内部的 scope 里面所有的 effect 也会都 stop,这个是怎么实现的?

如果让我自己来实现的话,那么首先是当前的 scope 应该把内部的所有 scope 都存起来,然后再 stop 的时候在调用内部 scope.stop 逻辑

那么先看看如何存的把:

这里是用 scopes 字段来存储的

  scopes: EffectScope[] | undefined

那看看在哪里 push 的把

  constructor(detached = false) {
    if (!detached && activeEffectScope) {
      this.parent = activeEffectScope
      this.index =
        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
          this
        ) - 1
    }
  }

是在构造器里面执行的 push ,只不过这里判断了 detached 变量,因为在功能的使用上,如果当然的 scope 的 detached 为true 的话,那么表示当前的 scope 是不可以被清理的

又因为 activeEffectScope 是在执行 this.on 的时候才会被赋值,现在还是之前的也就是上一个 scope 所以可以是当前的 scope 的 parent

然后使用 parent 把当前的 scope 给收集起来,这样就完成了收集的处理,

Index 是做什么的, 好像是和后面优化的逻辑相关,暂时不关心

在去看看 stop 时候的处理

 if (this.scopes) {
        this.scopes.forEach(e => e.stop(true))
      }

简单 ,和刚刚上面猜测的一样,调用 scope 的 stop 方法就可以了

Detached Nested Scopes 如果 scope 是独立的话,那么当 parent scope 调用 stop 的时候 不应该被清理

这里的实现也很简单了,想一想,如果是 Detached 的话,那么就不应该被清除,那么只需要不收集到 scopes 里面不就完事了吗。 所以在构造器里面的实现就是判断一下

    if (!detached && activeEffectScope) {
      this.parent = activeEffectScope
      this.index =
        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
          this
        ) - 1
    }

detached 是 true 的话,那么你就别给我收集了

onScopeDispose 是类似于组件的 onUnMounted 的功能的

也就是在 stop 执行完成后,被调用。让用户可以处理一些副作用(比如清空一些事件侦听)

export function onScopeDispose(fn: () => void) {
  if (activeEffectScope) {
    activeEffectScope.cleanups.push(fn)
  } else if (__DEV__) {
    warn(
      `onDispose() is called when there is no active effect scope ` +
        ` to be associated with.`
    )
  }
}

这里要注意的是, 清理函数是可以有多个的,所以这里是用 数组来存 cleanups

onScopeDispose 的逻辑就是收集所有的处理函数

接着是在 stop 完成之后调用即可

  stop(fromParent?: boolean) {
    if (this.active) {
      this.effects.forEach(e => e.stop())
      this.cleanups.forEach(cleanup => cleanup())
      if (this.scopes) {
        this.scopes.forEach(e => e.stop(true))
      }
      this.active = false
    }
  }

可以看到,这里的 cleanups 的执行是在 当前 scope 所有的 effect都执行完 stop 之后调用的

知识点

可以利用 stack 来处理递归的每一个状态

使用 Stack 来处理递归嵌套

可以使用二进制的方式来管理多个状态

这里的限制是这个状态只能是个 boolean

问题

targetType 都有哪几种?✅

如何区分触发依赖的时候是 ADD 还是 SET?✅

为什么清空 cleanup ,清空 effect 里面的 deps ✅

执行的代码变了,所以需要全部重新执行

effectsStack 和 trackStack 是为了处理什么场景的?✅

嵌套的场景

一个 stack 需要对应一个 effect

多层级递归调用的 demo 是哪一个 ✅

v8里面的 SMI 是什么?

总结

track 都做了什么

收集依赖

trigger 都做了什

调用收集到的所有的依赖

function ownKeys(target: object): (string | symbol)[] {

track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)

return Reflect.ownKeys(target)

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant