Vue的ref和reactive的区别-源码解读

2024-05-30 20:33:41 浏览数 (1)

引子

在看vueuse官方文档的时候,有这么一段话 Use ref instead of reactive whenever possible

所以想从源码角度去看下,两者的差别,为什么官方要这么说

结论

先说结论

  • ref可以对基本数据类型保持响应式,reactive只能对对象,数组保持响应式
  • ref的取值要用.value
  • reactive的内部原理使用proxy实现的
  • ref如果传的是非基本数据类型,内部其实也是转成reactive,无本质区别

ref源码

ref的源码路径:packages/reactivity/src/ref.ts 先看一个使用代码

代码语言:javascript复制
import { ref } from 'vue';
const count = ref(0);

上面的代码中,引入的ref其实是一个方法

代码语言:javascript复制
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
  return createRef(value, false)
}

调用了内部的createRef方法,第二个参数是shallow,代表是否是浅层次的响应式,false代表是深层次的响应,比如传的是对象,对象内部的属性都会有响应式

如果用的是shallowRef,这个值就是true

代码语言:javascript复制
export function shallowRef<T = any>(): ShallowRef<T | undefined>
export function shallowRef(value?: unknown) {
  return createRef(value, true)
}

继续看下createRef

代码语言:javascript复制
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

这里有个判断,如果已经是ref对象,就直接返回,避免重复ref

代码语言:javascript复制
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)
  }
 
  get value() {
    trackRefValue(this)
    return this._value
  }
 
  set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, DirtyLevels.Dirty, newVal)
    }
  }
}
 
 
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

RefImpl是一个class,有一个_rawValue,保存原始value,另外有个value,如果是对象,会转成reactive,跟直接用reactive没本质区别,不是的话,就是原始value 另外RefImpl还有一个value的get和set方法,所以我们用ref都要用.value的原因

代码语言:javascript复制
get value() {
  trackRefValue(this)
  return this._value
}
 
 
export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    trackEffect(
      activeEffect,
      (ref.dep ??= createDep(
        () => (ref.dep = undefined),
        ref instanceof ComputedRefImpl ? ref : undefined,
      )),
      __DEV__
        ? {
            target: ref,
            type: TrackOpTypes.GET,
            key: 'value',
          }
        : void 0,
    )
  }
}
 
 
export const createDep = (
  cleanup: () => void,
  computed?: ComputedRefImpl<any>,
): Dep => {
  const dep = new Map() as Dep
  dep.cleanup = cleanup
  dep.computed = computed
  return dep
}

在value的get方法中,trackRefValue方法就是集成响应式绑定关系,activeEffect就是响应式副作用函数,createDep返回的是一个map对象,用于保存响应式信息

代码语言:javascript复制
export function trackEffect(
  effect: ReactiveEffect,
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
  if (dep.get(effect) !== effect._trackId) {
    dep.set(effect, effect._trackId)
    const oldDep = effect.deps[effect._depsLength]
    if (oldDep !== dep) {
      if (oldDep) {
        cleanupDepEffect(oldDep, effect)
      }
      effect.deps[effect._depsLength  ] = dep
    } else {
      effect._depsLength  
    }
    if (__DEV__) {
      effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
    }
  }
}

响应式函数,也会有一个deps数组,里面保存这被哪些对象的响应式引用,get保存的副作用函数,是为了在set中触发

代码语言:javascript复制
set value(newVal) {
  const useDirectValue =
    this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
  newVal = useDirectValue ? newVal : toRaw(newVal)
  if (hasChanged(newVal, this._rawValue)) {
    this._rawValue = newVal
    this._value = useDirectValue ? newVal : toReactive(newVal)
    triggerRefValue(this, DirtyLevels.Dirty, newVal)
  }
}
 
 
export function triggerRefValue(
  ref: RefBase<any>,
  dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
  newVal?: any,
) {
  ref = toRaw(ref)
  const dep = ref.dep
  if (dep) {
    triggerEffects(
      dep,
      dirtyLevel,
      __DEV__
        ? {
            target: ref,
            type: TriggerOpTypes.SET,
            key: 'value',
            newValue: newVal,
          }
        : void 0,
    )
  }
}

