Vue 3 响应式原理:从零到一,触及灵魂

Vue 3 响应式原理:从零到一,触及灵魂

这是一篇从零开始的 Vue 3 响应式原理深度剖析。不假设你有任何前置知识,我会带你一步步推导出整个响应式系统的设计,让你真正理解"为什么"而不仅仅是"是什么"。


写在前面:为什么要理解响应式原理?

如果你只用过 Vue 写业务代码,你可能已经习惯了这种写法:

const count = ref(0)
count.value = 1  // 界面自动更新

但你有没想过:为什么赋值操作能触发界面更新?JavaScript 本身并没有这个能力。

这就是响应式系统的使命:在普通的 JavaScript 赋值操作之上,注入"通知机制"

读完这篇文章,你将能够回答:

  • 响应式系统的核心三要素是什么?
  • 为什么 Vue 3 要用 Proxy 而不是 Object.defineProperty?
  • 依赖收集到底是在收集什么?
  • 为什么修改数据后界面不会立即更新?
  • computed 的缓存是怎么实现的?
  • 响应式数据在组件从创建到销毁的整个生命周期中经历了什么?

第一章:问题的起点——如何让一段代码"记住"另一段代码?

1.1 一个最简单的需求

假设我们有这样一段代码:

let a = 1
let b = 2
let c = a + b  // c = 3

a = 2
// 现在 c 还是 3,但我们希望 c 自动变成 4

我们想要的效果是:当 a 或 b 变化时,所有依赖它们的表达式自动重新计算

这就是响应式最朴素的需求。

1.2 从"手动通知"到"自动追踪"

先看一个最原始的实现思路:

let a = 1
let b = 2
let c = 3

// 我们需要在 a 变化时做点什么
function setA(newValue) {
  a = newValue
  c = a + b  // 手动更新依赖 a 的表达式
}

这显然不可扩展。我们需要一种机制:让变量"知道"谁在依赖它

这就是响应式系统的核心矛盾:

  • 读取时:需要记住"谁在读我"
  • 写入时:需要通知"所有在读我的人"

翻译成技术术语:

  • 依赖收集(track):在读取时记录依赖关系
  • 派发更新(trigger):在写入时触发更新

1.3 响应式系统的"黄金三角"

任何一个响应式系统,都必须解决三个问题:

问题技术术语通俗解释
什么时候收集依赖?track 时机在读取数据时,记录当前是谁在读
收集什么?依赖的数据结构记录"哪个数据"被"哪个函数"依赖
什么时候触发更新?trigger 时机数据变化时,找到所有依赖函数并执行

这三个问题贯穿整篇文章,请带着它们往下读。


第二章:JavaScript 的局限性——为什么需要 Proxy?

2.1 我们想要的能力

要实现"读取时收集、写入时触发",我们需要在 JavaScript 的属性读取和写入操作上插入拦截代码

// 我们想要这样的效果
const data = { count: 0 }

// 当读取 data.count 时,执行 track()
// 当写入 data.count 时,执行 trigger()

data.count  // 触发 track
data.count = 1  // 触发 trigger

但 JavaScript 原生并没有提供这种拦截能力。我们需要一种方式来"劫持"对象的读写操作。

2.2 Vue 2 的方案:Object.defineProperty

Vue 2 使用 Object.defineProperty 来拦截属性的读写:

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log('收集依赖', key)
      return val
    },
    set(newVal) {
      console.log('触发更新', key)
      val = newVal
    }
  })
}

const data = { count: 0 }
defineReactive(data, 'count', 0)

data.count      // 输出:收集依赖 count
data.count = 1  // 输出:触发更新 count

但是,这个方法有严重的局限性:

// 问题1:无法拦截新增属性
const data = { count: 0 }
defineReactive(data, 'count', 0)

data.newProp = 'hello'  // 这个操作没有任何拦截!不会触发任何响应

// 问题2:无法拦截删除属性
delete data.count  // 也没有拦截

// 问题3:数组的问题
const arr = [1, 2, 3]
// 对数组的 defineProperty 拦截效率极低,且 arr[0] = 100 这种索引赋值也无法完美拦截

// 问题4:初始化时必须递归遍历所有属性
const deep = { a: { b: { c: 1 } } }
// 必须递归遍历 a、b、c,即使 c 可能永远不会被使用

