vue3: 响应式基础

181 阅读4分钟

一、reactive() 函数 响应式状态

1. reactive() 作用

reactive() 可以创建一个响应式 对象或数组

import { reactive } from 'vue'

const state = reactive({ count: 0 })

2. 响应式代理 vs. 原始对象

值得注意的是,reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的:

const raw = {}
const proxy = reactive(raw)

// 代理对象和原始对象不是全等的
console.log(proxy === raw) // false

只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是 仅使用你声明对象的代理版本

为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:

// 在同一个对象上调用 reactive() 会返回相同的代理
console.log(reactive(raw) === proxy) // true

// 在一个代理上调用 reactive() 会返回它自己
console.log(reactive(proxy) === proxy) // true

这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:

const proxy = reactive({})

const raw = {}
proxy.nested = raw

console.log(proxy.nested === raw) // false

3.reactive() 的局限性

reactive() API 有两条限制:

  1. 仅对对象类型有效(对象、数组和 MapSet 这样的集合类型),而对 stringnumber 和 boolean 这样的 原始类型 无效。

  2. 因为 Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失:

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

同时这也意味着当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性:

const state = reactive({ count: 0 })

// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++

// count 也和 state.count 失去了响应性连接
let { count } = state
// 不会影响原始的 state
count++

// 该函数接收一个普通数字,并且
// 将无法跟踪 state.count 的变化
callSomeFunction(state.count)

二、setup() 函数

1. setup() 作用

要在组件模板中使用响应式状态,需要在 setup() 函数中定义并返回。

import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({ count: 0 })

    function increment() {
      state.count++
    }

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

暴露的方法通常会被用作事件监听器:

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

2. setup() 缺点

在 setup() 函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用构建工具<script setup></script> 来简化该操作

3. <script setup>

在 setup() 函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用构建工具来简化该操作。当使用单文件组件(SFC)时,我们可以使用 <script setup> 来大幅度地简化代码。

<script setup>
import { reactive } from 'vue'

const state = reactive({ count: 0 })

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

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

4. <script setup>注意项

<script setup> 中的顶层的导入和变量声明可在同一组件的模板中直接使用。你可以理解为模板中(template中)的表达式和 <script setup> 中的代码处在同一个作用域中。

三、 nextTick() 函数

当你更改响应式状态后,DOM 会自动更新。然而,你得注意 DOM 的更新并不是同步的。相反,Vue 将缓冲它们直到更新周期的 “下个时机” 以确保无论你进行了多少次状态更改,每个组件都只更新一次。

若要等待一个状态改变后的 DOM 更新完成,你可以使用 nextTick() 这个全局 API:

import { nextTick } from 'vue'

function increment() {
  state.count++
  nextTick(() => {
    // 访问更新后的 DOM
  })
}

四、 用 ref() 定义响应式变量

1. ref() 定义响应式变量

reactive() 的种种限制归根结底是因为 JavaScript 没有可以作用于所有值类型的 “引用” 机制。为此,Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref

import { ref } from 'vue'

const count = ref(0)

ref() 将传入参数的值包装为一个带 .value 属性的 ref 对象:

const count = ref(0)

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

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

简言之,ref() 让我们能创造一种对任意值的 “引用”,并能够在不丢失响应性的前提下传递这些引用。这个功能很重要,因为它经常用于将逻辑提取到 组合函数 中。

2. ref 在模板中的解包

当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value。下面是之前的计数器例子,用 ref() 代替:

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

const count = ref(0)

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

<template>
  <button @click="increment">
    {{ count }} <!-- 无需 .value -->
  </button>
</template>

请注意,仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包”。 例如, object 是顶层属性,但 object.foo 不是。 所以我们给出以下 object:

const object = { foo: ref(1) }

下面的表达式将不会像预期的那样工作:

<script setup>
import { ref } from "vue";
const num = ref(0)
const object = { foo: ref(100) }

</script>

<template>
  <p >数量:{{ num }}</p>
  <!--  [object Object]1 -->
  <p >数量:{{ object.foo +1}}</p> 
</template>

渲染的结果会是一个 [object Object]1,因为 object.foo 是一个 ref 对象。我们可以通过将 foo 改成顶层属性来解决这个问题, 渲染如图:

image.png

正确写法:

<script setup>
  import { ref } from "vue";
  const num = ref(0)
  const object = { foo: ref(1) }
  const { foo } = object

</script>

<template>
  <p >数量:{{ num }}</p>
  <p >数量:{{foo + 1}}</p> 
</template>

需要注意的是,如果一个 ref 是文本插值(即一个 {{ }} 符号)计算的最终值,它也将被解包。因此下面的渲染结果将为 1

<p >数量:{{object.foo}}</p> 

这只是文本插值的一个方便功能,相当于 {{ object.foo.value }}

3. ref 在响应式对象中的解包

当一个 ref 被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包,因此会表现得和一般的属性一样:

const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:

const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 1

只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。

4. 数组和集合类型的 ref 解包

const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)