Vue响应式原理
# 03.Vue响应式原理
# 目录介绍
# 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>
思考题:
ref(0)返回的是什么类型的对象?为什么需要通过.value访问?reactive的对象被整体替换后为什么会丢失响应式?底层原因是什么?- 什么场景下适合用
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>
思考题:
- 计算属性的缓存机制是如何实现的?它怎么知道依赖变了?
- 如果一个计算属性依赖另一个计算属性,会形成链式计算,这样性能好吗?
- 什么场景下应该用 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>
思考题:
- 为什么搜索功能用
watch而排序用watchEffect?能否互换? - 防抖(debounce)在 watch 中的实现方式是怎样的?还有没有更优雅的方案?
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>
思考题:
toRefs和直接返回reactive对象有什么区别?为什么 composable 函数建议用toRefs?shallowRef和ref在大数据场景下性能差异有多大?什么时候该用?- 为什么 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 的副作用,精确追踪
思考题:
- 为什么 Vue 3 选择 Proxy 而不继续使用 Object.defineProperty?除了功能差异,还有什么考量?
WeakMap在依赖收集中起什么作用?为什么不用普通 Map?- 为什么 ref 在 JS 中需要
.value,而在模板中不需要?Vue 是如何实现模板自动解包的?
上次更新: 2026/06/28, 17:55:19