2.3 Vue 3 的方案:Proxy

Proxy 是 ES6 引入的特性,它可以代理整个对象,拦截 13 种基本操作:

const data = { count: 0, nested: { value: 1 } }

const proxy = new Proxy(data, {
  get(target, key) {
    console.log('读取', key)
    return target[key]
  },
  set(target, key, value) {
    console.log('写入', key, value)
    target[key] = value
    return true
  },
  deleteProperty(target, key) {
    console.log('删除', key)
    delete target[key]
    return true
  }
})

proxy.count        // 输出:读取 count
proxy.count = 1    // 输出:写入 count 1
proxy.newProp = 2  // 输出:写入 newProp 2(新增属性也能拦截!)
delete proxy.count // 输出:删除 count

Proxy 解决了 defineProperty 的所有痛点:

问题definePropertyProxy
新增属性❌ 无法拦截✅ 可拦截
删除属性❌ 无法拦截✅ 可拦截
数组索引赋值⚠️ 需特殊处理✅ 直接拦截
数组 length 修改⚠️ 需特殊处理✅ 直接拦截
初始化性能需递归遍历所有属性懒代理,按需代理

这就是 Vue 3 选择 Proxy 的根本原因。


第三章:reactive 的实现——从零构建

3.1 最简版 reactive

让我们从零开始构建一个最简单的 reactive 函数:

// 存储所有响应式对象的依赖关系
// targetMap: 对象 → 属性 → 依赖函数集合
const targetMap = new WeakMap()

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

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, key)
      
      const result = Reflect.get(target, key, receiver)
      
      // 如果读取的值是对象,递归代理(懒代理)
      if (result && 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 hadKey = Object.prototype.hasOwnProperty.call(target, key)
      const result = Reflect.deleteProperty(target, key)
      
      if (hadKey && result) {
        trigger(target, key)
      }
      
      return result
    }
  })
}

function track(target, key) {
  if (!activeEffect) return
  
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  
  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 dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}

3.2 运行一下

// 创建一个响应式对象
const state = reactive({ count: 0, user: { name: 'Vue' } })

// 定义一个副作用函数
function effect(fn) {
  activeEffect = fn
  fn()
  activeEffect = null
}

// 使用 effect
effect(() => {
  console.log('count 变成了:', state.count)
})
// 输出:count 变成了: 0

// 修改数据
state.count = 1  // 输出:count 变成了: 1
state.count = 2  // 输出:count 变成了: 2

// 验证懒代理
console.log(state.user.name)  // 读取 user.name,此时 user 才被代理

3.3 关键设计解读

1. 为什么用 WeakMap?

const targetMap = new WeakMap()  // 注意是 WeakMap,不是 Map
  • 键是原始对象(不是 Proxy)
  • WeakMap 对键是弱引用:当原始对象不再被其他代码引用时,可以被垃圾回收
  • 这意味着:组件销毁后,整个依赖树自动被回收,无需手动清理

2. 为什么用 Reflect?

Reflect.get(target, key, receiver)
  • 保证 this 指向正确
  • 返回操作的结果(比如 set 返回是否成功)
  • 与 Proxy 的拦截器一一对应,是标准搭配

3. 懒代理是什么?

if (result && typeof result === 'object') {
  return reactive(result)
}
  • 只有访问到嵌套对象时,才将其转为响应式
  • Vue 2 是初始化时递归遍历所有属性
  • 如果对象有 1000 个深层属性但只用了 1 个,Vue 3 的性能优势巨大

第四章:ref 的设计——如何让基础值也变得响应式?

4.1 问题:Proxy 只能代理对象

Proxy 只能代理对象,不能代理基础值(number、string、boolean 等)。

const count = 1
// 无法用 Proxy 代理 count,因为它不是对象

但是业务中我们经常需要响应式的基础值:countisLoadingname 等。

4.2 解决方案:包装对象

Vue 3 的解决方案是 ref:用一个对象把基础值包起来。

// 使用方式
const count = ref(0)
count.value   // 读取
count.value = 1  // 修改

简化版实现:

function ref(value) {
  // 如果已经是 ref,直接返回
  if (value && value.__v_isRef) return value
  
  return new RefImpl(value)
}

