Skip to content
第 115 / 250 章前端⏱ 10 分钟阅读

第 115 章:响应式基础

学习目标

  • 理解 Vue 3 响应式系统的核心原理
  • 掌握 ref、reactive、computed 的用法
  • 学会 watch 和 watchEffect 的使用
  • 区分 ref 与 reactive 的适用场景

一、什么是响应式

响应式:当数据变化时,依赖该数据的视图自动更新。

1.1 一个最简单的例子

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

const count = ref(0);

const increment = () => count.value++;
</script>

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

点击按钮 → count.value++ → Vue 检测到变化 → 模板自动更新。

1.2 响应式 vs 普通变量

typescript
// ❌ 普通变量:改变后视图不会更新
let count = 0;
const increment = () => {
  count++;
  // 视图不会更新,因为 Vue 没有追踪 count
};

// ✅ 响应式:改变后视图自动更新
import { ref } from 'vue';
const count = ref(0);
const increment = () => {
  count.value++;
  // 视图自动更新
};

二、ref:定义响应式数据

2.1 基本用法

typescript
import { ref } from 'vue';

const count = ref(0);        // number
const name = ref('Tom');      // string
const list = ref([1, 2, 3]);  // 数组
const user = ref({ id: 1 });  // 对象
const nothing = ref(null);    // null

2.2 访问与修改

typescript
const count = ref(0);

console.log(count.value);  // 0
count.value = 10;           // 修改
count.value++;              // 自增

模板自动解包

<template> 中使用 ref 时,Vue 会自动 .value,不需要写 .value

vue
<script setup>
const count = ref(0);
</script>

<template>
  <!-- 不用 .value -->
  <p>{{ count }}</p>
  <button @click="count++">+</button>
</template>

2.3 ref 包装对象

typescript
const user = ref({ name: 'Tom', age: 18 });

// 修改整个对象
user.value = { name: 'Jerry', age: 20 };

// 修改属性(自动响应式)
user.value.name = 'Jerry';
user.value.age = 21;

// 在模板中,顶层属性自动解包
// user.value 是对象,需要通过 user.name 访问

三、reactive:定义响应式对象

3.1 基本用法

typescript
import { reactive } from 'vue';

const state = reactive({
  count: 0,
  name: 'Tom',
  list: [1, 2, 3],
  user: { id: 1 }
});

// 直接访问/修改属性(不需要 .value)
state.count++;
state.user.name = 'Jerry';
state.list.push(4);

3.2 reactive 的局限性

typescript
const state = reactive({ count: 0 });

// ❌ 整体替换不响应
state = reactive({ count: 1 });  // 报错!const 不能重新赋值

// ✅ 用 ref 替代
const count = ref(0);
count.value = 1;  // ✅
// 或
let state = reactive({ count: 0 });
state = reactive({ count: 1 });  // ✅ 但失去响应式

// ✅ 用 Object.assign
Object.assign(state, { count: 1, name: 'New' });

3.3 ref vs reactive

维度refreactive
适用类型任意仅对象
访问.value直接访问
整体替换✅ 支持❌ 不支持
解构丢失响应式丢失响应式
TS 推断精确自动推断

3.4 何时选哪个

typescript
// ✅ ref:基本类型 / 需要整体替换 / 单值
const count = ref(0);
const name = ref('Tom');

// ✅ reactive:对象/数组,且不需要整体替换
const state = reactive({
  user: null,
  list: []
});

// ✅ 实际项目推荐:全部用 ref + 单个 store
const user = ref<User | null>(null);
const list = ref<User[]>([]);

四、computed:计算属性

4.1 基本用法

typescript
import { ref, computed } from 'vue';

const firstName = ref('Tom');
const lastName = ref('Jerry');

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});

// fullName 是一个 ref
console.log(fullName.value);  // 'Tom Jerry'

4.2 特点:有缓存

typescript
const count = ref(0);
const doubled = computed(() => {
  console.log('计算 doubled');
  return count.value * 2;
});

