Vue3.0源码系列(六):响应式原理(ref,isRef,unRef)
这篇文章为大家带来ref系列API,ref系列api在我们日常开发中可以说是一定会用到。当我们在使用的时候,有没有想过它底层源码到底是怎样实现的那?又为我们做了哪些处理,是我们更方便,高效的进行搬砖那。好了,下面让我带你走进ref系列api的源码世界,一探究竟。文章是我学习过程的记录,希望和同学们分享知识,觉得讲的不错,可以点个赞哈,里面如果有些理解与您有冲突,可以交流哈。下面是我学习的github地址,里面有更全的分析,欢迎start哈,好了,开始今天的ref之旅......
上一篇Vue3.0源码系列(五):响应式原理(shallowReadonly,isProxy)
vue源码分析系列github地址:github.com/zzq921/my-m…
一:ref: 接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property的.value
1.下面我们利用单元测试来实现一个简单的ref功能,通过这4个单元测试,也就是说功能点,我们就会完整实现一个ref的核心逻辑。
单元测试(1):我们用ref包裹一个数字1,我们期待a.value可以返回我们的值1。
单元测试(2):我们在effect中获取用ref包裹起来的a的value值,我们期望得到dummy为1
单元测试(3):当我们为ref的a赋值为2时候,期望所有值变化为2.
单元测试(4):当我们重复为ref赋值同样的值时候,我们期望值不再变化。
describe('ref',()=>{
it('happy path',()=>{
const a = ref(1)
//单元测试(1)
expect(a.value).toBe(1)
})
it('should is reactive',()=>{
const a = ref(1)
let dummy;
let calls = 0
effect(()=>{
calls++
dummy = a.value
})
//单元测试(2)
expect(calls).toBe(1)
expect(dummy).toBe(1)
//单元测试(3)
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
//单元测试(4)
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
})
})
2.下面我们来看看源码中是怎么实现ref的吧,一起揭开它的神秘面纱,看看他的脸是不是红又圆。下图代码中,首先我们创建一个ref的函数,里面创建一个ref的类RefImpl,方便我们获取和监听value的改变。可以看到,通过创建类,我们就简单的实现了上面单元测试(1)的功能。当我们访问a.value 时候,就会返回数值1。
export function ref(value) {
//创建一个ref的class类
return new RefImpl(value)
}
//ref的类RefImpl
class RefImpl {
//创建私有属性
private _value: any;
constructor(value){
this._value = value
}
get value() {
//返回我们的传入值
return this._value
}
}
3.当我们实现单元测试(2)和(3)功能点的时候,我们首先要明白,用effect包裹,我们要收集依赖,它已经具有响应式,当我们访问value和获取变量值时候,应该进行依赖收集和触发依赖。在上面的源码基础上,我们书写逻辑源码。我们访问a.value时候,我们要进行依赖收集trackRefValue(this) ,当我们为a.value赋值时候,会触发依赖 triggerEffects(this.dep)
class RefImpl {
private _value: any; //创建私有属性
public dep; //创建公共属性dep,收集effect即收集依赖
constructor(value){
this._value = value
this.dep = new Set();
}
get value() {
trackRefValue(this) //进行依赖收集
return this._value
}
set value(newValue) {
this._value = newValue
triggerEffects(this.dep) //此为封装的触发依赖函数,实质前面的文章reactive已经讲过。
}
}
function trackRefValue(ref) {
//当我们收集的时候首先必须被effect了,也就是activeEffect存在,才会被收集。
if(isTracking()) {
//ref.dep 就是ref中新建的dep
trackEffects(ref.dep) //trackEffects为封装的收集依赖逻辑,实质前面的文章reactive已经讲过。
}
}
通过上面的源码,我们就实现了单元测试(2)和单元测试(3),当然,里面有一点要说明一下,那就是isTracking()这个函数,他是判断当前是否存在effect的标志,存在,依赖才会被收集。如果不存在,即没有effect了,当然就不用作收集了。
4.最后就剩下单元测试(4),实际上单元测试的逻辑实际上是个边缘的case,只要对源码做一下兼容,就能实现,它的意思就是,当我们赋值给ref同一个值时候,我们期望不去set中去触发依赖。下面我们就对它进行兼容吧。可以看到我们通过判断新newValue和旧this._value是否相等, 我们就实现了禁止依赖触发,实现了单元测试(4)。
set value(newValue) {
if(hasChange(this._value,newValue)) return
this._value = newValue
triggerEffects(this.dep) //封装的触发依赖函数,实质前面的文章reactive已经讲过。
}
export const hasChange = (val,newVal)=>{
return !Object.is(val,newVal)
}
5.当然,还有一个很重要的点,ref函数不仅传基本数据类型,还能够传对象。那么我们就看看对于传对象我们底层源码是怎么兼容的吧。首先,我们还是来看一下单元测试,可以看到我们获取ref的对象值还是得通过.value才能获取。
单元测试(5):我们可以通过.value获取到ref的对象值,当我们改变对象的value使,相应数据会变化。
it("should make nested properties reactive",()=>{
const a = ref({
count:1
})
let dummy;
effect(()=>{
dummy = a.value.count
})
//单元测试(5)
expect(dummy).toBe(1)
a.value.count = 2
expect(dummy).toBe(2)
})
其实vue3源码中对于ref传对象的情况,源码中是会区分的,当传入的是对象,它就会通过reactive对其进行一个包装,使其具有reactive的功能,能够进行数据响应。最重要的就是convert()函数,通过它的转化来处理参数为对象的情况。还有一个重要的点,就是_rawValue这个私有属性,它是为了保存最新的value值,方便新旧object进行对比。如果不保存,我们为新的值包裹成reactive,这样我们无法进行对比。这样我们就完成了单元测试(5)的功能。
class RefImpl {
private _value: any; //创建私有属性
private _rawValue: any;
public dep; //创建公共属性dep,收集effect
constructor(value){
this._rawValue = value // 保存最新的value值,方便新旧object进行对比,不然object和reactive(object)无法进行对比
this._value = convert(value)
this.dep = new Set();
}
get value() {
trackRefValue(this) //进行依赖收集
return this._value
}
set value(newValue) {
if(hasChange(this._value,newValue)){
this._rawValue = newValue // 保存最新的value值,方便新旧object进行对比,不然object和reactive(object)无法进行对比
this._value = convert(newValue)
triggerEffects(this.dep) //触发依赖
}
}
}
//当ref 中包裹为对象时,我们用reactive包裹,使其具有响应式数据。否则直接返回value值
function convert(value) {
return isObject(value)?reactive(value):value;
}
二:isRef:判断当前数据是否为ref类型。 单元测试(1):isRef包裹一个ref的数据b,期望返回一定是true 单元测试(2):number和reactive类型的数据,期望它不是ref
it('isRef',()=>{
const b = ref(1)
const user = reactive({age:1})
//单元测试(1)
expect(isRef(b)).toBe(true)
//单元测试(2)
expect(isRef(1)).toBe(false)
expect(isRef(user)).toBe(false)
})
下面我们看看源码中怎么实现的isRef,其实ref源码逻辑我们明白了,isRef的源码书写就顺理成章了,我们只需在ref的类中添加一个__v_isRef = true的public属性,通过判断__v_isRef是否为true,就实现了isRef的判断。
export function isRef(ref) {
//在ref类中添加一个__v_isRef变量,代表是ref
return !!ref.__v_isRef
}
class RefImpl {
public __v_isRef = true //属性代表为是ref
}
三:unRef: 如果参数为ref,则返回内部值,否则返回参数本身。val = isRef(val) ? val.value : val。
export function unRef(ref) {
//如果数据是ref,我们返回ref.value,如果不是的话 ,直接返回value就好了
return isRef(ref) ? ref.value : ref
}
至此,我们vue3当中ref的api我们就完成了,这就是他们底层的核心流程源码,其实最主要的就是ref的实现,相信大家如果认真学习,还是非常容易明白的,不像我们想象的那么难,如果过你也觉得这篇文章对你有帮助,欢迎点赞收藏哈。下一篇将为大家打来比较重要的计算属性computed的源码分析,大家期待吗?
转载自:https://juejin.cn/post/7084160711638663182