「Vue3学习篇」-Ref和Reactive函数

430 阅读7分钟

『引言』

🌟ref和reactive都是Vue3中新增的响应式API,用于实现组件的数据响应式更新。那它们二者分别是怎么使用的?二者的区别又是什么呢?……🤔,接下来我们带着疑问,一起来探索一下。💪💪

『Ref函数👇』

『作用』

一般用来定义一个基本类型的响应式数据(String、Number、Boolean……),返回的是一个ref对象,对象中有一个value属性。如果需要对数据进行操作,需要使用该ref对象的value属性

当然,ref也可以接受对象类型。将通过 reactive()转为具有深层次响应式的对象。

ref响应式原理是依赖于Object.defineProperty()get()set()

『语法』

const xxx = ref()

『⚠️注意』

  • 对数据进行操作:xxx.value
  • 模板中读取数据:<div>{{ xxx }}</div>
  • 在ref接受对象类型时,若要避免这深层次的转换,请使用 shallowRef() 来替代。

『示例』

关于ref()基本的知识了解的差不多了,现在我们就动手使用ref(),实现一个效果。

如下所示👇

ref.png

那话不多说,下面我们来动手实现一下💪💪

<template>
  <div>
    <h1>人物简介</h1>
    <p>姓名:{{name}}</p>
    <p>年龄:{{age}}岁</p>
    <p>爱好:{{hobbies.join('、')}}</p>
    <p>地址:{{address.provice}} - {{ address.city }} </p>
    <p>描述:{{description}}</p>
    <button @click="modifyInfo">
    修改信息
    </button> 
  </div> 
</template>

<script setup>
import { ref } from 'vue'
    const name = ref('pupu')
    const age = ref(10)
    const hobbies = ref(['唱歌', '画画'])
    const address = ref({
      provice: '浙江省',
      city: '杭州市'
    })
    const description = ref('一点也不可爱,不喜欢吃蜂蜜!')

    const modifyInfo = () => {
        name.value = 'wnxx'
        age.value = 3 
        hobbies.value = ['打羽毛球', '旅游']
        address.value.provice = '云南省'
        address.value.city = '丽江市'
        description.value = '非常的可爱,特别喜欢吃蜂蜜!'
        console.log(name,hobbies,address)
    } 
</script>

上面的示例看起来非常的简单,没有很复杂,并且从代码中我们可以看出数据类型举例的很全面,包括值类型,字符等等,还有数组、对象类型。

我们在点击修改信息按钮之后,在控制台打印了一下name,hobbies,addres三个信息。

如下图所示👇:

ref打印信息.png

『Reactive函数👇』

『作用』

定义一个对象类型的响应式数据,reactive定义的响应式数据是"深层次的"。

底层本质是将传入的数据包装成一个Proxy。

『语法』

const 代理对象 = reactive(源对象)
const proxy = reactive(obj)

『⚠️注意』

  • 对数据进行操作和模板中读取数据:都不需要.value
  • 若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,请使用 shallowReactive() 作替代。
  • 不能通过 ...data 方式,这样会丢失响应式。

『示例』

还是实现上述ref()实现的一个效果。

如下所示👇

<template>
  <div>
    <h1>人物简介</h1>
    <p>姓名:{{data.name}}</p>
    <p>年龄:{{data.age}}岁</p>
    <p>爱好:{{data.hobbies.join('、')}}</p>
    <p>地址:{{data.addres.provice}} - {{data.addres.city}}</p>
    <p>描述:{{data.description}}</p>
    <button @click="modifyInfo">
    修改信息
    </button> 
  </div> 
</template>

<script setup>
import { reactive } from 'vue'
    const data = reactive ({
      name: 'pupu',
      age: 10,
      hobbies: ['唱歌', '画画'],
      addres: {
        provice: '浙江省',
        city: '杭州市'
      },
      description: '一点也不可爱,不喜欢吃蜂蜜!'
      })

    const modifyInfo = () => {
        data.name = 'wnxx'
        data.age = 3 
        data.hobbies = ['打羽毛球', '旅游']
        data.addres.provice = '云南省'
        data.addres.city = '丽江市'
        data.description = '非常的可爱,特别喜欢吃蜂蜜!'
        console.log(data.name,data.hobbies,data.addres)
    } 
</script>

同样的,点击修改信息按钮之后,在控制台打印了一下name,hobbies,addres三个信息。打印出来的结果明显会发现是一个Proxy对象。

如下图所示👇:

reactive.png

『总结👇』

我们通过几个方面对ref()和reactive()进行一下对比,这样会更方便理解。

『ref和reactive的关系』

ref(obj)等价于reactive({value: obj})

『ref()和reactive()对比』

『定义数据』

ref()主要用于定义基本数据类型。

reactive()主要用于定义对象类型(数组类型)。

ref()也可以定义对象或数组类型,内部会通过reactive转为代理对象。

『使用方式』

ref()在对数据进行操作时,需要.value,模版中不需要。

reactive()则都不需要。

『原理』

ref()本质是Object.defineProperty()的get()和set()

reactive()使用proxy实现数据代理,并通过Reflect操作源对象内部的数据。

『答疑1』

为什么ref在对数据进行操作时,需要.value🤔❓

原因就是ref是通过.value属性的get和set来实现响应式的。

『ref源码分析』

『ref』

入口函数如下:

export function ref(value?: unknown) {
  return createRef(value, false)
}

『createRef』

接下来走的是createRef()这个方法。

function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

createRef()有两个参数,一个是rawValue传入的基本数据类型的默认值,另一个是shallow是否是深层次响应的boolean值。

isRef()是判断传入的rawValue是否是ref对象,是则直接返回;如果传入的rawValue不是ref对象,则返回一个RefImpl实例。

