了解 Vue3 的响应式利器,让你开发效率大大提升

在读本篇文章前先了解一下Vue3响应式原理是怎样,可以查资料或者我的上一篇文章:\# 🔥细说Vue响应式原理的10个细节![1]

相对于Vue2的defineProperty实现的数据响应式,Vue3对数据响应的处理分工更加明确,通过组合式api中ref与reactive两个暴露给开发者的函数对数据进行包装,从而实现了数据响应式,那么它们有什么区别?下面我们一起来根据例子来学习!

ref定义基本数据类型、引用数据类型的响应式。也就是说ref(value),这个value类型可以是基本数据类型,也可以是引用数据类型,但是在js中使用时必须以属性.value格式使用,在template中可以直接调用数据。

<template>
  <div>
    <div><button @click="changeValue">修改</button></div>
    <div>
      <p>当前strRef:{{ strRef }}</p>
      <p>当前objRef:姓名:{{ objRef.name }} 爱好:{{ objRef.hobboy }}</p>
      <p>当前arrRef:{{ arrRef }}</p>
    </div>
  </div>
</template>
<script>
import { defineComponent, ref, shallowRef } from 'vue'
export default defineComponent({
  setup () {
    const strRef = ref('sapper');// 基本数据类型
    const arrRef = ref([1, 3, 2]);// 数组类型
    const objRef = ref({  // 对象类型
      name: 'sapper',
      hobboy: ['吉他', '原神']
    })
    const changeValue = () => {
      strRef.value = '工兵';
      arrRef.value[1] = 4;
      objRef.value.hobboy[1] = '滑冰';
    }
    return {strRef,objRef,arrRef,changeValue}
  }
})
</script>
复制代码

reactive定义引用类型数据的响应式,不支持基本数据类型,如果需要写基本数据类型只能是放在对象中,也就是说reactive(value),这个value类型必须是引用类型。

<template>
  <div>
    <div><button @click="changeValue">修改</button></div>
    <div>
      <div>当前objReactive:
        <br/>
        姓名:{{ objReactive.name }}<br/> 
        爱好:{{ objReactive.hobboy }}
      </div>
      <div>当前arrReactive:{{ arrReactive }}</div>
    </div>
  </div>
</template>
<script>
import { defineComponent, reactive } from 'vue'
export default defineComponent({
  setup () {
    const arrReactive = reactive([1, 3, 2]);// 数组类型
    const objReactive = reactive({  // 对象类型
      name: 'sapper',
      hobboy: ['吉他', '原神']
    })
    const changeValue = () => {
      arrReactive[1] = 4;
      objReactive.name = '工兵';
      objReactive.hobboy[1] = '滑冰';
    }
    return {objReactive,arrReactive,changeValue}
  }
})
</script>
复制代码

从上面两个例子中我们可以看出不管什么类型数据,ref都需要以.value来调用ref定义的数据,对于引用数据类型来看,我们可以看出代码不美观,所以一般对于引用类型数据,都推荐使用reactive来定义对于基本数据类型,可以使用ref也可以使用reactive来定义。既然到了这里我们都了解了ref和reactive的运用区别了,那么我们继续来一起探讨一下它们的响应原理又有什么区别?

揭秘ref

从上面的例子,我们先打印看一下基本数据类型(strRef)、引用数据类型(arrRef、ObjRef)的ref内部封装结构是什么样的?如下三图所示

66e8c28c108ef234da29cdf9d3902aac.jpeg
image.png
848e9cfe62065bea9a8ceaa9a8594195.jpeg
image.png

