编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

    • README
    • HTML工具手册

    • CSS样式与布局

    • JavaScript核心

    • TypeScript入门

    • Vue高级进阶

      • Vue3快速入门
      • Vue组件开发实战
      • Vue响应式原理
        • 3.1 响应式基础
          • 3.1.1 什么是响应式
          • 3.1.2 ref创建响应式
          • 3.1.3 reactive创建响应式
          • 3.1.4 ref和reactive区别
          • 3.1.5 综合案例与思考
        • 3.2 计算属性
          • 3.2.1 computed基本用法
          • 3.2.2 可写计算属性
          • 3.2.3 computed和method区别
          • 3.2.4 综合案例与思考
        • 3.3 侦听器watch
          • 3.3.1 watch基本用法
          • 3.3.2 监听多个源
          • 3.3.3 deep和immediate
          • 3.3.4 watchEffect
          • 3.3.5 停止侦听器
          • 3.3.6 综合案例与思考
        • 3.4 响应式工具函数
          • 3.4.1 toRef和toRefs
          • 3.4.2 shallowRef和shallowReactive
          • 3.4.3 readonly只读代理
          • 3.4.4 综合案例与思考
        • 3.5 响应式原理
          • 3.5.1 Proxy代理机制
          • 3.5.2 依赖收集与触发
          • 3.5.3 与Vue2的区别
          • 3.5.4 综合案例与思考
      • Vue路由管理实战
      • Vue状态管理实战
      • Vue组合式API
      • Vue工程化实践
    • Web工程化实践

  • Linux应用开发

  • IoT智能硬件开发

  • Apps
  • Web开发和进阶
  • Vue高级进阶
杨充
2026-04-23
目录

Vue响应式原理

# 03.Vue响应式原理

# 目录介绍

  • 3.1 响应式基础
    • 3.1.1 什么是响应式
    • 3.1.2 ref创建响应式
    • 3.1.3 reactive创建响应式
    • 3.1.4 ref和reactive区别
    • 3.1.5 综合案例与思考
  • 3.2 计算属性
    • 3.2.1 computed基本用法
    • 3.2.2 可写计算属性
    • 3.2.3 computed和method区别
    • 3.2.4 综合案例与思考
  • 3.3 侦听器watch
    • 3.3.1 watch基本用法
    • 3.3.2 监听多个源
    • 3.3.3 deep和immediate
    • 3.3.4 watchEffect
    • 3.3.5 停止侦听器
    • 3.3.6 综合案例与思考
  • 3.4 响应式工具函数
    • 3.4.1 toRef和toRefs
    • 3.4.2 shallowRef和shallowReactive
    • 3.4.3 readonly只读代理
    • 3.4.4 综合案例与思考
  • 3.5 响应式原理
    • 3.5.1 Proxy代理机制
    • 3.5.2 依赖收集与触发
    • 3.5.3 与Vue2的区别
    • 3.5.4 综合案例与思考

# 3.1 响应式基础

# 3.1.1 什么是响应式

响应式是 Vue 的核心特性:当数据变化时,视图自动更新。你只需要关注数据的变化,Vue 会帮你处理 DOM 更新。

传统方式(手动操作DOM):
  数据变化 → 手动找到DOM元素 → 手动更新内容

Vue响应式方式:
  数据变化 → Vue自动检测 → 自动更新DOM
// 传统方式:手动操作DOM
let count = 0
document.getElementById('btn').onclick = () => {
  count++
  document.getElementById('display').textContent = count  // 手动更新
}

// Vue方式:声明式响应式
const count = ref(0)
const increment = () => count.value++  // 只管改数据,DOM自动更新

# 3.1.2 ref创建响应式

ref 用于创建任意类型的响应式数据:

<template>
  <p>计数:{{ count }}</p>
  <p>名字:{{ name }}</p>
  <p>列表:{{ list }}</p>
  <button @click="increment">+1</button>
</template>

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

// 基本类型
const count = ref(0)          // Ref<number>
const name = ref('Vue')       // Ref<string>
const isActive = ref(true)    // Ref<boolean>

