跳到主要内容

Vue 响应式原理

理解 Vue 的响应式系统对于深入掌握 Vue 至关重要。本章将详细介绍 Vue 3 响应式系统的工作原理,帮助你更好地理解和使用响应式 API。

为什么需要响应式?

在传统的 JavaScript 中,我们无法自动检测到普通变量的访问或修改。考虑以下代码:

let count = 0
console.log(count) // 输出 0,但没有任何机制可以"追踪"这次访问
count = 1 // 修改了值,但没有机制可以"通知"依赖这个值的地方

Vue 的响应式系统解决了这个问题:它能够追踪数据的读取,并在数据修改时通知所有依赖它的地方进行更新。

Vue 的解决方案

Vue 使用两种技术来实现响应式:

  1. 依赖追踪:当组件渲染时,记录所有被使用的响应式数据
  2. 触发更新:当响应式数据被修改时,通知所有依赖该数据的组件重新渲染

响应式基础

什么是响应式?

响应式是指当数据变化时,依赖这些数据的视图会自动更新。这是一个声明式的编程范式——你只需要声明数据和视图的关系,Vue 会自动处理同步:

import { ref } from 'vue'

// 创建响应式数据
const count = ref(0)

// 当 count 变化时,所有使用它的地方都会自动更新
count.value = 1

Vue 2 vs Vue 3 对比

特性Vue 2Vue 3
实现方式Object.definePropertyProxy
添加属性需要 Vue.set直接支持
删除属性需要 Vue.delete直接支持
数组索引需要 Vue.set直接支持
数组长度不能监听直接支持
Map/Set不支持直接支持
性能一般显著提升

Vue 3 使用 Proxy 的优势

  • 可以拦截对象的所有操作,而不仅是属性读写
  • 可以监听动态添加的属性
  • 可以监听数组索引和长度变化
  • 支持原生集合类型(Map、Set、WeakMap、WeakSet)
  • 性能更好,惰性响应式(嵌套对象只有被访问时才转为响应式)

Vue 3 响应式原理详解

Proxy 代理

Proxy 是 ES6 提供的元编程特性,可以拦截对象的各种操作:

const original = { count: 0 }

// 创建代理对象
const proxy = new Proxy(original, {
// 拦截属性读取
get(target, key, receiver) {
console.log(`读取属性: ${key}`)
return Reflect.get(target, key, receiver)
},

// 拦截属性设置
set(target, key, value, receiver) {
console.log(`设置属性: ${key} = ${value}`)
return Reflect.set(target, key, value, receiver)
},

// 拦截属性删除
deleteProperty(target, key) {
console.log(`删除属性: ${key}`)
return Reflect.deleteProperty(target, key)
}
})

proxy.count // 输出: 读取属性: count
proxy.count = 1 // 输出: 设置属性: count = 1
delete proxy.count // 输出: 删除属性: count

Reflect 的作用:Reflect 提供了与 Proxy 拦截器对应的方法,用于执行对象的默认行为。使用 Reflect 可以确保 this 指向正确,并保持默认行为的一致性。

reactive() 的实现原理

Vue 3 的 reactive() 函数使用 Proxy 创建响应式对象:

// 简化版实现
const reactiveMap = new WeakMap()

function reactive(target) {
// 如果已经是响应式对象,直接返回
if (reactiveMap.has(target)) {
return reactiveMap.get(target)
}

// 创建 Proxy
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 依赖收集:记录"谁在读取这个属性"
track(target, key)

// 获取值
const result = Reflect.get(target, key, receiver)

// 如果是对象,递归创建响应式(惰性响应式)
if (result !== null && typeof result === 'object') {
return reactive(result)
}

return result
},

set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)

// 只有值真正变化时才触发更新
if (oldValue !== value) {
// 触发更新:通知所有依赖这个属性的地方
trigger(target, key)
}

return result
},

deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
trigger(target, key)
return result
}
})

// 缓存代理对象
reactiveMap.set(target, proxy)
return proxy
}

ref() 的实现原理

ref() 用于创建任意类型的响应式数据,包括基本类型:

// 简化版实现
function ref(value) {
// 如果已经是 ref,直接返回
if (value.__v_isRef) {
return value
}

return {
__v_isRef: true, // 标记为 ref
_rawValue: value, // 存储原始值
_value: value, // 存储响应式值

get value() {
// 依赖收集
track(this, 'value')
return this._value
},

set value(newValue) {
// 只有值真正变化时才触发更新
if (newValue !== this._rawValue) {
this._rawValue = newValue
// 如果新值是对象,转为响应式
this._value = isObject(newValue) ? reactive(newValue) : newValue
// 触发更新
trigger(this, 'value')
}
}
}
}

