我们都知道 Vue 的响应式是通过数据劫持实现的,Vue2 的数据劫持是通过 Object.defineProperty 实现的,而 Vue3 升级的一部分原因就是将defineProperty替换成更为强大的 Proxy。
众所周知,Proxy 是 defineProperty 的升级版,之所以没有推广开来的原因还是适配性的问题。现在时机终于成熟啦 。
我们现在要分析的就是响应式的 reactive 和 ref,这两者的使用方式不尽相同。当我们使用的时候也许会有疑问:既然都是响应式,那么这两者的本质区别是什么呢。带着这个疑问我们来分析一下两者的源码吧。
废话不多说,我们直接看一下 reactive 的源码:
//packages/reactivity/src/reactive.ts
export function reactive(target: object) {
//如果传入的数据是readonly的,那么直接返回这个readonly的数据
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
//返回一个Reactive对象
return createReactiveObject(
target,
false,
mutableHandlers, //响应式数据的代理handler,一般Object和Array
mutableCollectionHandlers //响应式集合的代理handler,一般是Set、Map、WeakMap、WeakSet
)
}
上面这段代码很简单,就是当我们调用 reactive 的时候:
至于上面代码提到了一个ReactiveFlags枚举,我们这里插播一下该参数的类型
export const enum ReactiveFlags {
SKIP = '__v_skip', //用于标记对象不可进行代理
IS_REACTIVE = '__v_isReactive', //reactive
IS_READONLY = '__v_isReadonly', //readonly
RAW = '__v_raw' //是proxy上面的原始target
}
接着就是使用 createReactiveObject 创建响应式对象,那么在看createReactiveObject之前,我们需要了解一下,
//原始对象到响应式对象的映射
export const reactiveMap = new WeakMap<Target, any>()
//readonly对象到原始对象的映射
export const readonlyMap = new WeakMap<Target, any>()
可以看出在 reactive.ts 中会预存两个 WeakMap:reactiveMap 和 readonlyMap。这两者分别代表了原始对象到响应式对象的映射和 readonly 对象到响应式对象的映射。
const enum TargetType {
INVALID = 0, //其他类型
COMMON = 1, //Array、Object类型的
COLLECTION = 2 //Set 、 Map 、 WeakMap 、 WeakSet类型的
}
上面代码告诉我们,target 目标数据的类型有三种其中COMMON表示 Array 和 Object 的基本类型,COLLECTION表示 Set、Map、WeakMap、WeakSet 四种集合类型,INVALID表示无效类型
下面我们继续顺着代码来看一下**createReactiveObject()**的实现:
首先,createReactiveObject接收四个参数目标对象、是否是 readonly、基本数据类型的劫持 handler、集合类型的数据劫持 handler。
function createReactiveObject(
target:Target,
isReadonly:boolean,
baseHandlers:ProxyHandler,
collectionHandlers:ProxyHandler
)
接下来,对目标数据进行判断。如果不是对象类型,则直接返回该数据并且在开发环境下给出警报。如果目标数据是 Proxy 直接返回该数据,但是有个例外情况,那就是当我们在 reactive 基础上调用 readonly 的时候。
//如果不是一个对象直接返回,开发环境下给出提示
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
//如果已经是一个响应式Proxy则直接返回响应式的Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
上面那句:当我们在 reactive 的基础上调用 readonly 的时候,听起来比较拗口,所以我们直接上一个测试用例看一下上述情况出现的时机:
//packages/reactivity/__tests__/readonly.spec.ts
test('readonly + reactive should make get() value also readonly + reactive', () => {
const map = reactive(new Collection())
const roMap = readonly(map)
const key = {}
map.set(key, {})
const item = map.get(key)
expect(isReactive(item)).toBe(true)
expect(isReadonly(item)).toBe(false)
const roItem = roMap.get(key)
expect(isReactive(roItem)).toBe(true)
expect(isReadonly(roItem)).toBe(true)
})
最后,是我们 reactive 创建响应式的核心逻辑了。使用 Proxy 实现数据劫持,传入我们的劫持 handler。同时将 Proxy 响应式收集起来更新响应式数据映射。
//只有普通类型和集合类型数据类型才可以被处理为响应式
//主要类型:Object,Array,Map,Set,WeakMap,WeakSet
const targetType = getTargetType(target)
//如果数据类型是无效的
if (targetType === TargetType.INVALID) {
return target
}
//创建响应式Proxy
const proxy = new Proxy(
target,
//根据类型传入handler(collection:Set、Map、WeakMap、WeakSet,common:Object,Array)
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
//响应式数据收集起来,更新响应式数据映射
proxyMap.set(target, proxy)
return proxy
看完上面的代码,我们来简单总结一下 crateReactiveObject:
上面我们看完了 reactive 的源码,知道了 reactive 的创建方式,下面我们来看一下 ref 的源码实现,相信我们看到最后也就能解答出我们上面提出的问题:ref 和 reactive 的本质区别是什么了。
和 reactive 相同的暴露方法:
ref 是通过 createRef方法创建 ref 对象,和createReactiveObject不同的是,createRef 调用 new RefImpl()来完成 ref 对象的创建。
//packages/reactivity/src/ref.tss
export function ref(value?: unknown) {
return createRef(value)
}
//创建ref对象
function createRef(rawValue: unknown, shallow = false) {
//如果本身就是一个ref的话,直接返回值
if (isRef(rawValue)) {
//如果目标数据是ref数据类型,直接返回
return rawValue
}
return new RefImpl(rawValue, shallow)
}
下面我们看一下 RefImpl class 的源码:
class RefImpl {
//当前数据
private _value: T
//这个属性会标记对象是否是ref
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow = false) {
//如果shallow为false,直接让ref.value等于value,否则对rawValue进行convert转换成reactive
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
//读取value的时候会出发track
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
//先判断新老value是否相同,如果不相同则再赋值并触发trigger
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
这里,我们首先分析一下其中的constructor:
接下来就是 RefImpl 中 value 的 get 和 set 方法,这两个方法很简单:
♣ 友情提示,上面提到的 trigger 和 track,大家可以参考上一篇文章
♣ 从这里我们也可以看出来 ref 对象将响应式的数据挂载到了value属性上
接下来我们再看一下 convert 的实现:
//数据类型转换
const convert = (val: T): T =>
//最终还是通过reactive()方法处理数据
isObject(val) ? reactive(val) : val
这个方法的实现也是非常简单,就是单纯的判断一下我们的数据是不是 Object,如果是的话直接reactive处理我们的数据,否则直接返回该数据。
好了,看完 ref 的代码之后我们就可以总结出 ref 和 reactive 的本质区别了:
这里我们贴一部分 ref 的测试用例:
这个测试用例也证实了我们源码中的set部分,相同的数据赋值不会触发赋值和 trigger
describe('reactivity/ref', () => {
it('should hold a value', () => {
const a = ref(1)
expect(a.value).toBe(1)
a.value = 2
expect(a.value).toBe(2)
})
it('should be reactive', () => {
const a = ref(1)
let dummy
let calls = 0
effect(() => {
calls++
dummy = a.value
})
expect(calls).toBe(1)
expect(dummy).toBe(1)
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
// same value should not trigger
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
})
这节比较简单明了,主要是解析了一下 reactive 和 ref 的一个源码实现。其实我们这里就算涉及到的 reactive 和 ref 源码部分也仅仅是比较核心的一点,至于一些衍生的方法,例如 readonly,shallowReactive,isReactive 等方法。这里均没有涉及
是因为考虑到这些方法都是从其中衍生出来的,所以没有必要详细的解释了。
因为最近搬砖比较忙,所以文章总是断断续续的写,也没有太强的连贯性 。希望后面会好起来啦,也算是对自己美好的期望吧 。