console.log(doubled.value);  // 计算 doubled, 0
console.log(doubled.value);  // 不再计算,直接返回缓存
count.value = 1;
console.log(doubled.value);  // 计算 doubled, 2(因为 count 变了)

4.3 可写的计算属性

typescript
const firstName = ref('Tom');
const lastName = ref('Jerry');

const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`;
  },
  set(newValue: string) {
    const [first, last] = newValue.split(' ');
    firstName.value = first;
    lastName.value = last;
  }
});

fullName.value = 'Alice Cooper';
console.log(firstName.value);  // 'Alice'
console.log(lastName.value);   // 'Cooper'

五、watch:侦听器

5.1 基本用法

typescript
import { ref, watch } from 'vue';

const count = ref(0);

// 监听 ref
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} -> ${newVal}`);
});

// 修改 count 触发回调
count.value = 1;  // 输出: count: 0 -> 1

5.2 监听 reactive 对象的属性

typescript
const user = reactive({ name: 'Tom', age: 18 });

// ❌ 直接监听 reactive 对象不生效
// watch(user, ...)  // ❌

// ✅ 监听 reactive 的某个属性(getter 形式)
watch(
  () => user.name,
  (newVal, oldVal) => {
    console.log(`name: ${oldVal} -> ${newVal}`);
  }
);

5.3 监听多个数据源

typescript
const firstName = ref('Tom');
const lastName = ref('Jerry');

watch(
  [firstName, lastName],
  ([newFirst, newLast], [oldFirst, oldLast]) => {
    console.log(`${oldFirst} ${oldLast} -> ${newFirst} ${newLast}`);
  }
);

5.4 选项

typescript
const count = ref(0);

// 立即执行
watch(count, (val) => {
  console.log(val);
}, { immediate: true });

// 深监听(reactive 对象)
const state = reactive({ nested: { count: 0 } });
watch(state, (val) => {
  console.log(val);
}, { deep: true });

5.5 watchEffect:自动收集依赖

typescript
import { watchEffect } from 'vue';

const count = ref(0);

// 自动追踪函数内用到的响应式数据
watchEffect(() => {
  console.log('count is:', count.value);
  // 任何 count 变化都会触发
});

count.value = 1;  // 输出: count is: 1

watch vs watchEffect

维度watchwatchEffect
监听源显式指定自动收集
初始执行默认不执行立即执行
访问新旧值✅ 支持❌ 不支持
适用已知监听源自动追踪

六、解构响应式对象

6.1 直接解构会丢失响应式

typescript
const state = reactive({ count: 0, name: 'Tom' });

// ❌ 丢失响应式
const { count, name } = state;
console.log(count);  // 普通 number,变化不触发更新

6.2 用 toRefs 保留响应式

typescript
import { toRefs } from 'vue';

const state = reactive({ count: 0, name: 'Tom' });

// ✅ 解构后仍是 ref
const { count, name } = toRefs(state);

console.log(count.value);  // ref 对象,变化触发更新

6.3 用 toRef 单独转换

typescript
import { toRef } from 'vue';

const state = reactive({ count: 0 });

// 单个属性转 ref
const countRef = toRef(state, 'count');

七、响应式工具函数

7.1 isRef / isReactive / isReadonly

typescript
import { ref, reactive, readonly, isRef, isReactive, isReadonly } from 'vue';

const r = ref(0);
const obj = reactive({});
const ro = readonly({});

isRef(r);          // true
isReactive(obj);   // true
isReadonly(ro);    // true

7.2 unref

typescript
import { unref } from 'vue';

const r = ref(0);
const value = unref(r);  // 等价于 r.value,如果是普通值则原样返回

7.3 shallowRef / shallowReactive

typescript
import { shallowRef, shallowReactive } from 'vue';

// 浅响应式:只对顶层属性响应
const state = shallowReactive({
  count: 0,
  nested: { value: 0 }
});

state.count = 1;          // ✅ 响应
state.nested.value = 1;   // ❌ 不响应

7.4 readonly

typescript
import { readonly } from 'vue';