为什么 ref 需要 .value?

JavaScript 中没有一种机制可以拦截基本类型(如数字、字符串)的访问和修改。通过将基本类型包装在一个对象中,使用 .value 属性进行读写,Vue 就可以在 getter 和 setter 中执行依赖追踪和触发更新:

// 问题:无法拦截基本类型的修改
let count = 0
count = 1 // 没有办法知道 count 被修改了

// 解决方案:包装成对象
const count = ref(0)
count.value = 1 // 可以在 setter 中拦截

依赖追踪系统

核心概念

Vue 的响应式系统基于三个核心操作:

  1. track(追踪):记录"谁在读取这个响应式数据"
  2. trigger(触发):通知"这个响应式数据被修改了"
  3. effect(副作用):响应式数据变化时要执行的代码

依赖收集实现

// 当前正在执行的副作用函数
let activeEffect = null

// 存储所有依赖关系
// targetMap: WeakMap<target, Map<key, Set<effect>>>
const targetMap = new WeakMap()

// 追踪依赖
function track(target, key) {
if (!activeEffect) return

// 获取或创建 target 的依赖映射
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}

// 获取或创建 key 的副作用集合
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}

// 将当前副作用添加到集合
dep.add(activeEffect)
}

// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return

const effects = depsMap.get(key)
if (effects) {
// 执行所有依赖这个属性的副作用
effects.forEach(effect => effect())
}
}

// 创建副作用
function effect(fn) {
activeEffect = fn
fn() // 执行时会触发 track
activeEffect = null
}

依赖追踪流程

┌─────────────────────────────────────────────────────────────────┐
│ 依赖追踪流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 组件渲染 │
│ │ │
│ ▼ │
│ 2. effect(组件渲染函数) │
│ │ │
│ ▼ │
│ 3. 读取响应式数据 → track(target, key) │
│ │ │
│ ▼ │
│ 4. 记录依赖关系:target.key → [effect] │
│ │ │
│ ▼ │
│ 5. 修改响应式数据 → trigger(target, key) │
│ │ │
│ ▼ │
│ 6. 执行所有依赖的 effect → 组件重新渲染 │
│ │
└─────────────────────────────────────────────────────────────────┘

响应式对象的特性

Reactive Proxy vs 原始对象

reactive() 返回的是原始对象的 Proxy,两者不相等:

import { reactive } from 'vue'

const raw = { count: 0 }
const proxy = reactive(raw)

console.log(proxy === raw) // false
console.log(proxy.count) // 0(通过 Proxy 访问)

重要原则:只使用 Proxy 版本的对象,不要保留对原始对象的引用。修改原始对象不会触发更新:

// 错误:修改原始对象不会触发更新
raw.count = 1 // 不会触发更新!

// 正确:修改 Proxy 对象
proxy.count = 1 // 会触发更新

Proxy 的一致性

对同一个对象多次调用 reactive() 会返回同一个 Proxy:

const raw = { count: 0 }
const proxy1 = reactive(raw)
const proxy2 = reactive(raw)

console.log(proxy1 === proxy2) // true

// 对 Proxy 调用 reactive 返回自身
console.log(reactive(proxy1) === proxy1) // true

深层响应式

reactive() 会将嵌套对象也转为响应式:

import { reactive } from 'vue'

const state = reactive({
user: {
name: '张三',
address: {
city: '北京'
}
}
})

// 深层修改也是响应式的
state.user.address.city = '上海' // 触发更新

这是通过"惰性响应式"实现的:只有访问嵌套对象时,才会将其转为响应式。这种设计比 Vue 2 的递归转换性能更好。

ref 的解包行为

在 reactive 对象中自动解包

当 ref 作为 reactive 对象的属性时,会自动解包:

import { ref, reactive } from 'vue'

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

// 自动解包,不需要 .value
console.log(state.count) // 0

// 修改时也自动解包
state.count = 1
console.log(count.value) // 1

// 赋值新的 ref 会替换旧 ref
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
console.log(count.value) // 1(旧 ref 不再关联)

数组和集合类型中的 ref 不会解包

在数组、Map、Set 等集合类型中,ref 不会自动解包:

import { ref, reactive } from 'vue'

// 数组中的 ref
const books = reactive([ref('Vue 3 Guide')])
console.log(books[0].value) // 需要 .value

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

模板中的自动解包

在模板中使用 ref 时会自动解包,但有一个重要的限制:只有顶层的 ref 属性才会解包

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

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

<template>
<!-- ✅ 正确:顶层 ref 自动解包 -->
<p>{{ count }}</p>
<p>{{ count + 1 }}</p>

<!-- ❌ 错误:嵌套 ref 不会解包 -->
<p>{{ object.id + 1 }}</p>
<!-- 结果: "[object Object]1" -->

<!-- ✅ 正确:解构到顶层 -->
<p>{{ object.id }}</p>
<!-- 结果: 1(文本插值的特殊处理) -->
</template>

解决方案:解构到顶层:

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

const object = { id: ref(1) }
const { id } = object // 解构到顶层
</script>

<template>
<p>{{ id + 1 }}</p> <!-- 2 -->
</template>

DOM 更新时机

异步更新策略

Vue 的 DOM 更新是异步的。当响应式数据变化时,Vue 不会立即更新 DOM,而是将这些更新缓存在一个队列中,在"下一个 tick"时批量执行:

import { ref } from 'vue'

const count = ref(0)
count.value++
count.value++
count.value++

// DOM 还没有更新
console.log(document.querySelector('#count').textContent) // 0

// 多次修改只会触发一次 DOM 更新

这种策略确保了无论修改了多少次数据,组件只会更新一次,提高了性能。

nextTick

使用 nextTick() 等待 DOM 更新完成:

import { ref, nextTick } from 'vue'

const count = ref(0)

async function increment() {
count.value++

// DOM 还没更新
console.log(document.querySelector('#count').textContent) // 旧值

// 等待 DOM 更新
await nextTick()

// DOM 已更新
console.log(document.querySelector('#count').textContent) // 新值
}

实际应用场景

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

const show = ref(false)
const inputRef = ref(null)

async function showAndFocus() {
show.value = true

// DOM 还没更新,input 不存在
// inputRef.value 是 null

await nextTick()

// DOM 已更新,可以安全地操作 input
inputRef.value?.focus()
}
</script>

<template>
<input v-if="show" ref="inputRef" />
<button @click="showAndFocus">显示并聚焦</button>
</template>

Computed 原理

计算属性的实现

计算属性基于"惰性求值"和"缓存":

function computed(getter) {
let value
let dirty = true // 脏标记:是否需要重新计算

// 创建一个副作用,用于追踪依赖
const effect = () => {
value = getter()
dirty = false
}

return {
get value() {
if (dirty) {
// 执行计算,追踪依赖
activeEffect = effect
value = getter()
activeEffect = null
dirty = false
}
return value
}
}
}

缓存机制

计算属性只有在其依赖发生变化时才会重新计算:

import { ref, computed } from 'vue'

const count = ref(1)
const doubled = computed(() => {
console.log('计算中...') // 只在需要时输出
return count.value * 2
})

// 首次访问,执行计算
console.log(doubled.value) // 输出: 计算中... 然后输出: 2

// 再次访问,使用缓存
console.log(doubled.value) // 直接输出: 2(不重新计算)

// 依赖变化,标记为脏
count.value = 2

// 重新计算
console.log(doubled.value) // 输出: 计算中... 然后输出: 4

Watch 原理

watchEffect 的实现

watchEffect 立即执行回调,并自动追踪其中使用的所有响应式数据:

function watchEffect(effect) {
// 创建一个包装函数
const wrappedEffect = () => {
activeEffect = wrappedEffect
effect() // 执行时会触发 track
activeEffect = null
}

// 立即执行一次
wrappedEffect()

// 返回停止函数
return () => {
// 清理所有依赖
cleanup(wrappedEffect)
}
}

watch 的实现

watch 允许指定要监听的源,并获取新旧值:

function watch(source, callback, options = {}) {
let getter
let oldValue

// 确定如何获取值
if (typeof source === 'function') {
getter = source
} else if (isRef(source)) {
getter = () => source.value
} else if (isReactive(source)) {
getter = () => traverse(source)
}

// 创建副作用
const effect = () => {
const newValue = getter()
if (newValue !== oldValue || options.deep) {
callback(newValue, oldValue)
oldValue = newValue
}
}

// 立即执行(immediate 选项)
if (options.immediate) {
effect()
} else {
oldValue = getter()
}

// 追踪依赖
trackEffect(effect)

// 返回停止函数
return () => stopEffect(effect)
}

