从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的状态

相关推荐
程序媛小果1 分钟前
基于java+SpringBoot+Vue的桂林旅游景点导游平台设计与实现
java·vue.js·spring boot
喵叔哟20 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web