// 引用类型(也可以用 ref)
const list = ref([1, 2, 3])   // Ref<number[]>
const user = ref({ name: '张三', age: 28 })  // Ref<{name:string, age:number}>

// 在 JS 中访问/修改需要 .value
const increment = () => {
  count.value++                    // 修改基本类型
  list.value.push(4)               // 修改数组
  user.value.name = '李四'          // 修改对象属性
}

// 在模板中自动解包,不需要 .value
// {{ count }} 等价于 {{ count.value }}
</script>

# 3.1.3 reactive创建响应式

reactive 用于创建对象/数组类型的响应式数据:

<template>
  <p>{{ state.name }},{{ state.age }}岁</p>
  <p>技能:{{ state.skills.join(', ') }}</p>
  <button @click="updateInfo">更新信息</button>
</template>

<script setup lang="ts">
import { reactive } from 'vue'

// reactive 只能用于对象类型
const state = reactive({
  name: '张三',
  age: 28,
  skills: ['Vue', 'TypeScript']
})

// 直接修改属性,无需 .value
const updateInfo = () => {
  state.name = '李四'
  state.age++
  state.skills.push('React')
}

// ❌ 不能整体替换(会丢失响应式)
// state = { name: '新对象' }

// ✅ 可以用 Object.assign 批量更新
// Object.assign(state, { name: '王五', age: 30 })
</script>

# 3.1.4 ref和reactive区别

对比项 ref reactive
适用类型 任意类型(基本类型+对象) 仅对象/数组类型
访问方式 JS中需要 .value,模板中自动解包 直接访问属性
整体替换 ✅ ref.value = newValue ❌ 不能整体替换
解构 解构后保持响应式 ❌ 解构后丢失响应式
推荐场景 大多数场景(官方推荐) 表单等复杂对象
<script setup lang="ts">
import { ref, reactive } from 'vue'

// reactive 解构会丢失响应式!
const state = reactive({ count: 0, name: 'Vue' })
// ❌ 解构后不再是响应式
// let { count, name } = state

// ref 不存在解构问题
const count = ref(0)
const name = ref('Vue')
// 每个 ref 都是独立的响应式引用
</script>

官方建议:优先使用 ref。它更灵活、没有解构陷阱,适用于大多数场景。

# 3.1.5 综合案例与思考

综合案例:用户信息编辑器

<template>
  <div class="editor">
    <h3>用户信息编辑器</h3>
    <div class="form">
      <label>姓名:<input v-model="name" /></label>
      <label>年龄:<input v-model.number="age" type="number" /></label>
      <label>
        技能:
        <input v-model="newSkill" @keyup.enter="addSkill" placeholder="回车添加" />
      </label>
      <div class="tags">
        <span v-for="(skill, i) in skills" :key="skill" class="tag">
          {{ skill }}
          <button @click="skills.splice(i, 1)">×</button>
        </span>
      </div>
    </div>

    <div class="preview">
      <h4>预览(ref数据)</h4>
      <p>{{ name }},{{ age }}岁</p>
      <p>技能:{{ skills.join(' / ') }}</p>
    </div>

    <div class="preview">
      <h4>地址(reactive数据)</h4>
      <label>省:<input v-model="address.province" /></label>
      <label>市:<input v-model="address.city" /></label>
      <p>完整地址:{{ address.province }} {{ address.city }}</p>
    </div>
  </div>
</template>

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

// ref:基本类型 + 数组
const name = ref('张三')
const age = ref(28)
const skills = ref(['Vue', 'TypeScript'])
const newSkill = ref('')

// reactive:对象
const address = reactive({
  province: '广东省',
  city: '深圳市'
})

const addSkill = () => {
  if (newSkill.value.trim() && !skills.value.includes(newSkill.value.trim())) {
    skills.value.push(newSkill.value.trim())
    newSkill.value = ''
  }
}
</script>