// 深度遍历以触发所有属性的追踪
function traverse(value, seen = new Set()) {
if (typeof value !== 'object' || seen.has(value)) return value
seen.add(value)

for (const key in value) {
traverse(value[key], seen)
}

return value
}

shallowRef 和 shallowReactive

浅层响应式

默认情况下,refreactive 都是深层响应式。如果只需要追踪顶层变化,可以使用浅层版本:

import { shallowRef, shallowReactive } from 'vue'

// shallowRef:只有 .value 的变化是响应式的
const state = shallowRef({
nested: { count: 0 }
})

state.value = { nested: { count: 1 } } // ✅ 触发更新
state.value.nested.count = 2 // ❌ 不触发更新

// shallowReactive:只有顶层属性变化是响应式的
const shallow = shallowReactive({
count: 0,
nested: { value: 1 }
})

shallow.count = 1 // ✅ 触发更新
shallow.nested.value = 2 // ❌ 不触发更新

使用场景

import { shallowRef, triggerRef } from 'vue'

// 大型数据结构,避免深层响应式开销
const bigData = shallowRef({
// 大量嵌套数据...
})

// 修改深层数据后手动触发更新
bigData.value.nested.deepValue = 'new'
triggerRef(bigData) // 手动触发更新

// 与外部状态管理系统集成
const externalState = shallowRef(null)
externalState.value = thirdPartyLib.getState()

响应式丢失问题

常见陷阱

import { reactive, toRefs } from 'vue'

const state = reactive({
name: '张三',
age: 25
})

// ❌ 错误:解构会丢失响应式
let { name, age } = state
name = '李四' // 不会触发更新,只是修改了局部变量

// ✅ 正确:使用 toRefs 保持响应式
const { name, age } = toRefs(state)
name.value = '李四' // 会触发更新

toRefs 的原理

function toRefs(object) {
const result = {}

for (const key in object) {
result[key] = toRef(object, key)
}

return result
}

function toRef(object, key) {
return {
get value() {
return object[key]
},
set value(newValue) {
object[key] = newValue
}
}
}

toRefs 创建的 ref 与原对象保持同步:修改任一方都会影响另一方。

性能优化建议

避免不必要的响应式

import { ref, reactive, shallowRef, markRaw } from 'vue'

// 大型不可变数据使用 shallowRef
const bigList = shallowRef([])

// 确实不需要响应式的对象使用 markRaw
const staticConfig = markRaw({
// 大量静态配置...
})

// 第三方库实例通常不需要响应式
const chartInstance = markRaw(new Chart(...))

合理使用计算属性

import { ref, computed } from 'vue'

const items = ref([...])

// ✅ 好:计算属性有缓存
const filteredItems = computed(() =>
items.value.filter(item => item.active)
)

// ❌ 差:方法每次都重新计算
function getFilteredItems() {
return items.value.filter(item => item.active)
}

避免在 watchEffect 中修改依赖

import { ref, watchEffect, watch } from 'vue'

const count = ref(0)

// ❌ 危险:可能导致无限循环
watchEffect(() => {
count.value = count.value + 1
})

// ✅ 正确:使用 watch 并添加条件
watch(count, (newValue) => {
if (newValue < 10) {
count.value = newValue + 1
}
})

小结

本章我们深入学习了 Vue 响应式原理:

  1. 响应式概念:数据变化自动更新视图的机制
  2. Proxy 代理:Vue 3 响应式系统的基础
  3. reactive 实现:对象响应式代理的创建
  4. ref 实现:基本类型和对象类型的响应式包装
  5. 依赖追踪:track 和 trigger 的协作机制
  6. ref 解包规则:reactive 对象中、数组/集合中、模板中的不同行为
  7. DOM 更新时机:异步更新策略和 nextTick
  8. Computed 原理:惰性求值和缓存机制
  9. Watch 原理:监听数据变化的不同方式
  10. 浅层响应式:shallowRef 和 shallowReactive
  11. 性能优化:避免不必要的响应式开销

练习

  1. 手写一个简化版的 reactive() 函数
  2. 手写一个简化版的 ref() 函数
  3. 分析 computedwatchEffect 的依赖追踪过程
  4. 实现 toRefs() 函数

延伸阅读