Vue3.0源码系列(六):响应式原理(ref,isRef,unRef)

这篇文章为大家带来ref系列API,ref系列api在我们日常开发中可以说是一定会用到。当我们在使用的时候,有没有想过它底层源码到底是怎样实现的那?又为我们做了哪些处理,是我们更方便,高效的进行搬砖那。好了,下面让我带你走进ref系列api的源码世界,一探究竟。文章是我学习过程的记录,希望和同学们分享知识,觉得讲的不错,可以点个赞哈,里面如果有些理解与您有冲突,可以交流哈。下面是我学习的github地址,里面有更全的分析,欢迎start哈,好了,开始今天的ref之旅......

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的源码分析,大家期待吗?

猜你喜欢

转载自juejin.im/post/7084160711638663182