最后会调用RefImpl类构造函数,将rawValue, shallow传入。

『new RefImpl 生成实例对象』

class RefImpl<T> {
  // _value:表示ref接收的最新的值
  private _value: T
  // 私有的_rawValue:表示存放ref旧值
  private _rawValue: T

  public dep?: Dep = undefined
  // 公共的只读变量__v_isRef:表示标识该对象是一个ref响应式对象的标记
  public readonly __v_isRef = true

  // __v_isShallow:表示是否是浅层次响应的属性
  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }
}

在RefImpl类构造函数中,会判断是否是浅层次响应的属性,如果是,则直接把value的值赋值,不是则使用toReactive()。

export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

toReactive()就是看传入的value是否是引用类型,是则用reactive(),不是则直接返回。

『get方法』

当通过ref.value读取ref值时,就走get方法。

 get value() {
    trackRefValue(this)
    return this._value
  }

在get方法中会调用trackRefValue方法。

export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    if (__DEV__) {
      trackEffects(ref.dep || (ref.dep = createDep()), {
        target: ref,
        type: TrackOpTypes.GET,
        key: 'value'
      })
    } else {
      trackEffects(ref.dep || (ref.dep = createDep()))
    }
  }
}

『set方法』

当修改ref.value的值时,就走set方法,会对新旧值进行比较,不同就需要更新,新旧值更新之后会调用triggerRefValue方法。

set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, newVal)
    }
  }

triggerRefValue方法中,让依赖该 ref 的副作用函数执行更新。

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  const dep = ref.dep
  if (dep) {
    if (__DEV__) {
      triggerEffects(dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      triggerEffects(dep)
    }
  }
}

关于ref源码分析就到这🔚了。

『答疑2』

上面我们介绍reactive()的时候,注意点写到不能通过...data的方式,这样会丢失响应式。那这是为什么❓具体是什么原因❓如何解决这个问题🤔❓

首先我们先来看一看下面的这一段源码。

function createReactiveObject(
  target: Target,    // 传入的目标
  isReadonly: boolean,  // 是否只读
  baseHandlers: ProxyHandler<any>,  // 实现代理的核心,是将对象转换成代理对象的一些程序
  collectionHandlers: ProxyHandler<any>,//是将集合、数组转换成代理对象的一些程序
  proxyMap: WeakMap<Target, any>  // 存储 target 对象的
) {
  if (!isObject(target)) {   // 先判断 reactive 代理的是不是一个对象,因为proxy只能代理一个对象
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  
  // 优化:如果目标数据已经被代理了,直接返回
  if (   
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  
  // proxyMap 中已经存入过 target,直接返回
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  
  // 只有特定类型的值才能被 observe.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  
  // 如果上面的条件都不满足就通过 proxy 来代理一个响应式对象
  const proxy = new Proxy(   
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)   
  return proxy
}

这段源码是实现响应式代理的核心方法:createReactiveObject(),主要是通过调用createReactiveObject()函数来创建reactive对象。

从上面的源码中我们可以看出,该函数会先判断reactive()传入的参数是否为一个对象,如果不是则返回原值。这么说的话,其实reactive()可以传入基本类型的参数,只是这时候的数据会失去响应式。

『示例』

我们通过2个小示例来进行展示说明,这样会更加直观的感受到不一样的地方。

我们先定义一个原始的数据data,再传入reactive(),我们观察一下原始数据和响应式数据的变化。

<template>
  <div>
    <h1>{{ state.name }}</h1>
    <button @click="modifyInfo">
    修改信息
    </button>
  </div> 
</template>

<script setup>
import { reactive } from 'vue'
let data = { name: 'pupu' }
let state = reactive(data)

const modifyInfo = () => {
      data.name = 'wnxx'
      console.log(state.name) // wnxx,但template里仍为pupu
} 

</script>

从上面这段代码中可以看到,修改原始数据data.name,随之响应式数据state.name也会发生变化,我们点击修改信息按钮,但是会出现视图未更新的情况。

01.png 再来看一下下面这段代码👇:

<template>
  <div>
    <h1>{{ state.name }}</h1>
    <button @click="modifyInfo">
    修改信息
    </button>
  </div> 
</template>

<script setup>
import { reactive } from 'vue'
let data = { name: 'pupu' }
let state = reactive(data)

const modifyInfo = () => {
      state.name = 'wnxx'
      console.log(data.name) // wnxx,template里为wnxx
    } 

</script>

这段代码中,修改响应式数据state.name的值,原始数据data.name也会发生变化,点击修改信息的按钮,同时视图会更新。 02.png

我们继续刚才的源码分析,在判断完目标是否为一个对象之后,会判断目标是否已经被代理过,还会判断判断 proxyMap 中是否已经存入过 target,再然后判断特定类型 getTargetType()等等。具体的可以看官方对于 reactive 函数相关的代码,点击此处查看

『原因』

我们已经知道reactive() 函数会返回 Proxy 响应式的对象,如果直接解构的话,返回的数据不再是一个Proxy 响应式的对象,也就不具有响应式特点。

『示例』

onst target = {name: 'wnxx'}
const handler = { 
  get(target, key) {
        if (key in target) {
            return target[key]
        } else {
            return new ReferenceError(key + '属性不存在')
        }
    }
} 
const data = new Proxy(target, handler) 
const newdata = {...data}
console.log(newdata, 'newdata')
console.log(target, 'target')
console.log(data, 'data')

看一下控制台打印信息图:

03.png 通过查看控制台打印的信息,我们可以看到data一个Proxy对象经过 ... 解构以后的新对象newdata 已经不是 Proxy 对象了

『解决办法』

对于reactive解构,失去响应性问题。它的解决办法就是使用toRefs()。关于toRefs()在下一篇文章里会具体介绍的。