Vue3.0源码系列(四):响应式原理(readonly,isReadonly,isReactive)

458 阅读4分钟

这篇文章我们继续探索Vue3.0源码中的三个很重要的响应式api的的原理,废话不多少,直接搞起。。。,下面是三个api的官网解释,让我们一起看一下源码中是怎样实现的吧。

上一篇:Vue3.0源码系列(三):响应式原理(stop和onStop原理实现)

一. readonly:接受一个对象 (响应式或纯对象),任何被访问的属性为只读。老规矩,我们从他的单元测试开始讲起,

单元测试(1):我们要实现的源码逻辑为创建一个对象a,用readonly函数来包裹a对象,这是就有了一个wrapped对象,我们期待被readonly包裹之后wrapped和a是不相等的。这个很好理解,因为被readonly包裹,是只读对象,肯定会和a对象不相等。然后我们读取wrapped.foo属性值为1.

单元测试(2):我们利用jest模拟一个警告warn方法,当我们企图给readonly的对象赋值时,警告warn被调用,因为 user为只读对象。

describe('readonly',()=>{
  //单元测试(1)
  it('happy path',()=>{
    const a = {foo:1,bar:{baz:2}}
    const wrapped = readonly(a)
    expect(wrapped).not.toBe(a)
    expect(wrapped.foo).toBe(1)
  })
  //单元测试(2)
  it('warn then call set',()=>{
    console.warn = jest.fn()
    const user = readonly({age:12})
    user.age = 11
    expect(console.warn).toBeCalled()
  })
})

下面就是源码当中的代码,它向外导出一个readonly方法,我们会向里面传入一个数据对象,只读也就是说明我们的数据不会被set,也就是说数据不会被更新,所以set中不会触发trigger触发依赖。因为不会触发trigger,所以再get中我们也就无需做依赖收集track。下面代码我们就实现了单元测试(1)和 单元测试(2)。大家觉得是不是超级简单那。当然源码中会对此进行一个代码的优化和封装,这无关紧要,对我们理解不产生影响

   export function readonly(raw) {
      return new Proxy(raw, {
          get(key,value) {
              let res = Reflect.get(target,key)
              //track()
              return res
          }
          set(target,key,value) {
              //trigger()
              console.warn(`key:${key} set 失败,因为 target是readonly`,target)
              return true
          }
      })
    }
    

二.isReactive:检查对象是否是由reactive创建的响应时代理。老规矩,我们从他的单元测试开始讲起:

 describe('reactive', () => {
  it('happy path',()=>{
    const original = {age:10}
    const observe = reactive(original)
    expect(observe).not.toBe(original)
    expect(observe.age).toBe(10)
    //单元测试(1)
    expect(isReactive(observe)).toBe(true)
    //单元测试(2)
    expect(isReactive(original)).toBe(false)
  })
})

源码分析:我们判断一个数据是否是reactive,只需要知道其是否触发了我们new proxy的get方法,这样我们就很容易得到,我们要在创建reactive的get方法里去做文章。怎样触发get那,是不是当我们访问 value[###],就可以触发我们proxy中的get方法。

//源码实现
export function isReactive(value) {
  return !!value[ReactiveFlags.IS_REACTIVE];
}

export const enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
}

//baseHandlers.ts
function createGetter(isReadonly=false) {
  return function get(target,key) {
    //当key匹配时也就是isReactive成立,放回true
    if(key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    }
    let res = Reflect.get(target,key)
    
    return res
  }
}
 

三.isReadonly:检查对象是否是由readonly创建的只读代理。老规矩,我们从他的单元测试开始讲起,

单元测试(1):首先我们用readonly包裹数据,然后用isReadonly包裹只读数据wrapped,可以看出来他一定是一个只读数据,返回true

单元测试(2):当我们访问只读数据里面的属性时,还应该是一个只读,也应该返回true

单元测试(3):用isReadonly包裹一个原始数据a时,我们可以想到,a没有被readonly包裹,它肯定不是一个只读属性,所以一定返回false

describe('readonly',()=>{
  it('happy path',()=>{
    const a = {foo:1,bar:{baz:2}}
    const wrapped = readonly(a)
    //单元测试(1)
    expect(isReadonly(wrapped)).toBe(true)
    //单元测试(2)
    expect(isReadonly(wrapped.bar)).toBe(true)
    //单元测试(3)
    expect(isReadonly(a)).toBe(false)
  })
})

isReadonly的源码逻辑和isReactive源码逻辑一致,通过触发get方法来实现isReadonly的判断。

export const enum ReactiveFlags {
  IS_READONLY = '__v_isReadonly'
}
export function isReadonly(value) {
  return !!value[ReactiveFlags.IS_READONLY];
}

function createGetter(isReadonly=false) {
  return function get(target,key) {
    if(key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    }else if(key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    }
    let res = Reflect.get(target,key)
    
    return res
  }
}

总结:至此,今天的三个api就讲到这里了,相信大家在工作中还是非常常见的在使用他们,了解其中原理能够让我们在使用中更加从容。今天的分享就到这里啦,我们下期见,886!晚安