从Vue3源码测试用例来解构功能的实现—响应式系统

前言

Vue3源码关于响应式的所有测试用例

对响应式还不理解的同学建议去看看我这篇文章响应式原型,下面我会从Vue3源码来分析响应式的核心功能。分析的内容有下面内容:

  • reactive 的实现
  • track 依赖收集
  • trigger 触发依赖
  • effect.scheduler
  • effect.stop
  • readonly 的实现
  • isReactive
  • isReadonly
  • 嵌套 reactive
  • 嵌套 readonly
  • isRef
  • unref
  • computed

effect篇

测试一:基本功能

js 复制代码
it('should run the passed function once (wrapped by a effect)', () => {
  const fnSpy = jest.fn(() => {})
  effect(fnSpy)
  expect(fnSpy).toHaveBeenCalledTimes(1)
})
  • 使用jest.fn模拟函数,便于监听调用情况,此时我们可以看到,effect函数进行包裹的时候就会进行一次fnSpy的执行,我们看看effect函数做了什么。
js 复制代码
/packages/reactivity/src/effect.ts
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  // 关注这里的 new ReactiveEffect(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
}
  • 我们先关注 new ReactiveEffect(fn)
js 复制代码
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() {
		···
      return this.fn()

		···
  }
	···
}

这里很容易就可以看明白,构造函数接受了fn参数,然后定义了run 方法,当执行run方法的时候,则执行this.fn(),也就是在前面我们可以看到执行了_effect.run()。

测试二:基本功能

js 复制代码
it('should observe basic properties', () => {
    let dummy
    const counter = reactive({ num: 0 })
    effect(() => (dummy = counter.num))

    expect(dummy).toBe(0)
    counter.num = 7
    expect(dummy).toBe(7)
  })

effect传入一个函数,在这个函数有响应式数据,当响应式数据发生变化的时候,传入的函数会再次执行

再次回到effect函数中,每次执行effect都会new ReactiveEffect,得到_effect实例,然后执行_effect.run

js 复制代码
const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined
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() {
    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
      }
    }
  }

}

提前定义了两个变量 effectStack、activeEffect。当执行run的时候,会判断effectStack中是否已经存过activeEffect,如果没有存过则执行effectStack.push((activeEffect = this))这里做了两步操作,一个是将this赋值给activeEffect, 然后将activeEffect保存在effectStack。然后执行了this.fn。当执行了this.fn就出触发依赖收集,则会执行track。然后把activeEffect收集到dep中,完成依赖收集,那么后续的修改触发set,便会执行dep里的activeEffect(),如下

js 复制代码
// 依赖收集
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!isTracking()) {
    return
  }
  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()))
  }
  trackEffects(dep, eventInfo)
}

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
	···
  dep.add(activeEffect!)
	···
}

当响应式数据发生了变化,则会触发set。然后会触发trigger去触发依赖更新

js 复制代码
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  for (const effect of isArray(dep) ? dep : [...dep]) {
    ···
    effect.run()
    ···
  }
}

因为执行了run ,所以之前传入的fn会再次执行,所以测试可以通过

测试三:实现返回值

js 复制代码
it('should return a new reactive version of the function', () => {
    function greet() {
      return 'Hello World'
    }
    const effect1 = effect(greet)
    const effect2 = effect(greet)
    expect(typeof effect1).toBe('function')
    expect(typeof effect2).toBe('function')
    expect(effect1).not.toBe(greet)
    expect(effect1).not.toBe(effect2)
})

测试用例中effect会返回一个函数。看着这个函数是一个什么样的函数。

js 复制代码
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)
	···
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

其实返回的就是_effect.run,每一次进行effect包裹就会创建一个实例对象,有一个小伏笔runner.effect = _effect可以考虑到后续使用effect上的方法,我们看下面的用例:

测试四:实现effect.scheduler

js 复制代码
it('scheduler', () => {
    let dummy
    let run: any
    const scheduler = jest.fn(() => {
      run = runner
    })
    const obj = reactive({ foo: 1 })
    const runner = effect(
      () => {
        dummy = obj.foo
      },
      { scheduler }
    )
    // 首次会执行一次_fn()
    expect(scheduler).not.toHaveBeenCalled()
    expect(dummy).toBe(1)
    // should be called on first trigger
    // 第二次会会通过scheduler来控制执行时机
    obj.foo++
    expect(scheduler).toHaveBeenCalledTimes(1)
    // should not run yet
    expect(dummy).toBe(1)
    // manually run
    run()
    // should have run
    expect(dummy).toBe(2)
})