可以看到,只有新的内容跟原有内容不一样,才会触发响应式,响应式就是把副作用函数拿出来执行一下

reactive的源码

源码路径:packages/reactivity/src/reactive.ts 上面知道,ref的如果传的是对象,最终也是转成reactive,接下来看下reactive的实现,如何实现响应式

先看下reactive的使用例子

代码语言:javascript复制
import { reactive } from 'vue';
const state = reactive({ count: 0 });
// 访问
console.log(state.count); // 0
// 更新
state.count = 1;

其实reactive也是一个方法

代码语言:javascript复制
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 (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap,
  )
}

createReactiveObject方法,其实返回的是一个proxy

代码语言:javascript复制
export const reactiveMap = new WeakMap<Target, any>()
 
 
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>,
) {
  if (!isObject(target)) {
    if (__DEV__) {
      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 specific value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  )
  proxyMap.set(target, proxy)
  return proxy
}

reactiveMap是缓存所有的proxy,下次重新reactive避免重复生成proxy,由于是weakmap,再target被回收后,对应的proxy也会被自动回收

proxy都有一个handler,源码针对数组跟对象,是两个不同的handler处理,我们这里只看下对象的场景,就是baseHandlers,也就是方法传参的mutableHandlers

代码语言:javascript复制
export const mutableHandlers: ProxyHandler<object> =
  /*#__PURE__*/ new MutableReactiveHandler()
 
class MutableReactiveHandler extends BaseReactiveHandler{}
 
 
 
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
 
    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)
    }
 
    return res
  }
}

跟ref一样的,也是在get方法中,由track方法跟副作用函数绑定响应式,另外这个返回的如果是一个对象,返回是一个reactive对象,如果是基本类型,就直接返回基本类型

代码语言:javascript复制
const targetMap = new WeakMap<object, KeyToDepMap>()
 
 
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))))
    }
    trackEffect(
      activeEffect,
      dep,
      __DEV__
        ? {
            target,
            type,
            key,
          }
        : void 0,
    )
  }
}

targetMap用于全局存储全部的响应式数据,key就是对象,value也是一个Map数组

在这个map数组中,key是对象的某个属性字段,value是副作用函数,这样副作用函数是跟对象的某个字段绑定,而不是跟整个对象绑定

接下来看下set方法

代码语言:javascript复制
class MutableReactiveHandler extends BaseReactiveHandler {
  constructor(isShallow = false) {
    super(false, isShallow)
  }
 
 
  set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object,
  ): boolean {
    let oldValue = (target as any)[key]
     
 
    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方法,执行响应式

代码语言:javascript复制
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>,
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }
 
 
  let deps: (Dep | undefined)[] = []
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }
  }
 
  pauseScheduling()
  for (const dep of deps) {
    if (dep) {
      triggerEffects(
        dep,
        DirtyLevels.Dirty,
        __DEV__
          ? {
              target,
              type,
              key,
              newValue,
              oldValue,
              oldTarget,
            }
          : void 0,
      )
    }
  }
  resetScheduling()
}

triggerEffects就是从dep数组中,获取全部的响应式副作用函数,一个个调用执行

后记

现在看起来,vueUse官网的这个说法是不对的,没必要所有场景都用ref,因为在代码层面,用ref,都需要用.value,增加的复杂度,确实没必要,比如下面的例子

代码语言:javascript复制
import { ref } from 'vue';
 
// Using `ref` to hold an object
const userProfile = ref({
  name: 'John Doe',
  age: 30
});
 
function updateProfile() {
  // Access and modify the object through `.value`
  userProfile.value.name = 'Jane Doe';
  userProfile.value.age = 28;
}

所以我的结论是 如果是基础类型,就用ref,其他类型,统一用reactive

0 人点赞