上篇文章我们分析了reactive方法的过程,并完成了手写一个reactive方法,但此时的reactive方法是有局限性的;有以下两个问题: 1.reactive方法能接受简单类型的值吗?
js
<script>
const { reactive, effect } = Vue
const obj = reactive('张三')
console.log(obj)
</script>
结果控制台报错了,reactive最终要生成一个proxy的实例,new Proxy
的target必须是一个对象,它是一个被代理对象;如果target变成了一个非对象的值,此时的new Proxy
是没办法代理的,所以会报错;
2.如果我们对reactive的obj进行解构,它还具备响应性吗?
js
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: '张三'
})
let { name } = obj
effect(() => {
document.querySelector('#app').innerText = name
})
setTimeout(() => {
name = '李四'
console.log(name)
}, 1000)
</script>
我们等待了一秒,控制台里打印出李四
,但页面上的张三
也没变成李四
;也就是说reactive在解构之后会失去响应性;这是为什么呢?obj
在解构之前有响应性因为它是一个proxy
,let { name } = obj
解构之后,name
不是一个proxy
了,它变成了一个字符串,也就失去了响应性;
上述两个🌰中,我们可以知道现在的reactive方法是有局限性的,它不能在非对象的数据上使用;如果想要非对象的数据也具有响应性,该怎么办呢?这时候我们就可以通过ref来实现了;本篇文章就是探究ref是如何解决这个问题的。
ref复杂数据类型的响应性
首先有一个🌰:
js
<script>
const { ref, effect } = Vue
const obj = ref({
name: '张三'
})
console.log(obj)
effect(() => {
document.querySelector('#app').innerText = obj.value.name
})
setTimeout(() => {
obj.value.name = '李四'
}, 2000)
</script>
js
export function ref(value?: unknown) {
return createRef(value, false)
}
js
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
rawValue为{name: "张三"}
对象;isRef是false跳过; 判断isRef的方法,就是判断r.__v_isRef
的属性;
js
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true)
}
类似于上篇文章中的ReactiveEffect实例;
js
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) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
js
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
第8行的constructor中传入的value是{name: "张三"}
对象,__v_isShallow
是判断它是否是浅层的;this._rawValue
由toRaw
得到,还是{name: "张三"}
对象;this._value``是toReactive
来转化成reactive;这有一个重点:如果ref中传入的是对象,会走reactive的逻辑;如果不是对象,则会走ref逻辑;
第5行的dep,上篇文章中也有dep,是一个set存放ReactiveEffect实例;
第6行的__v_isRef
为true;isRef方法就能判断为true了;
js
<script>
const { ref, effect } = Vue
const obj = ref({
name: '张三'
})
console.log(obj)
effect(() => {
document.querySelector('#app').innerText = obj.value.name
})
setTimeout(() => {
obj.value.name = '李四'
}, 2000)
</script>
这段代码中的第7行会打印出下图的内容:
当我们的ref执行完后呢,接着该执行effect了;effect的逻辑在上篇文章这里可以看到,本篇文章不再赘述了;
现在我们来看一下RefImpl的set和get方法做了什么吧;来写一个简单的🌰;
js
<script>
class RefImpl {
// 实例的getter行为
get value() {
console.log('get value')
return 'get Value'
}
// 实例的setter行为
set value(val) {
console.log('set value')
}
}
const ref = new RefImpl()
console.log(ref)
</script>
如果我们在终端输入ref.value = 123
会触发RefImpl的set方法,再输入ref.value
会触发getter行为,这和Vue中RefImpl是类似的,让我们继续来看Vue的RefImpl;
js
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) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
第13行的get方法中,调用了trackRefValue完成依赖收集的工作,返回了this._value
,这时的this._value
是一个proxy的实例;
接着是trackRefValue的方法;
js
export function trackRefValue(ref: RefBase<any>) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref)
if (__DEV__) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
} else {
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
}
第2行的activeEffect肯定是存在的,如下图:
第5行触发了trackEffects,ref.dep = createDep()
生成了一个dep;
trackEffects方法有点眼熟啊,这是我们上篇文章分析过的;
js
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// 省略
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
}
}
第7行,activeEffect
加入dep
中;activeEffect
和dep
就建立起了关联;这里和reactive的逻辑相同;
总结:
1.对于ref函数,会返回RefImpl类型的实例;
2.在该实例中,会根据传入的数据类型分别进行处理:
a.复杂数据类型:转化为reactive返回的proxy实例;
b.简单数据类型:不作处理;
3.无论我们执行obj.value.name还是obj.value.name = XXX本质上都触发了get方法;
4.之所以会进行响应性是因为obj.value是一个reactive函数生产的proxy;
ref简单数据类型的响应性
js
<script>
const { ref, effect } = Vue
const obj = ref('张三')
console.log(obj)
effect(() => {
document.querySelector('#app').innerText = obj.value
})
setTimeout(() => {
obj.value = '李四'
}, 2000)
</script>
当我们ref里传入一个字符串时,发现它是有响应性的,页面中张三
变成了李四
;
让我们来探究一下过程,首先进入ref方法:
js
export function ref(value?: unknown) {
return createRef(value, false)
}
然后进入class RefImpl
中,这与上面内容是相同的;不同就是在constructor中,this._value
不再是toReactive
返回的而直接是value
;这就是简单数据类型和复杂数据类型不同的处理逻辑;
js
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) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
和上文相同的逻辑在这里先省略了; 当执行到setTimeout
时会触发第18行的set方法;上文中复杂数据结构出发的是reactive中的setter行为,而这里的简单数据类型触发的是RefImpl中的setter,这里是它们最大的不同; 第20行hasChanged(newVal, this._rawValue)
是用来判断张三
和李四
是否相同; 第21行 this._rawValue = newVal
修改原始值,改成newVal; 第22行先判断this.__v_isShallow
,这里的this._value
会直接是newVal
;
然后调用triggerRefValue方法:
js
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
if (ref.dep) {
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(ref.dep)
}
}
}
第3行中的ref.dep
中是有值的; 第5行triggerEffects
方法是用来循环触发依赖;到这时依赖被成功触发,页面中张三
就变成了李四
;
总结:
ref的简单数据类型是用proxy或者Object.defineProperty来实现的响应性吗?并不是,我们可以看到是通过上文的class RefImpl
中的set
和get
方法;
咱们下篇文章见!