96a9cecfc57e12b448da0a4c9f145175.jpeg 从上面图片可以看出,不管是什么类型的数据,对于ref封装数据都是一个RefImpl对象reference implement的简写,是引用实现的意思,每个RefImpl对象都有6个属性:

  • dep:是一个Set类型的数据,用来存储当前的ref值收集的依赖。

  • _ v _ isRef :标记位,只要被ref定义了,都会标识当前数据为一个Ref,也就是它的值标记为true。

  • _ v _ isShallow:判断是否是shallowRef定义的数据。

    与ref不同的是,当使用shallowRef为引用类型创建响应性时,修改深层属性,不具备响应性。只有对.value的引用时才触发。

    const state = shallowRef({ count: 1 })
    // 不会触发更改
    state.value.count = 2
    // 会触发更改
    state.value = { count: 2 }
    复制代码
  • _ rawValue:用于保存当前ref值对应的原始值,如果传递的参数是对象,它就是用于保存转化前的原始值,否则_ value与_ rawValue相同。

  • _ value:用于保存ref当前值,如果传递的参数是对象,它就是用于保存经过reactive函数转化后的值,否则_ value与_ rawValue相同。从上面例子我们可以发现,对于引用类型的数据,它的值就是一个proxy对象,这其实就是reactive封装数据后对象(后面会说)。我们先看一下下图,发现_ rawValue就是没有做响应性处理的原始值,在看看_ value是一个proxy对象就是做了reactive响应处理的值。

    _ rawValue与**_ value**就是为了区分引用类型数据是否做响应式处理。

    3e7aeb3a24adbb0535c63ce27177e790.jpeg
    image.png
  • value:保存的是当前的值。

既然我们清楚了ref给数据封装了什么属性,接下来开始探讨源码究竟给怎么给上面6个属性进行赋值的:

  • ref函数:Vue3向开发者暴露的是ref函数,其实它就是封装了一个createRef函数。

    export function ref(value?: unknown) {
    return createRef(value, false)
    }
    复制代码
  • createRef函数:有两个参数,一个是要做响应处理的数据,一个是判断数据是否为shallowRef定义的数据。它主要做的事情就是判断当前rawValue(暂时没有做响应处理的数据)是否为ref类型数据、创建RefImpl实例对象。

    function createRef(rawValue: unknown, shallow: boolean) {
    if (isRef(rawValue)) {
      return rawValue
    }
    return new RefImpl(rawValue, shallow)
    }
    复制代码
  • RefImpl类:创建RefImpl类给_ rawValue和_ value属性赋值,判断当前定义的ref数据是否为shallowRef定义的数据,然后获取响应性值时对数据依赖进行收集并返回_ value,修改响应式值时修改并通知依赖更新。

    ref定义的数据为什么需要带.value调用数据? 就是因为RefImpl类暴露给实例对象的get、set方法是value,所以在调用的时候,需要带上。

    b72e70c8efa3371e9a7feac7d2a6c87f.jpeg
    image.png

其实,RefImpl实例关键就在于trackRefValue(this)triggerRefValue(this, newVal)的两个函数的处理,我们大概也知道它们就是依赖收集、依赖更新。这里就不一一探讨。

揭秘Reactive

上面也说了reactive封装数据的用法,它只支持传入引用类型数据(数组、对象),如果需要在reactive中使用基础类型只能放在对象中。既然这样我们来探讨一下reactive函数究竟做了什么?

const arrReactive = reactive([1, 3, 2]);// 数组类型
const objReactive = reactive({  // 对象类型
  name: 'sapper',
  hobboy: ['吉他', '原神']
})
const changeValue = () => {
  arrReactive[1] = 4;
  objReactive.name = '工兵';
  objReactive.hobboy[1] = '滑冰';
  console.log('arrReactive',arrReactive);
  console.log('objReactive',objReactive);
}
复制代码

688a4c203b9445a4e82d69a7c49563c0.jpeg 从上图可以看出,使用reactive封装的数据返回的都是一个proxy对象,proxy就是代理,如把一个对象代理到另一个对象,好比如房子所有者,代理房子给二手房东销售,二手房东就可以拥有房子销售权利。从上图我们可以看到Proxy对象有三个属性:

  • **[[Handler]]**:创建Proxy对象传入的第二个参数,是对当前需要代理的目标target进行一些相关配置处理。

  • **[[Target]]**:需要代理的目标target,也就是被代理的目标。

  • **[[IsRevoked]]**:表示是否可撤销,生成可撤销的proxy对象用Proxy.revocable()方法。那么Proxy对象可以做什么?我们看看下面例子:

    const houseOwner = {home:'房源',price:1200,type:'一房一厅'};
    const proxyOwner = new Proxy(houseOwner,{
    get:function (target,key){
      console.log(`${key}属性被访问!`)
      return target[key];
    },
    set:function(target,key,value){
      if(target[key]===value){
        return;
      }
      target[key] = value;
      return target[key];
    },
    })
    console.log(proxyOwner);
    proxyOwner.price = 1300;// 对被代理对象的修改
    proxyOwner.remark = '采光点好!';
    console.log(proxyOwner.price);// price属性被访问
    复制代码

