引子
在看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