const original = reactive({ count: 0 });
const copy = readonly(original);

// 修改会报警告
copy.count = 1;  // ⚠️ Set operation on key "count" failed

八、实战案例

8.1 待办列表

vue
<script setup lang="ts">
import { ref, computed } from 'vue';

interface Todo {
  id: number;
  text: string;
  done: boolean;
}

const todos = ref<Todo[]>([]);
const newTodo = ref('');

// 添加
const add = () => {
  if (!newTodo.value.trim()) return;
  todos.value.push({
    id: Date.now(),
    text: newTodo.value,
    done: false
  });
  newTodo.value = '';
};

// 删除
const remove = (id: number) => {
  todos.value = todos.value.filter((t) => t.id !== id);
};

// 切换状态
const toggle = (id: number) => {
  const todo = todos.value.find((t) => t.id === id);
  if (todo) todo.done = !todo.done;
};

// 统计
const remaining = computed(
  () => todos.value.filter((t) => !t.done).length
);
</script>

<template>
  <div class="todo">
    <input v-model="newTodo" @keyup.enter="add" placeholder="添加待办" />
    <button @click="add">添加</button>

    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" :checked="todo.done" @change="toggle(todo.id)" />
        <span :class="{ done: todo.done }">{{ todo.text }}</span>
        <button @click="remove(todo.id)">删除</button>
      </li>
    </ul>

    <p>剩余: {{ remaining }} 项</p>
  </div>
</template>

8.2 搜索过滤

vue
<script setup lang="ts">
import { ref, computed } from 'vue';

const search = ref('');
const items = ref([
  'Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'
]);

const filteredItems = computed(() => {
  if (!search.value) return items.value;
  return items.value.filter((item) =>
    item.toLowerCase().includes(search.value.toLowerCase())
  );
});
</script>

<template>
  <input v-model="search" placeholder="搜索..." />
  <ul>
    <li v-for="item in filteredItems" :key="item">{{ item }}</li>
  </ul>
</template>

8.3 异步数据加载

vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue';

const userId = ref(1);
const user = ref<{ name: string } | null>(null);
const loading = ref(false);

watchEffect(async () => {
  loading.value = true;
  user.value = await fetch(`/api/users/${userId.value}`).then((r) => r.json());
  loading.value = false;
});
</script>

<template>
  <input v-model.number="userId" type="number" />
  <p v-if="loading">加载中...</p>
  <p v-else-if="user">{{ user.name }}</p>
</template>

九、常见错误

9.1 解构丢失响应式

typescript
const state = reactive({ count: 0 });

// ❌
const { count } = state;

// ✅
const { count } = toRefs(state);
// 或者
const countRef = toRef(state, 'count');

9.2 ref 在模板外忘 .value

typescript
const count = ref(0);

// ❌ 在 script 中直接用
setTimeout(() => {
  console.log(count);  // ref 对象
}, 1000);

// ✅
setTimeout(() => {
  console.log(count.value);
}, 1000);

9.3 整体替换 reactive 对象

typescript
const state = reactive({ count: 0 });

// ❌ 整体赋值不响应
state = reactive({ count: 1 });

// ✅ 修改属性
state.count = 1;

// ✅ 用 ref 替代
const state = ref({ count: 0 });
state.value = { count: 1 };

十、本章小结

API作用
ref任意类型的响应式数据
reactive对象/数组的响应式代理
computed带缓存的派生值
watch显式监听数据变化
watchEffect自动收集依赖
toRefs / toRef解构保留响应式
readonly只读代理
shallowRef / shallowReactive浅响应式

动手练习

  1. 计算属性:写一个购物车,显示商品总数和总价
  2. watch:监听表单输入,实现"输入即搜索"(debounce)
  3. watchEffect:实现一个自动同步 localStorage 的响应式数据
  4. 解构:用 toRefs 解构 reactive 对象,验证模板中仍响应

推荐阅读


下一章第 116 章:组件基础

本站基于 VitePress 构建 · 由 Codebook 团队维护