<style scoped>
.editor { max-width: 500px; }
.form label { display: block; margin: 8px 0; }
.form input { padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; }
.preview { margin-top: 16px; padding: 12px; background: #f8f8f8; border-radius: 8px; }
.tags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.tag { background: #42b883; color: white; padding: 4px 10px; border-radius: 16px; font-size: 13px; }
.tag button { background: none; border: none; color: white; cursor: pointer; margin-left: 4px; }
</style>

思考题:

  1. ref(0) 返回的是什么类型的对象?为什么需要通过 .value 访问?
  2. reactive 的对象被整体替换后为什么会丢失响应式?底层原因是什么?
  3. 什么场景下适合用 reactive,什么场景下用 ref 更好?

# 3.2 计算属性

# 3.2.1 computed基本用法

computed 基于响应式数据派生新值,具有缓存特性:

<template>
  <p>原始价格:¥{{ price }}</p>
  <p>折扣:{{ discount * 10 }}折</p>
  <p>折后价格:¥{{ discountedPrice }}</p>
  <p>含税价格:¥{{ priceWithTax }}</p>

  <input v-model.number="price" type="number" />
  <input v-model.number="discount" type="number" step="0.1" min="0" max="1" />
</template>

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

const price = ref(100)
const discount = ref(0.8)

// 计算属性:只有 price 或 discount 变化时才重新计算
const discountedPrice = computed(() => {
  console.log('discountedPrice 重新计算')  // 依赖不变则不执行
  return (price.value * discount.value).toFixed(2)
})

const priceWithTax = computed(() => {
  return (price.value * discount.value * 1.13).toFixed(2)
})
</script>

# 3.2.2 可写计算属性

通过 get/set 创建可写的计算属性:

<template>
  <p>姓:{{ firstName }}</p>
  <p>名:{{ lastName }}</p>
  <p>全名:{{ fullName }}</p>
  <input v-model="fullName" placeholder="输入全名(姓 名)" />
</template>

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

const firstName = ref('张')
const lastName = ref('三')

const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (newValue: string) => {
    const parts = newValue.split(' ')
    firstName.value = parts[0] || ''
    lastName.value = parts[1] || ''
  }
})
</script>

# 3.2.3 computed和method区别

<template>
  <!-- 计算属性:有缓存,依赖不变不会重新执行 -->
  <p>computed: {{ reversedMessage }}</p>
  <p>computed: {{ reversedMessage }}</p>  <!-- 读缓存,不重新计算 -->

  <!-- 方法:每次渲染都会重新执行 -->
  <p>method: {{ reverseMessage() }}</p>
  <p>method: {{ reverseMessage() }}</p>  <!-- 重新执行函数 -->
</template>

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

const message = ref('Hello Vue')

// computed:缓存结果,依赖不变时直接返回缓存
const reversedMessage = computed(() => {
  console.log('computed 执行')
  return message.value.split('').reverse().join('')
})

// method:每次调用都执行
const reverseMessage = () => {
  console.log('method 执行')
  return message.value.split('').reverse().join('')
}
</script>
对比项 computed method
缓存 ✅ 有缓存,依赖不变不重新计算 ❌ 每次调用都执行
调用方式 属性访问 {{ xxx }} 函数调用 {{ xxx() }}
适用场景 基于现有数据派生值 事件处理、需要传参的逻辑

# 3.2.4 综合案例与思考

综合案例:购物车价格计算器

<template>
  <div class="cart">
    <h3>购物车</h3>
    <div v-for="item in cart" :key="item.id" class="item">
      <span>{{ item.name }}</span>
      <span>¥{{ item.price }}</span>
      <input v-model.number="item.quantity" type="number" min="1" max="99" />
      <span>小计: ¥{{ (item.price * item.quantity).toFixed(2) }}</span>
    </div>

    <div class="summary">
      <p>商品数量: {{ totalCount }} 件</p>
      <p>商品总价: ¥{{ subtotal }}</p>
      <p>
        优惠券:
        <input v-model.number="coupon" type="number" min="0" placeholder="输入优惠金额" />
      </p>
      <p>运费: ¥{{ shipping }}(满99免运费)</p>
      <p class="total">应付金额: ¥{{ finalPrice }}</p>
    </div>
  </div>
</template>

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

const cart = ref([
  { id: 1, name: 'Vue实战', price: 99, quantity: 1 },
  { id: 2, name: 'TS手册', price: 59, quantity: 2 },
  { id: 3, name: 'Vite指南', price: 39, quantity: 1 }
])