class RefImpl {
  constructor(value) {
    this.__v_isRef = true  // 标记这是一个 ref
    this._value = toReactive(value)  // 存储值,如果是对象则用 reactive 包装
  }
  
  get value() {
    // 依赖收集
    trackRefValue(this)
    return this._value
  }
  
  set value(newVal) {
    if (newVal !== this._value) {
      this._value = toReactive(newVal)
      // 触发更新
      triggerRefValue(this)
    }
  }
}

function toReactive(value) {
  return value && typeof value === 'object' ? reactive(value) : value
}

4.3 ref 的自动解包

Vue 3 在模板和 reactive 对象中会自动解包 ref,不需要写 .value

<template>
  <!-- 模板中直接使用,不需要 .value -->
  <div>{{ count }}</div>
</template>

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

const count = ref(0)
// 修改时还是需要 .value
const increment = () => count.value++
</script>
// reactive 对象中自动解包
const count = ref(0)
const state = reactive({
  count: count  // ref 作为属性
})

console.log(state.count)  // 0,不需要 .value
state.count = 1           // 直接修改,同时也会更新 count.value
console.log(count.value)  // 1

4.4 ref vs reactive:如何选择?

场景推荐原因
基础值(number、string、boolean)refreactive 无法处理
需要整体替换的对象refref.value = newObj 会触发更新;reactive 整体替换会丢失响应
固定结构的对象reactive更直接的访问方式,不需要 .value
不确定类型的值ref灵活,可以存任何类型

第五章:副作用函数 Effect——响应式的"发动机"

5.1 什么是副作用函数?

副作用函数是指那些执行后会对外部产生影响的函数。

在 Vue 的上下文中:

  • 组件渲染函数是副作用(它会产生 DOM 更新)
  • watchEffect 的回调是副作用(它可能执行任意代码)
  • computed 的 getter 也是副作用(它读取响应式数据)

响应式系统的本质:让副作用函数"记住"它访问了哪些响应式数据,当这些数据变化时,重新执行副作用函数。

5.2 完整的 Effect 实现

let activeEffect = null
const effectStack = []  // 处理嵌套 effect

class ReactiveEffect {
  constructor(fn, scheduler = null) {
    this.fn = fn              // 要执行的函数
    this.scheduler = scheduler  // 调度器(用于异步更新)
    this.active = true        // 是否激活
    this.deps = []            // 反向记录:这个 effect 被哪些 dep 收集了
  }
  
  run() {
    if (!this.active) {
      return this.fn()
    }
    
    // 清理旧的依赖(处理条件分支变化的情况)
    cleanupEffect(this)
    
    // 压栈,设置当前激活的 effect
    effectStack.push(this)
    activeEffect = this
    
    // 执行用户函数,触发依赖收集
    const result = this.fn()
    
    // 出栈,恢复上一个 effect
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    
    return result
  }
  
  stop() {
    if (this.active) {
      cleanupEffect(this)
      this.active = false
    }
  }
}

function cleanupEffect(effect) {
  const { deps } = effect
  for (let i = 0; i < deps.length; i++) {
    deps[i].delete(effect)
  }
  effect.deps.length = 0
}

function effect(fn, options = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  _effect.run()
  
  // 返回 runner,可以手动执行或停止
  const runner = _effect.run.bind(_effect)
  runner.effect = _effect
  return runner
}

5.3 为什么需要 effectStack?

考虑嵌套 effect 的场景:

effect(() => {           // effect1
  console.log('外层', state.a)
  effect(() => {         // effect2
    console.log('内层', state.b)
  })
})

执行流程:

  1. effect1 开始执行,activeEffect = effect1
  2. effect1 内部访问 state.a,依赖收集:state.a 的 dep 添加 effect1
  3. effect1 内部创建 effect2,effect2 开始执行,activeEffect = effect2
  4. effect2 访问 state.b,依赖收集:state.b 的 dep 添加 effect2
  5. effect2 执行完毕,activeEffect 恢复为 effect1
  6. effect1 执行完毕,activeEffect = null

如果没有 effectStack,effect2 执行完毕后我们无法知道上一个 effect 是谁,嵌套依赖收集就会出错。

5.4 为什么需要 cleanupEffect?

