Skip to content

声明响应式状态

ref() 在组合式API中,推荐使用ref()函数来声明响应式状态

javascript
import { ref } from 'vue'

const count = ref(0)

ref()接收参数,并将其包裹在一个带有.value属性的 ref 对象中返回

javascript
const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

要在组件模板中使用ref,要在组件的setup函数中声明并返回

javascript
import { ref } from 'vue'

export default {
  // `setup` 是一个特殊的钩子,专门用于组合式 API。
  setup() {
    const count = ref(0)

    // 将 ref 暴露给模板
    return {
      count,
    }
  },
}

注意: 在模板中使用ref时不需要添加.value

对于复杂i点的逻辑,可以在同一作用域中生命更改ref的函数,并暴露给模板

JavaScript

import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    function increment() {
      // 在 JavaScript 中需要 .value
      count.value++
    }

    // 不要忘记同时暴露 increment 函数
    return {
      count,
      increment
    }
  }
}

暴露出来的方法就可以用在模板页面上

html
<button @click="increment">{{ count }}</button>

<script setup>

setup函数中手动暴露大量的方法十分繁琐, Vue3 提供了对应的语法糖来简化操作

html
<script setup>
  import { ref } from 'vue'

  const count = ref(0)

  function increment() {
    count.value++
  }
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

<script setup> 中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用。

为社么使用ref而不是普通变量

当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式 系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染

在标准的JavaScript中,检测普通变量的访问或修改是行不通 的.热纳尔我们可以通过getterssetter方法来拦截对象属性的getset操作

.value属性给予了Vue一个机会来检测ref何时被访问或修改,在其内部,Vue在他的getter中执行追踪,在setter中执行触发 原理类似于:

javascript
const obj = {
  _value: 0,
  get value() {
    // 在这里执行追踪
    return this._value
  },
  set value(newValue) {
    // 在这里执行触发
    this._value = newValue
  },
}

深层响应性

Ref可以持有任何类型的值,包括深层嵌套的对象,数组或者JavaScript内置的数据结构,比如MapSet

Ref会使它的值具有深层响应性. 这意味着即使改变嵌套对象或数组时,变化也会被检测到

javascript
import { ref } from 'vue'

const obj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar'],
})

function mutateDeeply() {
  // 以下都会按照期望工作
  obj.value.nested.count++
  obj.value.arr.push('baz')
}

也可以通过shallow ref来放弃深层响应性,使用shallowRef()函数

javascript
import { shallowRef } from 'vue'

const obj = shallowRef({
  nested: { count: 0 },
  arr: ['foo', 'bar'],
})

function mutateShallowly() {
  // 以下不会触发更新
  obj.value.nested.count++
  obj.value.arr.push('baz')
}

对于浅层ref只有.value的访问会被追踪,而嵌套对象的属性访问不会被追踪,因此不会触发更新.浅层ref适用于那些不需要深层响应性的场景,例如当你只关心对象的引用变化时

Dom更新时机

DOM 会在响应式状态被修改时自动更新,但不是同步的. Vue会在next tick更新周期中会从所有的状态修改,以确保不管进行了多少次修改每个组件都只会被更新一次 要等待Dom更新完成后在执行额外的代码,可以使用nextTick()全局API

javascript
import { ref, nextTick } from 'vue'

const count = ref(0)

function increment() {
  count.value++
  nextTick(() => {
    // 在 DOM 更新后执行
    console.log('DOM 已更新')
  })
}

reactive()

ref外,还可以使用reactive()API声明响应式状态,不同的是,reactive()将使对象本身具有响应性

javascript
import { reactive } from 'vue'

const state = reactive({
  count: 0,
  message: 'Hello Vue!',
})

function increment() {
  state.count++
}

响应式对象是 JavaScript 代理(Proxy) 其行为就喝普通对象一样. 不同的是 Vue能够拦截对响应式对象所有属性的访问和修改,一遍进行依赖追踪和触发更新 (关于Proxy可以阅读这篇文章) reactive()将深层的转换对象: 当访问嵌套对象时,他们也会被reactive()包装.

ref的值是一个对象时,ref()也会在内部调用它,与浅层ref类似,也有一个shallowReactive()API可以选择退出深层响应

javascript
import { shallowReactive } from 'vue'

const state = shallowReactive({
  count: 0,
  nested: { message: 'Hello Vue!' },
})

function increment() {
  state.count++
  // 以下不会触发更新
  state.nested.message = 'Hello Vue!'
}

Reactive Proxy 和 Original

当你使用reactive()shallowReactive()创建一个响应式对象时,它会返回一个代理对象,这个代理对象是原始对象的代理 并不等同于原始对象.只有对代理的修改是响应式的,对原始对象修改不会触发更新.因此在Vue中,最佳实践是仅使用声明对象的代理版本