const coupon = ref(0)

// 计算属性链式派生
const totalCount = computed(() =>
  cart.value.reduce((sum, item) => sum + item.quantity, 0)
)

const subtotal = computed(() =>
  cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0).toFixed(2)
)

const shipping = computed(() =>
  Number(subtotal.value) >= 99 ? 0 : 10
)

const finalPrice = computed(() => {
  const total = Number(subtotal.value) + shipping.value - coupon.value
  return Math.max(0, total).toFixed(2)
})
</script>

<style scoped>
.cart { max-width: 500px; }
.item { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid #eee; }
.item input { width: 60px; }
.summary { margin-top: 16px; padding: 16px; background: #f8f8f8; border-radius: 8px; }
.total { font-size: 20px; color: #ff4444; font-weight: bold; }
</style>

思考题:

  1. 计算属性的缓存机制是如何实现的?它怎么知道依赖变了?
  2. 如果一个计算属性依赖另一个计算属性,会形成链式计算,这样性能好吗?
  3. 什么场景下应该用 method 而不是 computed?

# 3.3 侦听器watch

# 3.3.1 watch基本用法

watch 在响应式数据变化时执行副作用(API请求、DOM操作等):

<template>
  <input v-model="searchText" placeholder="搜索..." />
  <p>搜索: {{ searchText }}</p>
  <p>结果: {{ result }}</p>
</template>

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

const searchText = ref('')
const result = ref('')

// 监听 ref
watch(searchText, (newVal, oldVal) => {
  console.log(`搜索词变化: ${oldVal} → ${newVal}`)
  // 模拟 API 搜索
  result.value = `正在搜索 "${newVal}"...`
})

// 监听 reactive 对象的某个属性(使用 getter 函数)
import { reactive } from 'vue'
const state = reactive({ count: 0 })
watch(
  () => state.count,           // getter 函数
  (newVal) => {
    console.log('count 变为:', newVal)
  }
)
</script>

# 3.3.2 监听多个源

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

const firstName = ref('张')
const lastName = ref('三')

// 同时监听多个源
watch(
  [firstName, lastName],
  ([newFirst, newLast], [oldFirst, oldLast]) => {
    console.log(`姓名变化: ${oldFirst}${oldLast} → ${newFirst}${newLast}`)
  }
)
</script>

# 3.3.3 deep和immediate

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

const userInfo = ref({
  name: '张三',
  address: { city: '深圳', district: '南山' }
})

// deep: 深度监听嵌套对象的变化
watch(userInfo, (newVal) => {
  console.log('用户信息变化:', JSON.stringify(newVal))
}, { deep: true })

// immediate: 创建时立即执行一次回调
const page = ref(1)
watch(page, (newPage) => {
  console.log('加载第', newPage, '页数据')
  // fetchData(newPage)
}, { immediate: true })  // 立即执行一次,加载第1页

// once: 只触发一次(Vue 3.4+)
const token = ref('')
watch(token, (newToken) => {
  console.log('Token 已获取:', newToken)
}, { once: true })
</script>

# 3.3.4 watchEffect

watchEffect 自动追踪依赖,无需手动指定监听源:

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

const count = ref(0)
const name = ref('Vue')

// watchEffect 自动收集依赖
// 内部用到的响应式数据都会被自动追踪
watchEffect(() => {
  console.log(`count=${count.value}, name=${name.value}`)
  // count 或 name 任一变化,都会重新执行
})

// 常见用途:自动请求数据
const page = ref(1)
const pageSize = ref(10)
const data = ref<any[]>([])

watchEffect(async () => {
  // page 或 pageSize 变化时自动重新请求
  const response = await fetch(`/api/list?page=${page.value}&size=${pageSize.value}`)
  data.value = await response.json()
})
</script>

watch 和 watchEffect 对比:

对比项 watch watchEffect
依赖追踪 手动指定监听源 自动追踪
旧值访问 ✅ 可以获取 oldVal ❌ 无法获取
首次执行 默认不执行(需 immediate) 默认立即执行
适用场景 需要旧值对比、条件判断 依赖多个源、自动同步

# 3.3.5 停止侦听器

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

const count = ref(0)

// watch 和 watchEffect 返回停止函数
const stopWatch = watch(count, (val) => {
  console.log('count:', val)
  if (val >= 10) {
    stopWatch()  // count >= 10 时停止监听
    console.log('已停止监听')
  }
})

const stopEffect = watchEffect(() => {
  console.log('effect:', count.value)
})

// 在需要的时候调用 stopEffect() 停止
</script>

# 3.3.6 综合案例与思考

综合案例:实时搜索与防抖

<template>
  <div class="search-app">
    <input v-model="keyword" placeholder="搜索用户..." />
    <select v-model="sortBy">
      <option value="name">按姓名</option>
      <option value="age">按年龄</option>
    </select>

    <p v-if="loading" class="loading">搜索中...</p>
    <ul v-else>
      <li v-for="user in results" :key="user.id">
        {{ user.name }} - {{ user.age }}岁 - {{ user.city }}
      </li>
      <li v-if="results.length === 0">无结果</li>
    </ul>
    <p class="info">搜索次数: {{ searchCount }}</p>
  </div>
</template>

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

const keyword = ref('')
const sortBy = ref('name')
const results = ref<any[]>([])
const loading = ref(false)
const searchCount = ref(0)

// 模拟用户数据
const allUsers = [
  { id: 1, name: '张三', age: 28, city: '深圳' },
  { id: 2, name: '李四', age: 32, city: '北京' },
  { id: 3, name: '王五', age: 25, city: '上海' },
  { id: 4, name: '赵六', age: 35, city: '广州' },
  { id: 5, name: '张伟', age: 29, city: '深圳' }
]

// watch + 防抖:关键词变化时延迟搜索
let timer: number
watch(keyword, (newKeyword) => {
  clearTimeout(timer)
  if (!newKeyword.trim()) {
    results.value = []
    return
  }
  loading.value = true
  timer = setTimeout(() => {
    searchCount.value++
    results.value = allUsers.filter(u =>
      u.name.includes(newKeyword) || u.city.includes(newKeyword)
    )
    loading.value = false
  }, 500)  // 500ms 防抖
})

// watchEffect:排序方式变化时自动重排
watchEffect(() => {
  if (results.value.length > 0) {
    results.value.sort((a, b) => {
      if (sortBy.value === 'age') return a.age - b.age
      return a.name.localeCompare(b.name)
    })
  }
})
</script>

<style scoped>
.search-app { max-width: 400px; }
.search-app input, .search-app select { padding: 8px; margin: 4px; }
.loading { color: #999; }
.info { font-size: 13px; color: #999; }
</style>

思考题:

  1. 为什么搜索功能用 watch 而排序用 watchEffect?能否互换?
  2. 防抖(debounce)在 watch 中的实现方式是怎样的?还有没有更优雅的方案?
  3. watch 的 deep: true 会监听对象所有嵌套属性的变化,这对性能有什么影响?

# 3.4 响应式工具函数

# 3.4.1 toRef和toRefs

将 reactive 对象的属性转为独立的 ref,保持响应式连接:

<script setup lang="ts">
import { reactive, toRef, toRefs } from 'vue'

const state = reactive({
  name: '张三',
  age: 28,
  city: '深圳'
})

// toRef:提取单个属性为 ref
const nameRef = toRef(state, 'name')
nameRef.value = '李四'  // 同步修改 state.name

// toRefs:将所有属性转为 ref
const { name, age, city } = toRefs(state)
// name.value、age.value、city.value 都保持与 state 的连接

// 常见用途:在 composable 函数中返回
function useUser() {
  const user = reactive({ name: '张三', age: 28 })
  return toRefs(user)  // 解构后仍保持响应式
}

// 使用时可以直接解构
const { name: userName, age: userAge } = useUser()
</script>

# 3.4.2 shallowRef和shallowReactive

浅层响应式,只追踪第一层属性的变化:

<script setup lang="ts">
import { shallowRef, shallowReactive, triggerRef } from 'vue'

// shallowRef:只有 .value 整体替换时触发更新
const state = shallowRef({ count: 0, nested: { value: 1 } })
state.value.count = 1          // ❌ 不触发更新
state.value = { count: 1, nested: { value: 1 } }  // ✅ 整体替换才触发

// 手动触发更新
state.value.count = 2
triggerRef(state)  // 强制触发更新

// shallowReactive:只有第一层属性是响应式的
const obj = shallowReactive({
  name: '张三',                 // ✅ 响应式
  address: { city: '深圳' }    // ❌ 嵌套对象不是响应式
})
obj.name = '李四'               // ✅ 触发更新
obj.address.city = '北京'       // ❌ 不触发更新

使用场景:大型数据结构(如长列表、树形数据)只需要追踪顶层变化时,用浅层响应式可以提升性能。

# 3.4.3 readonly只读代理

创建响应式数据的只读副本:

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

const count = ref(0)
const readonlyCount = readonly(count)
// readonlyCount.value++  // ❌ 编译报错 + 运行时警告

const state = reactive({ name: '张三', age: 28 })
const readonlyState = readonly(state)
// readonlyState.name = '李四'  // ❌ 不可修改

// 典型用途:向子组件传递只读数据
// provide('config', readonly(appConfig))
</script>

# 3.4.4 综合案例与思考

综合案例:可组合函数中的响应式工具

<template>
  <div>
    <h3>鼠标位置追踪</h3>
    <p>X: {{ x }}, Y: {{ y }}</p>
    <p>在线状态: {{ isOnline ? '在线' : '离线' }}</p>
    <p>窗口宽度: {{ width }}px</p>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, toRefs, readonly, onMounted, onBeforeUnmount } from 'vue'

// 可组合函数:追踪鼠标位置
function useMouse() {
  const position = reactive({ x: 0, y: 0 })

  const update = (e: MouseEvent) => {
    position.x = e.clientX
    position.y = e.clientY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onBeforeUnmount(() => window.removeEventListener('mousemove', update))

  // 使用 toRefs 返回,让调用者可以解构
  return toRefs(readonly(position))
}

// 可组合函数:网络状态
function useOnline() {
  const isOnline = ref(navigator.onLine)
  const handler = () => { isOnline.value = navigator.onLine }

  onMounted(() => {
    window.addEventListener('online', handler)
    window.addEventListener('offline', handler)
  })
  onBeforeUnmount(() => {
    window.removeEventListener('online', handler)
    window.removeEventListener('offline', handler)
  })

  return { isOnline: readonly(isOnline) }
}

// 可组合函数:窗口尺寸
function useWindowSize() {
  const width = ref(window.innerWidth)
  const handler = () => { width.value = window.innerWidth }

  onMounted(() => window.addEventListener('resize', handler))
  onBeforeUnmount(() => window.removeEventListener('resize', handler))

  return { width: readonly(width) }
}

// 使用
const { x, y } = useMouse()
const { isOnline } = useOnline()
const { width } = useWindowSize()
</script>

思考题:

  1. toRefs 和直接返回 reactive 对象有什么区别?为什么 composable 函数建议用 toRefs?
  2. shallowRef 和 ref 在大数据场景下性能差异有多大?什么时候该用?
  3. 为什么 composable 函数返回时建议加 readonly?这是一种什么设计模式?

# 3.5 响应式原理

# 3.5.1 Proxy代理机制

Vue 3 使用 ES6 Proxy 实现响应式,拦截对象的读取和修改操作:

// 简化版响应式原理
function reactive(target) {
  return new Proxy(target, {
    get(obj, key, receiver) {
      // 读取属性时:收集依赖(track)
      track(obj, key)
      const result = Reflect.get(obj, key, receiver)
      // 如果属性值也是对象,递归代理
      if (typeof result === 'object' && result !== null) {
        return reactive(result)
      }
      return result
    },
    set(obj, key, value, receiver) {
      const oldValue = obj[key]
      const result = Reflect.set(obj, key, value, receiver)
      // 修改属性时:触发更新(trigger)
      if (oldValue !== value) {
        trigger(obj, key)
      }
      return result
    }
  })
}

# 3.5.2 依赖收集与触发

响应式工作流程:

1. 组件渲染时读取数据 → 触发 Proxy 的 get → 收集依赖
2. 数据修改 → 触发 Proxy 的 set → 通知所有依赖更新

┌──────────┐    get(track)    ┌──────────────┐
│ 组件渲染  │ ───────────────→│  响应式数据   │
│(读取数据) │                 │  (Proxy代理)  │
└──────────┘                 └──────────────┘
     ↑                              │
     │          set(trigger)        │
     └──────────────────────────────┘
              数据变化→重新渲染
// 依赖收集的简化实现
let activeEffect = null    // 当前正在执行的副作用函数
const targetMap = new WeakMap()  // 存储所有依赖关系

function track(target, key) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  let deps = depsMap.get(key)
  if (!deps) {
    deps = new Set()
    depsMap.set(key, deps)
  }
  deps.add(activeEffect)  // 将当前副作用添加到依赖集合
}

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const deps = depsMap.get(key)
  if (deps) {
    deps.forEach(effect => effect())  // 执行所有依赖的副作用
  }
}

# 3.5.3 与Vue2的区别

对比项 Vue 2 (Object.defineProperty) Vue 3 (Proxy)
新增属性 ❌ 无法检测(需要 Vue.set) ✅ 自动检测
删除属性 ❌ 无法检测(需要 Vue.delete) ✅ 自动检测
数组索引修改 ❌ 无法检测 ✅ 自动检测
数组长度修改 ❌ 无法检测 ✅ 自动检测
初始化性能 递归遍历所有属性,初始化慢 惰性代理,按需递归,初始化快
浏览器支持 IE9+ 不支持 IE
// Vue 2 的痛点
const vm = new Vue({
  data: { obj: { a: 1 } }
})
vm.obj.b = 2          // ❌ 新增属性,不触发更新
Vue.set(vm.obj, 'b', 2)  // ✅ 需要这样写

// Vue 3 完全没有这些问题
const obj = reactive({ a: 1 })
obj.b = 2              // ✅ 直接新增,自动响应
delete obj.a           // ✅ 直接删除,自动响应

# 3.5.4 综合案例与思考

综合案例:手写简易响应式系统

// 一个可运行的简易响应式系统,帮助理解Vue3响应式原理

type EffectFn = () => void

let activeEffect: EffectFn | null = null
const targetMap = new WeakMap<object, Map<string | symbol, Set<EffectFn>>>()

// 依赖收集
function track(target: object, key: string | symbol) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) targetMap.set(target, (depsMap = new Map()))
  let deps = depsMap.get(key)
  if (!deps) depsMap.set(key, (deps = new Set()))
  deps.add(activeEffect)
}

// 触发更新
function trigger(target: object, key: string | symbol) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const deps = depsMap.get(key)
  deps?.forEach(fn => fn())
}