考虑条件分支的 effect:

let showA = true
effect(() => {
  if (showA) {
    console.log(state.a)  // 访问 a
  } else {
    console.log(state.b)  // 访问 b
  }
})
  • 第一次执行(showA = true):依赖 state.a
  • 修改 showA = false,重新执行 effect:现在依赖 state.b

问题:state.a 的 dep 中仍然有这个 effect!当 state.a 变化时,effect 还会被触发,造成不必要的执行。

解决方案:每次 effect 重新执行前,先把自己从所有 dep 中删除(cleanupEffect),然后重新收集依赖。

这就是 effect.deps 反向记录的作用——让 effect 知道自己被哪些 dep 收集了,可以精准地"退租"。


第六章:调度器与异步更新——性能的关键

6.1 问题:频繁更新导致性能浪费

const state = reactive({ count: 0 })

effect(() => {
  console.log('count:', state.count)
})

state.count = 1
state.count = 2
state.count = 3

没有调度器的话,会输出:

count: 1
count: 2
count: 3

三次输出意味着三次执行。如果这是组件渲染,就会三次操作 DOM,严重浪费性能。

6.2 解决方案:异步批处理

我们想要的:

  • 同一个 effect 在同一轮更新中只执行一次
  • 所有更新收集起来,在下一个时机批量执行
const queue = new Set()  // 用 Set 自动去重
let isFlushing = false

function queueJob(job) {
  if (!queue.has(job)) {
    queue.add(job)
    if (!isFlushing) {
      isFlushing = true
      nextTick(flushJobs)
    }
  }
}

function flushJobs() {
  // 拷贝一份执行,因为执行过程中可能有新的 job 加入
  const jobs = Array.from(queue)
  queue.clear()
  for (const job of jobs) {
    job()
  }
  isFlushing = false
  // 如果在执行过程中有新的 job 加入,递归处理
  if (queue.size) {
    flushJobs()
  }
}

6.3 nextTick 的实现

const nextTick = (() => {
  const callbacks = []
  let pending = false
  
  function flushCallbacks() {
    pending = false
    const copies = callbacks.slice()
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }
  
  // 使用 Promise 微任务
  let timerFunc = () => {
    Promise.resolve().then(flushCallbacks)
  }
  
  return function nextTick(fn) {
    callbacks.push(fn)
    if (!pending) {
      pending = true
      timerFunc()
    }
  }
})()

为什么用微任务(Promise.then)而不是宏任务(setTimeout)?

事件循环顺序:
同步代码 → 微任务队列 → 宏任务队列

使用微任务:
修改数据 → 推入队列 → 同步代码结束 → 微任务执行 → 更新 DOM
                                    ↑ 同一轮事件循环,无延迟

使用宏任务:
修改数据 → 推入队列 → 同步代码结束 → 等待下一轮 → 宏任务执行 → 更新 DOM
                                    ↑ 多了一轮事件循环,有可见延迟

6.4 在 effect 中使用调度器

function effect(fn, options = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  _effect.run()
  return _effect.run.bind(_effect)
}

// 使用示例:带调度器的 effect
const runner = effect(
  () => {
    console.log('count:', state.count)
  },
  {
    scheduler: (effect) => {
      queueJob(effect.run.bind(effect))
    }
  }
)

组件渲染 effect 正是这样配置的:数据变化时不立即渲染,而是推入队列,等待 nextTick 批量执行。


第七章:computed——带缓存的副作用

7.1 问题:重复计算浪费性能

const state = reactive({ a: 1, b: 2 })

// 如果多个地方需要 a + b 的结果
console.log(state.a + state.b)  // 计算1
console.log(state.a + state.b)  // 计算2(重复计算)

我们希望:只在依赖变化时重新计算,否则返回缓存值

7.2 computed 的完整实现

class ComputedRefImpl {
  constructor(getter) {
    this._value = undefined
    this._dirty = true  // 脏标志:true 表示需要重新计算
    this.dep = new Set()  // 依赖这个 computed 的 effect
    
    // 创建一个 effect,但不立即执行
    this.effect = new ReactiveEffect(getter, () => {
      // scheduler:当依赖变化时
      if (!this._dirty) {
        this._dirty = true
        // 通知所有依赖这个 computed 的 effect
        trigger(this, 'value')
      }
    })
  }
  
