最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue3 源码解析 05--响应式 reactive 和 ref

    正文概述 掘金(桔子汽水)   2020-12-05   606

    前言

    我们都知道 Vue 的响应式是通过数据劫持实现的,Vue2 的数据劫持是通过 Object.defineProperty 实现的,而 Vue3 升级的一部分原因就是将defineProperty替换成更为强大的 Proxy。 众所周知,Proxy 是 defineProperty 的升级版,之所以没有推广开来的原因还是适配性的问题。现在时机终于成熟啦 ?。

    我们现在要分析的就是响应式的 reactive 和 ref,这两者的使用方式不尽相同。当我们使用的时候也许会有疑问:既然都是响应式,那么这两者的本质区别是什么呢。带着这个疑问我们来分析一下两者的源码吧。

    reactive 源码分析

    废话不多说,我们直接看一下 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 的时候:

    • 首先判断当前数据是不是 readonly 对象,这里的 readonly 其实就是我们使用 [Vue.readonly 创建的对象
    • 然后调用 createReactiveObject()方法创建响应式的对象

    至于上面代码提到了一个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之前,我们需要了解一下,

    • Vue3 中响应式的映射类型:
      //原始对象到响应式对象的映射
      export const reactiveMap = new WeakMap<Target, any>()
      //readonly对象到原始对象的映射
      export const readonlyMap = new WeakMap<Target, any>()
    

    可以看出在 reactive.ts 中会预存两个 WeakMap:reactiveMap 和 readonlyMap。这两者分别代表了原始对象到响应式对象的映射readonly 对象到响应式对象的映射

    • 另外,还有 target 的数据类型:
      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<any>,
        collectionHandlers:ProxyHandler<any>
      )
    

    接下来,对目标数据进行判断。如果不是对象类型,则直接返回该数据并且在开发环境下给出警报。如果目标数据是 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
      }
    
    
    //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:

    • 首先判断我们传入的数据是否是对象类型的,如果不是直接返回该数据。
    • 判断是否已经是 Proxy,如果是的话直接返回该数据。但是需要注意一个例外情况:当我们使用 Vue.readonly()处理 reactive 数据的时候不适用该判断条件。
    • 最后,在满足我们数据类型的情况下,使用 Proxy 完成我们的数据劫持和响应式数据的映射。

    ref 源码

    上面我们看完了 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<T> {
      //当前数据
      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 接收两个参数,第一个是我们的原始数据,第二个参数是 shallow 的标识
    • 根据我们的 shallow 判断我们是否调用convert方法转化为 reactive

    接下来就是 RefImpl 中 value 的 get 和 set 方法,这两个方法很简单:

    • 当触发 get 方法的时候触发 track 方法
    • 当触发 set 方法的时候,要先判断 newValue 和 oldValue 是否相同,不相同进行赋值并且触发 trigger 方法

    ♣ 友情提示,上面提到的 trigger 和 track,大家可以参考上一篇文章 ♣ 从这里我们也可以看出来 ref 对象将响应式的数据挂载到了value属性上

    接下来我们再看一下 convert 的实现:

    //数据类型转换
    const convert = <T extends unknown>(val: T): T =>
      //最终还是通过reactive()方法处理数据
      isObject(val) ? reactive(val) : val
    

    这个方法的实现也是非常简单,就是单纯的判断一下我们的数据是不是 Object,如果是的话直接reactive处理我们的数据,否则直接返回该数据。

    好了,看完 ref 的代码之后我们就可以总结出 ref 和 reactive 的本质区别了:

    • 两者的底层响应式实现是一样的,都是通过 createReactiveObject 来实现的数据响应式
    • 不同的是,ref 在创建的时候添加了一个**__v_isRef**标记来标识当前为 ref 对象
    • ref 对自身的 value 属性添加了数据劫持,get 的时候触发 track 方法,set 的时候触发 trigger 方法

    这里我们贴一部分 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 等方法。这里均没有涉及 是因为考虑到这些方法都是从其中衍生出来的,所以没有必要详细的解释了。

    因为最近搬砖比较忙,所以文章总是断断续续的写,也没有太强的连贯性 ?。希望后面会好起来啦,也算是对自己美好的期望吧 ?。


    下载网 » Vue3 源码解析 05--响应式 reactive 和 ref

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元