// 创建响应式对象
function myReactive<T extends object>(target: T): T {
  return new Proxy(target, {
    get(obj, key, receiver) {
      track(obj, key)
      return Reflect.get(obj, key, receiver)
    },
    set(obj, key, value, receiver) {
      const result = Reflect.set(obj, key, value, receiver)
      trigger(obj, key)
      return result
    }
  })
}

// 创建副作用
function myEffect(fn: EffectFn) {
  activeEffect = fn
  fn()  // 执行一次,触发 get,收集依赖
  activeEffect = null
}

// 使用示例
const state = myReactive({ count: 0, message: 'hello' })

myEffect(() => {
  console.log('count 变化:', state.count)
})

myEffect(() => {
  console.log('message 变化:', state.message)
})

state.count = 1      // 输出: count 变化: 1
state.count = 2      // 输出: count 变化: 2
state.message = 'hi' // 输出: message 变化: hi
// 修改 count 不会触发 message 的副作用,精确追踪

思考题:

  1. 为什么 Vue 3 选择 Proxy 而不继续使用 Object.defineProperty?除了功能差异,还有什么考量?
  2. WeakMap 在依赖收集中起什么作用?为什么不用普通 Map?
  3. 为什么 ref 在 JS 中需要 .value,而在模板中不需要?Vue 是如何实现模板自动解包的?
上次更新: 2026/06/28, 17:55:19
Vue组件开发实战
Vue路由管理实战

← Vue组件开发实战 Vue路由管理实战→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式