  get value() {
    // 收集依赖(谁在读取这个 computed)
    track(this, 'value')
    
    if (this._dirty) {
      this._value = this.effect.run()  // 重新计算
      this._dirty = false
    }
    return this._value
  }
}

function computed(getter) {
  return new ComputedRefImpl(getter)
}

7.3 执行流程详解

const state = reactive({ a: 1, b: 2 })
const sum = computed(() => {
  console.log('计算中...')
  return state.a + state.b
})

// 创建 computed 时:不会立即计算,没有输出

// 第一次访问:触发计算
console.log(sum.value)
// 输出:计算中...
// 输出:3
// _dirty 变为 false

// 第二次访问:直接返回缓存
console.log(sum.value)
// 输出:3(没有"计算中...")

// 依赖变化:state.a = 3
// 触发 state.a 的 set → trigger
// 找到 sum 内部的 effect → 执行 scheduler
// scheduler 设置 _dirty = true
// 同时 trigger(sum, 'value') 通知 sum 的依赖
// 注意:此时没有重新计算,只是标记为脏

// 再次访问:触发重新计算
console.log(sum.value)
// 输出:计算中...
// 输出:5

7.4 为什么需要两阶段通知?

const sum = computed(() => state.a + state.b)

watchEffect(() => {
  console.log('sum 变化了:', sum.value)
})

// 当 state.a 变化时:
// 1. state.a 的 trigger 执行
// 2. sum 内部的 effect 的 scheduler 执行
//    → _dirty = true
//    → trigger(sum, 'value')  ← 关键!
// 3. watchEffect 的 effect 被触发
// 4. watchEffect 重新执行,读取 sum.value
//    → 发现 _dirty = true,重新计算
//    → 输出新值

// 如果没有 trigger(sum, 'value'):
// 第 2 步只设置了 _dirty = true
// watchEffect 不知道需要重新执行
// 直到下次有人主动读取 sum.value 才会更新

第八章:响应式与组件生命周期的完整串联

这是理解 Vue 整体运作的关键。让我们追踪一个组件从创建到销毁的全过程。

8.1 组件初始化

// 1. 创建组件实例
const instance = {
  isMounted: false,
  subTree: null,
  ctx: null,  // 组件上下文(响应式数据)
  update: null  // 更新函数
}

// 2. 执行 setup,创建响应式数据
const setupResult = Component.setup(props, { emit })
instance.ctx = setupResult

// 3. 创建渲染 effect
function setupRenderEffect(instance) {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      // 首次挂载
      // 执行 beforeMount 钩子
      const subTree = instance.render.call(instance.ctx)
      patch(null, subTree, instance.container)
      instance.subTree = subTree
      instance.isMounted = true
      // 执行 mounted 钩子
    } else {
      // 更新
      // 执行 beforeUpdate 钩子
      const prevTree = instance.subTree
      const nextTree = instance.render.call(instance.ctx)
      patch(prevTree, nextTree, instance.container)
      instance.subTree = nextTree
      // 执行 updated 钩子
    }
  }
  
  // 创建 effect,配置 scheduler 实现异步更新
  instance.update = new ReactiveEffect(
    componentUpdateFn,
    () => queueJob(instance.update)  // 调度器
  )
  
  // 首次执行,触发依赖收集
  instance.update.run()
}

8.2 首次渲染时的依赖收集

执行 instance.update.run()
    │
    ▼
activeEffect = 组件渲染 effect
    │
    ▼
执行 render() 函数
    │
    ├── 访问 state.count → 触发 Proxy get → track(state, 'count')
    │   └── 将组件渲染 effect 添加到 state.count 的依赖集合中
    │
    ├── 访问 state.user.name → 触发 Proxy get → track(user, 'name')
    │   └── 将组件渲染 effect 添加到 user.name 的依赖集合中
    │
    └── ... 其他响应式数据访问
    │
    ▼
render() 返回虚拟 DOM
    │
    ▼
patch 将虚拟 DOM 转换为真实 DOM
    │
    ▼
activeEffect = null

结果:组件渲染 effect 被记录到了所有它访问过的响应式数据的依赖集合中。