从这个例子,可以看出Proxy对象的第二个参数给代理目标带上相关属性:set方法、get方法,再回到reactive封装的数据中,数据的Handler属性给数据带上了五个属性:deletePropertygetsethasownKeys。这五个属性怎么来的?我们一起探讨一下Vue3的源码实现:

// 源码位置:core-main/packages/reactivity/src/reactive.ts
// Vue3中暴露给开发者的是reactive方法
export function reactive(target: object) {
  // 判断target是否只读,是就不做处理
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}
复制代码

createReactiveObject函数主要为了创建Proxy实例对象,参数传了五个属性:target(目标数据)、isReadonly(target是否只读)、mutableHandlers(ProxyHandler)、mutableCollectionHandlers(ProxyHandler类型)、proxyMap(数据集合)。我们先了解一波createReactiveObject函数:

18fa2ba3e6cc0a4c94d7807fa2f9edc3.jpeg
image.png
  • ReactiveFlags:响应式数据标记。

    export const enum ReactiveFlags {
    SKIP = '__v_skip',// 标记对象不可进行代理
    IS_REACTIVE = '__v_isReactive',// 是否是Reactive封装的
    IS_READONLY = '__v_isReadonly',// 是否只读
    IS_SHALLOW = '__v_isShallow',// 是否是shallowRef封装的
    RAW = '__v_raw'// 是否是proxy原始的target
    }
    复制代码
  • TargetType:target的数据类型。

    const enum TargetType {
    INVALID = 0,
    COMMON = 1,// Array、Object类型
    COLLECTION = 2 // Set、Map、WaekMap、WeakSet类型
    }
    复制代码
  • baseHandlers:对于Array、Object类型数据,Proxy实例的第二个参数。传入的baseHandlers就是mutableHandlers。这个函数主要是为了给Proxy对象带上五个属性。

    // 源码位置:core-main/packages/reactivity/src/baseHandlers.ts
    export const mutableHandlers: ProxyHandler<object> = {
    // createGetter() 主要实现依赖收集和Reflect.set(target, key, value, receiver)
    get,
    // createSetter() 主要实现通知依赖更新和Reflect.get(target, key, receiver)
    set,
    // deleteProperty() 主要是删除target的指定key的属性Reflect.deleteProperty(target, key)
    deleteProperty,
    // has() 主要是判断target是否存在指定key的属性,Reflect.has(target, key)
    has,
    // ownKeys() 主要是获取target的key数组,Reflect.ownKeys(target)
    ownKeys
    }
    复制代码
  • collectionHandlers:对于Set、Map、WaekMap、WeakSet类型数据,Proxy实例的第二个参数。传入的baseHandlers就是mutableCollectionHandlers。mutableCollectionHandlers主要是对 set、map、weakSet、weakMap 四种类型的对象进行劫持。

    // 源码位置:core-main/packages/reactivity/src/mutableCollectionHandlers.ts
    export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
    get: /*#__PURE__*/ createInstrumentationGetter(false, false)
    }
    function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
    const instrumentations = shallow? 
      isReadonly? shallowReadonlyInstrumentations: shallowInstrumentations
      : isReadonly? readonlyInstrumentations: mutableInstrumentations
    
    return (target: CollectionTypes,key: string | symbol,receiver: CollectionTypes) => {
      ...
      return Reflect.get(
        hasOwn(instrumentations, key) && key in target? instrumentations:target,
        key,
        receiver
      )
    }
    }
    复制代码

总结

  • ref:定义基本数据类型、引用数据类型的响应式。封装数据类型为ref类型,主要就是创建了RefImpl实例对象

  • reactive:定义引用类型数据的响应式,不支持基本数据类型,如果需要写基本数据类型只能是放在对象中。封装数据为reactive类型,主要是创建了Proxy实例对象,通过Reflect实现数据的获取与修改。

关于本文

作者:前端有路灯

https://juejin.cn/post/7185798606522155068

最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿

回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!

回复「交流」,吹吹水、聊聊技术、吐吐槽!

回复「阅读」,每日刷刷高质量好文!

如果这篇文章对你有帮助,「在看」是最大的支持

 》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值