首先模拟一个函数scheduler, 然后用对象包裹放到effect的第二个参数中。进行effect的包裹,则会执行一次_fn(),当响应式数据发生改变的时候,则执行了scheduler。

js 复制代码
export const extend = Object.assign

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的第二个参数options, 代码中做了判断,如果以后这个options参数,则执行将options和_effect合并到_effect上,此时_effect就有了scheduler属性。当响应式数据发生变化的时候,执行trigger,接着执行triggerEffect时。

js 复制代码
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()
      }
    }
  }
}

如果scheduler存在的时候则执行scheduler,就不会执行run了。

测试四:实现effect.stop

js 复制代码
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)
  })

这段测试代码中将effect返回的函数用stop包裹了一下,然后当响应式数据发生了变化,就不会在触发依赖更新了。

猜测:当执行stop的时候,将dep的对应的依赖删掉?

js 复制代码
export function stop(runner: ReactiveEffectRunner) {
  runner.effect.stop()
}

stop的参数是runner,也就是测试中effect执行返回的runner。

js 复制代码
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  const _effect = new ReactiveEffect(fn)
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

上面说到runner其实就是_effect.run,然后有吧new ReactiveEffect的实例_effect挂到了runner.effect上面。stop中就是执行的这个实例上面的stop。

js 复制代码
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() {
    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
  }
}

在执行stop的时候,this.active判断是否已经清空过了(性能优化)。然后执行了cleanupEffect(this),可以想到这一步就是清除依赖的。在cleanupEffect中首先是从实例中拿到deps。

疑问:那这个deps是什么时候存的呢?

回忆一下收集依赖的时候:

js 复制代码
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)
  }
}

当dep收集到当前的activeEffect,反向activeEffect中的deps也收集了dep。

理解一下:为什么要反向收集?因为要知道当前的副作用被dep收集了,后续清除依赖的时候,可以一并把这些dep中的activeEffect全部清掉。

上面说到拿到deps,然后遍历删除对应的activeEffect就可以了。所以此时当响应式数据再次发生变化的时候就找不到对应的依赖了。

测试五:实现onStop

js 复制代码
import {
  reactive,
  effect,
  stop,
  toRaw,
  TrackOpTypes,
  TriggerOpTypes,
  DebuggerEvent,
  markRaw,
  shallowReactive,
  readonly,
  ReactiveEffectRunner
} from '../src/index'

it('events: onStop', () => {
  const onStop = jest.fn()
  const runner = effect(() => {}, {
    onStop
  })

  stop(runner)
  expect(onStop).toHaveBeenCalled()
})

测试中,模拟了函数onStop,然后将这个函数传入了effect的第二个参数。

stop是已经定义好的方法,当执行了stop并且传入了runner,结果发现onStop执行了。

看下stop方法

js 复制代码
export function stop(runner: ReactiveEffectRunner) {
  runner.effect.stop()
}

再来会议一下这个runner是什么。

js 复制代码
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)
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

runner其实就是_effect.run(触发副作用),把_effect保存到了runner.effect上面了。而上面的stop的方法接受参数runner,然后拿到了runner的effect调用了stop。

在调用了stop中执行了这样的代码

js 复制代码
stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }

如果this.onStop存在在执行this.onStop

由于extend(_effect, options)所以effect的第二个参数{onStop}会被合并到实例上面,所以onStop会被调用

computed篇

测试一:实现基本功能(缓存)

js 复制代码
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的作用和effect相当,不过拿到这个计算属性的值需要.value,那么可以直接说明类里有get value()方法。

js 复制代码
/packages/reactivity/src/computed.ts
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions
) {
  let getter: ComputedGetter<T>
 	···
  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter)
 	···
  return cRef as any
}

computed返回一个由new ComputedRefImpl创建的实例, getter就是computed的第一个参数。

js 复制代码
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)
  
    if (self._dirty) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

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

new ComputedRefImpl构造函数会new ReactiveEffect创建一个副作用。

当访问由ComputedRefImpl类创建的实例的value的时候(所以访问计算属性的值得时候需要.value),会触发get

js 复制代码
if (self._dirty) {
  self._dirty = false
  self._value = self.effect.run()!
}

这一步操作,我认为是computed的精髓。_dirty默认是true

首先会判断_dirty是否为true,第一次的时候肯定是true,接着就把这个值设置为fasle,然后把self.effect.run()返回结果赋值给 self._value