8.3 数据变化时的更新链路

// 用户操作
function handleClick() {
  state.count++  // 假设 state.count 是响应式数据
}

// 完整链路:
// 
// 1. state.count++ → 触发 Proxy set 拦截
//    ↓
// 2. set 拦截中调用 trigger(state, 'count')
//    ↓
// 3. trigger 从 targetMap 中找到依赖 count 的所有 effect
//    找到了:组件渲染 effect
//    ↓
// 4. 调用 effect.scheduler(即 queueJob)
//    ↓
// 5. queueJob 将 instance.update 推入异步队列
//    ↓
// 6. 同步代码执行完毕
//    ↓
// 7. nextTick 的微任务执行 flushJobs
//    ↓
// 8. flushJobs 执行 instance.update
//    ↓
// 9. instance.update 执行 componentUpdateFn
//    ├── 执行 beforeUpdate 钩子
//    ├── 重新执行 render(),生成新的虚拟 DOM
//    ├── 执行 patch,更新真实 DOM
//    └── 执行 updated 钩子
//    ↓
// 10. 用户看到新界面

8.4 组件销毁时的清理

function unmountComponent(instance) {
  // 执行 beforeUnmount 钩子
  
  // 停止渲染 effect
  if (instance.update) {
    instance.update.effect.stop()
  }
  // effect.stop() 会:
  // 1. 设置 effect.active = false
  // 2. 遍历 effect.deps,从每个 dep 中删除自己
  // 3. 清空 effect.deps
  
  // 卸载 DOM
  unmount(instance.subTree)
  
  // 执行 unmounted 钩子
  
  // 此时,原始对象不再被引用,WeakMap 中的条目自动被垃圾回收
}

8.5 生命周期与响应式的对照表

生命周期响应式状态能否访问 DOM典型用途
beforeCreate响应式数据未初始化插件注入
created响应式数据已就绪数据请求、事件监听
beforeMount首次渲染前最后一次修改数据
mounted首次渲染后DOM 操作、第三方库初始化
beforeUpdate数据变化、重新渲染前获取更新前的 DOM 状态
updated重新渲染后DOM 操作(注意避免死循环)
beforeUnmount卸载前清理定时器、取消请求
unmounted卸载后最终清理

第九章:Vue 2 vs Vue 3——全面对比

9.1 核心差异表

维度Vue 2Vue 3
数据劫持Object.definePropertyProxy + Reflect
拦截能力只能拦截属性读写拦截 13 种操作(读写、删除、in 等)
新增属性需要 $set自动侦测
删除属性需要 $delete自动侦测
数组变化重写数组原型方法直接代理,索引和 length 都可拦截
初始化性能递归遍历所有属性懒代理,按需代理
依赖管理Dep + WatchertargetMap + ReactiveEffect
调度更新queueWatcherqueueJob(更通用)
TypeScript 支持较差(需装饰器或类组件)原生支持
Tree-shaking不支持按需引入,体积更小
响应式核心代码约 2.5KB(gzipped)约 1.5KB(gzipped)

9.2 性能基准测试

场景Vue 2Vue 3提升
组件初始化(1000 个)~120ms~60ms50%
列表更新(1000 项)~85ms~30ms65%
深层对象访问初始化时全部代理访问时才代理视使用情况
内存占用基准约减少 50%-

9.3 迁移建议

如果你还在使用 Vue 2,以下情况值得考虑迁移到 Vue 3:

  • 项目中有大量的深层嵌套对象
  • 频繁需要 $set / $delete
  • 需要更好的 TypeScript 支持
  • 希望获得更好的性能
  • 需要使用 Vue 3 独有的新特性(Composition API、Suspense、Teleport 等)

第十章:面试高频题与灵魂回答

Q1:Vue 3 响应式原理是什么?

:Vue 3 响应式系统建立在三个核心概念之上:

  1. Proxy 代理:使用 Proxy 拦截对象的所有读写操作,在 get 中收集依赖(track),在 set/deleteProperty 中触发更新(trigger)

  2. 副作用函数(ReactiveEffect):封装需要响应式执行的函数,记录它依赖了哪些数据,也记录哪些数据依赖了它(双向记录)

  3. 调度器(Scheduler):负责异步批量执行副作用,避免频繁更新导致的性能问题