为了保证代理对象更多一致性,对同一个原始对象调用reactive()shallowReactive()会返回同一个代理对象 对同一个原始对象调用reactive()shallowReactive()也会返回同一个代理对象, 而对一个已存在的代理对象调用reactive()shallowReactive()会返回这个代理对象本身,而不是新的代理对象

reactive() 的局限性

1.有限的值类型 : 只能用于对象类型(包括数组和函数),不能用于原始值类型(如字符串、数字、布尔值等) 2.不能替换整个对象 : reactive()只能使对象的属性具有响应性,不能替换整个对象.如果需要替换整个对象,可以使用ref()shallowRef()

javascript
let state = reactive({ count: 0 })

// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 })

3.对解构操作不友好 : 将响应式对象解构后,解构的属性将不再具有响应性

javascript
import { reactive } from 'vue'

const state = reactive({ count: 0 })
const { count } = state
// 这里的 count 是一个普通变量,不再具有响应性
count++ // 不会触发更新

由于这些限制,简易使用ref()来声明响应式状态,除非需要处理复杂的对象或数组

额外的ref解包细节

当你在模板中使用ref时,Vue会自动解包它的值,因此你可以直接使用count而不是count.value

html
<template>
  <button @click="increment">{{ count }}</button>
</template>
<script setup>
  import { ref } from 'vue'

  const count = ref(0)

  function increment() {
    count.value++
  }
</script>

如果将一个新的ref赋值给一个已有的ref,则会自动解包并重新赋值

javascript
import { ref } from 'vue'

const count = ref(0)
count = ref(1) // count.value 现在是 1

在 Vue 3 的响应式系统中,ref 的解包行为取决于它所处的上下文环境:

深层响应式对象内:如果一个 ref 被嵌套在一个由 reactive() 创建的深层响应式对象中,当直接通过属性访问时,它会自动解包(即无需通过 .value 获取其值)。例如:

javascript
const deepObj = reactive({
  nested: {
    count: ref(1),
  },
})
console.log(deepObj.nested.count) // 输出 1(自动解包)

浅层响应式对象中:如果 ref 是浅层响应式对象(如 shallowReactive 创建的对象)的直接属性,访问时 不会 自动解包,仍需使用 .value:

javascript
const shallowObj = shallowReactive({
  count: ref(1),
})
console.log(shallowObj.count.value) // 需要手动 .value

总结:ref 的自动解包仅发生在深层响应式对象的嵌套属性中,而在浅层对象的顶层属性或非响应式对象中保持原始 ref 形态。

数组和集合的注意事项

ref的值是一个数组或集合时,可以直接使用数组或集合的方法来修改它们的内容,而不需要使用.value

javascript
import { ref } from 'vue'

const items = ref(['apple', 'banana', 'orange'])

function addItem(item) {
  // 直接使用数组方法
  items.value.push(item)
}

function removeItem(index) {
  // 直接使用数组方法
  items.value.splice(index, 1)
}

ref作为响应式数组或原生集合类型(如SetMap)中的元素被访问时,它不会解包

javascript
import { ref } from 'vue'

const books = reactive([ref('Vue3 Guide')])
console.log(books[0].value) // Vue3 Guide

在模板中解包的注意事项

在模板渲染上下文中,只有顶级ref属性才会被解包

html
<template>
  <div>
    <p>{{ count }}</p>
    <!-- 自动解包 -->
    <p>{{ object.id +1 }}</p>
    <!-- 不会自动解包 -->
  </div>
</template>
<script setup>
  import { ref } from 'vue'

  const count = ref(0)
  const object = { id: ref(1) }
</script>

这个例子中,id将会被渲染为[object Object] + 1,因为它没有被解包,而count会被自动解包并渲染为数字 为了解决这个问题,我们可以将id结构为一个顶级属性

html
<template>
  <div>
    <p>{{ count }}</p>
    <!-- 自动解包 -->
    <p>{{ id + 1 }}</p>
    <!-- 自动解包 -->
  </div>
</template>
<script setup>
  import { ref } from 'vue'

  const count = ref(0)
  const object = { id: ref(1) }
  const { id } = object // 将 id 结构为顶级属性
</script>

这样id在使用时就符合我们的预期了

另外,如果ref是文本插值的最终计算结果,它也会被解包,这个特性是文本插值的便利特性

html
<template>
  <div>
    <p>{{ count }}</p>
    <!-- 自动解包 -->
    <p>{{ object.id }}</p>
    <!-- 自动解包 -->
  </div>
</template>
<script setup>
  import { ref } from 'vue'

  const count = ref(0)
  const object = { id: ref(1) }
</script>