执行run的时候就会访问响应式数据的值去收集依赖。

然后返回 self._value

js 复制代码
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)

    // now it should compute
    expect(cValue.value).toBe(1)
    expect(getter).toHaveBeenCalledTimes(2)

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

这个测试说明,当第一次访问cValue.value的时候,触发了get,首次触发的时候,_dirty由true变成了false。当value.foo 发生了变化的时候,则会去触发依赖的更新。

这里的触发依赖了reactive创建的响应式数据foo的依赖。

由于new ReactiveEffect有第二个参数scheduler,所以依赖更新的时候会执行scheduler,

js 复制代码
() => {
  if (!this._dirty) {
    this._dirty = true
  }
})

这里如果_dirty为false的时候, 则将设置成true。

当再次的访问computed的时候,会触发get,由于_dirty此时变成了true 所以会再次执行self._value = self.effect.run()!重新赋值。

如果连续访问computed的value的时候,而computed依赖的响应式数据没有发生变化,_dirty只有第一次访问的时候为true,后面再访问就是false,拿到的也就是第一次访问的值,这就建立了缓存

简而言之就是:通过effect的scheduler设置来实现的,我们第一次去获取computed计算属性,会直接执行一次fn(),而后修改响应式的值再进行获取,触发的是settrigger进行执行,先会去执行scheduler,那会使得dirty的值发生改变,使得获取数据不再是缓存。

isReactive

js 复制代码
test('Object', () => {
  const original = { foo: 1 }
  const observed = reactive(original)

  expect(isReactive(observed)).toBe(true)
  expect(isReactive(original)).toBe(false)

})

可以检查对象是否是由 reactive创建的响应式代理。

找到isReactive定义的位置

js 复制代码
/packages/reactivity/src/reactive.ts

export const enum ReactiveFlags {
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  RAW = '__v_raw'
}

export function isReactive(value: unknown): boolean {
  return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}

其实就是访问value的ReactiveFlags.IS_REACTIVE这个属性,然后就会触发代理对象中的get

js 复制代码
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }
  }
}

可以看到如果key是ReactiveFlags.IS_REACTIVE则返回!isReadonly

看下刚刚的例子,因为observed是调用reactive创建的对象,所以会在baseHandlers.ts文件中会走

js 复制代码
const get = /*#__PURE__*/ createGetter()

因为isReadonly没有传参所以默认为false所以此时访问的ReactiveFlags.IS_REACTIVE返回的!isReadonly为true

而isReactive(original) 由于original[ReactiveFlags.IS_REACTIVE]是undefined ,所以返回false

isReadonly

js 复制代码
it('should make nested values readonly', () => {
  const original = { foo: 1, bar: { baz: 2 } }
  const wrapped = readonly(original)
  expect(isReactive(wrapped)).toBe(false)
  expect(isReadonly(wrapped)).toBe(true)
  expect(isReactive(original)).toBe(false)
  expect(isReadonly(original)).toBe(false)
  expect(isReactive(wrapped.bar)).toBe(false)
  expect(isReadonly(wrapped.bar)).toBe(true)
  expect(isReactive(original.bar)).toBe(false)
  expect(isReadonly(original.bar)).toBe(false)
})

检查对象是否是由readonly创建的只读代理

isReadonly处理的方式和isReactive类似,isReadonly访问的是ReactiveFlags.IS_READONLY

js 复制代码
export function isReadonly(value: unknown): boolean {
  return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}

此时应该调用代理对象中的

js 复制代码
const readonlyGet = /*#__PURE__*/ createGetter(true)

回忆一下createGetter,此时isReadonly是true,所以当key为ReactiveFlags.IS_READONLY直接返回isReadonly 则会true

嵌套的对象是当在访问的时候判断是否是对象递归处理

js 复制代码
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)
}

上述功能说白了就是利用了proxy代理对象的性质,既然没有在类里定义(多种情况,定义了也无济于事),访问属性,那么key值就暴露出来了。

isRef

js 复制代码
test('isRef', () => {
  expect(isRef(ref(1))).toBe(true)
  expect(isRef(computed(() => 1))).toBe(true)

  expect(isRef(0)).toBe(false)
  expect(isRef(1)).toBe(false)
  // an object that looks like a ref isn't necessarily a ref
  expect(isRef({ value: 0 })).toBe(false)
})

检查值是否为一个 ref 对象