一句话总结:Vue 3 通过 Proxy 在数据读取时记录"谁在读",在数据修改时通知"所有在读的人"重新执行,并通过调度器批量处理更新。

Q2:为什么 Vue 3 用 Proxy 代替 defineProperty?

:有三个核心原因:

  1. 能力差异:defineProperty 只能拦截属性的读取和写入,无法拦截新增属性、删除属性、数组索引赋值等操作。Proxy 可以拦截 13 种基本操作,从根本上解决了 Vue 2 需要 $set / $delete 的问题。

  2. 性能差异:defineProperty 需要在初始化时递归遍历所有属性,即使这些属性可能永远不会被使用。Proxy 采用懒代理策略,只在访问到嵌套对象时才进行代理,初始化性能提升 2-4 倍。

  3. 代码复杂度:defineProperty 需要为数组单独处理(重写 7 个原型方法),Proxy 对数组的拦截是天然的,不需要特殊处理。

Q3:ref 是如何实现响应式的?为什么需要 .value?

:由于 Proxy 只能代理对象,无法代理基础值(number、string、boolean 等),ref 通过创建一个包装对象 { value: 原始值 } 来解决这个问题。

这个包装对象通过 Object.defineProperty 或类似的机制拦截 .value 的读写:

  • 读取时:执行 track,收集当前依赖
  • 写入时:执行 trigger,触发所有依赖

.value 的存在是因为 JavaScript 的语法限制——我们无法直接拦截对基础值的访问,必须通过一个属性访问来建立拦截点。

在模板和 reactive 对象中,Vue 3 会自动解包 ref,因此不需要写 .value,这是编译器和运行时的协作结果。

Q4:computed 的缓存是如何实现的?

:computed 的缓存通过 _dirty(脏标志)实现:

  1. 每个 computed 内部创建一个 ReactiveEffect,但不立即执行(懒执行)
  2. 首次访问 .value 时,_dirty = true,执行 effect 计算值并缓存,然后将 _dirty 设为 false
  3. 后续访问直接返回缓存值,不重新计算
  4. 当依赖的响应式数据变化时,触发 computed 内部 effect 的 scheduler,将 _dirty 设为 true,并主动 trigger 自己的依赖
  5. 下次访问 .value 时发现 _dirty = true,重新计算并更新缓存

关键设计:computed 的依赖变化时只标记脏,不重新计算,真正计算发生在被读取时。这种"懒计算"策略避免了不必要的计算。

Q5:Vue 3 的异步更新是如何实现的?

:Vue 3 的异步更新通过**调度器(Scheduler)+ 队列(Queue)+ 微任务(nextTick)**实现:

  1. 每个组件渲染 effect 都配置了一个 scheduler,当数据变化触发更新时,scheduler 不直接执行 effect,而是调用 queueJob

  2. queueJob 使用 Set 数据结构存储需要执行的 job(自动去重),然后通过 nextTick 调度 flushJobs 的执行

  3. flushJobs 遍历队列,执行所有 job

性能提升的本质:一个组件内如果响应式数据被修改了 100 次,没有异步更新会导致 100 次 DOM 操作;有异步更新只会有 1 次 DOM 操作,性能提升 100 倍。

为什么用微任务:微任务在同步代码执行完毕后立即执行,没有额外的事件循环延迟,既能批量处理,又能保证更新的及时性。

Q6:什么是懒代理?有什么好处?

:懒代理是 Vue 3 响应式系统的核心优化之一。

在 Vue 2 中,data 初始化时会递归遍历所有属性,将其转换为 getter/setter,即使深层属性从未被使用。

在 Vue 3 中,reactive 只代理最外层的对象,内层嵌套对象在被访问时才递归调用 reactive 变成响应式。

好处

  • 初始化速度大幅提升(尤其对于深层嵌套的大对象)
  • 内存占用减少(未被访问的属性不会创建响应式开销)
  • 对于"宽而浅"的数据结构,优势尤其明显

Vue 3 响应式原理:从零到一,触及灵魂
http://localhost:8090/archives/vue-3-xiang-ying-shi-yuan-li-cong-ling-dao-yi-chu-ji-ling-hun
作者
Administrator
发布于
2026年04月07日
许可协议