js 复制代码
export function isRef(r: any): r is Ref {
  return Boolean(r && r.__v_isRef === true)
}

其实这个和isReactive的实现方式差不多,isRef接受一个对象,判断这个对象的__v_isRef是否存在

回忆一下

js 复制代码
class RefImpl<T> {
  private _value: T
  private _rawValue: T

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

  constructor(value: T, public readonly _shallow: boolean) {
    ···
  }

  get value() {
    ···
  }

  set value(newVal) {
    ···
  }
}

ref其实就是一个工厂函数返回的对象实例,这个对象实例是通过new RefTmpl创建的。

这个类中有一个不可更改的属性 __v_isRef = true

所以只要是ref创建的值,都会有这个属性。

unRef

js 复制代码
test('unref', () => {
  expect(unref(1)).toBe(1)
  expect(unref(ref(1))).toBe(1)
})

如果参数是一个 ref,则返回内部值,否则返回参数本身

js 复制代码
export function unref<T>(ref: T | Ref<T>): T {
  return isRef(ref) ? (ref.value as any) : ref
}

readonly

首先看下测试

js 复制代码
it('should make nested values readonly', () => {
  const original = { foo: 1, bar: { baz: 2 } }
  const wrapped = readonly(original)
  expect(wrapped).not.toBe(original)
  expect(isReadonly(wrapped)).toBe(true)
  expect(isReadonly(original)).toBe(false)
  expect(isReadonly(wrapped.bar)).toBe(true)
  expect(isReadonly(original.bar)).toBe(false)
  // get
  expect(wrapped.foo).toBe(1)
  // has
  expect('foo' in wrapped).toBe(true)
  // ownKeys
  expect(Object.keys(wrapped)).toEqual(['foo', 'bar'])
})

it('should not allow mutation', () => {
  const qux = Symbol('qux')
  const original = {
    foo: 1,
    bar: {
      baz: 2
    },
    [qux]: 3
  }
  const wrapped: Writable<typeof original> = readonly(original)

  wrapped.foo = 2
  expect(wrapped.foo).toBe(1)
  expect(
    `Set operation on key "foo" failed: target is readonly.`
  ).toHaveBeenWarnedLast()

  wrapped.bar.baz = 3
  expect(wrapped.bar.baz).toBe(2)
  expect(
    `Set operation on key "baz" failed: target is readonly.`
  ).toHaveBeenWarnedLast()
})

Readonly的意思是我们只可以访问值,却不可以修改值

js 复制代码
export function readonly<T extends object>(
  target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
  return createReactiveObject(
    target,
    true,
    readonlyHandlers,
    readonlyCollectionHandlers,
    readonlyMap
  )
}

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  // 如果已经是响应式对象,则直接返回,避免重复创建
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // 响应式处理: 利用Proxy做代理
  // vue2
  // Object.defineProperty
  // COLLECTION: Map Set
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

其实差不多和reactive的处理方式是一样的,只不过第二个参数isReadonly是true

baseHandlers传的是readonlyHandlers,所以当访问用readonly包裹的对象的时候,proxy里面的拦截是用readonlyHandlers拦截的

js 复制代码
export const readonlyHandlers: ProxyHandler<object> = {
  get: readonlyGet,
  set(target, key) {
    if (__DEV__) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  },
  deleteProperty(target, key) {
    if (__DEV__) {
      console.warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      )
    }
    return true
  }
}

可以看到,当访问到对象的时候,会触发readonlyGet,而当修改对象,对着删除对象中的属性的时候,都会发出警告,所以由此可以断定,readonly只能访问不能修改和删除。

js 复制代码
const readonlyGet = /*#__PURE__*/ createGetter(true)
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
   
    const targetIsArray = isArray(target)

    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - does not apply for Array + integer key.
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

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

    return res
  }
}

这里首先返回res,然后判断是否是isReadonly,因为readonly是不允许修改的,所以就没有必要去触发依赖更新,那也就没有必要去收集依赖了。下面判断res是否是对象,来处理嵌套对象的,所以当readonly包裹的对象是一个嵌套的,那对象的每一层都是readonly的状态

相关推荐
百万蹄蹄向前冲35 分钟前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5811 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路1 小时前
GeoTools 读取影像元数据
前端
ssshooter2 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友2 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry3 小时前
Jetpack Compose 中的状态
前端
dae bal3 小时前
关于RSA和AES加密
前端·vue.js
柳杉3 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog4 小时前
低端设备加载webp ANR
前端·算法
LKAI.4 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi