响应式

如果只能用一句话概括 Vue 的响应式,可以这样说:

你改数据,Vue 能知道;Vue 知道后,会把依赖这个数据的地方重新运行。

这句话看起来简单,但背后其实是一整套机制:

  • 谁在读取数据
  • 谁在修改数据
  • 哪些逻辑依赖了这份数据
  • 数据变化后应该通知谁重新执行

如果你先把这张图看懂,后面再看 tracktriggereffect,就不会觉得响应式系统特别抽象。

一、为什么响应式这么重要

在没有响应式系统时,你通常要手动做很多事:

state.count++
updateView()

而在 Vue 里,我们更希望写成:

state.count++

剩下的更新工作交给框架处理。

这就是响应式的价值:让状态变化自动驱动视图和副作用更新

二、先不要急着看源码,先建立直觉

可以把响应式系统理解成一个“订阅通知系统”:

  1. 某段逻辑运行时读取了数据
  2. Vue 记住“这段逻辑依赖了这个数据”
  3. 当数据变化时,Vue 去通知对应逻辑重新运行

这三个动作分别对应三个关键词:

  • track:收集依赖
  • trigger:触发更新
  • effect:被收集、会重新运行的副作用函数

三、一个最小响应式例子

先别看 Vue 源码,先看最小模型:

let activeEffect: Function | null = null
const bucket = new Set<Function>()

const data = { text: 'hello' }

const obj = new Proxy(data, {
    get(target, key) {
        if (activeEffect) {
            bucket.add(activeEffect)
        }
        return target[key]
    },
    set(target, key, value) {
        target[key] = value
        bucket.forEach(fn => fn())
        return true
    },
})

function effect(fn: Function) {
    activeEffect = fn
    fn()
    activeEffect = null
}

effect(() => {
    document.body.innerText = obj.text
})

setTimeout(() => {
    obj.text = 'world'
}, 1000)

这个版本已经能体现最基础的思路:

  • 读取时收集依赖
  • 修改时重新执行依赖

但它还远远不够。

四、为什么这个最小版本不够用

因为真实项目里的数据不会只有一个字段,也不会只有一个副作用函数。

比如下面这些问题,最小版本都处理不好:

  • 不同属性应该分别追踪
  • 不同对象不能混在一起
  • 分支切换后旧依赖要清理
  • 副作用不能无限递归触发
  • 更新不能每次都立刻同步执行

所以真正可用的响应式系统,必须把数据结构设计得更精细。

五、Vue 3 的核心数据结构

Vue 3 里最关键的一层结构,可以简化理解成:

WeakMap<object, Map<PropertyKey, Set<ReactiveEffect>>>

它表示:

  • 第一层 WeakMap:按对象区分
  • 第二层 Map:按属性区分
  • 第三层 Set:记录依赖这个属性的 effect

你可以把它想成一本三层通讯录:

  • 哪个对象
  • 哪个字段
  • 对应哪些订阅者

六、track 在做什么

当响应式对象的某个属性被读取时,会触发依赖收集。

简化后可以理解成:

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)
}

这段逻辑的本质非常朴素:

只要当前存在正在运行的 effect,就把它记到当前属性对应的依赖集合里。

七、trigger 在做什么

当属性被修改时,Vue 会找到依赖它的 effect,然后重新执行。

简化理解:

function trigger(target, key) {
    const depsMap = targetMap.get(target)
    if (!depsMap) return

    const effects = depsMap.get(key)
    effects && effects.forEach(effect => effect())
}

这就是“通知订阅者”的过程。

八、effect 为什么是整个系统的中心

很多人学响应式时只盯着 reactive,其实真正连接“数据”和“更新逻辑”的桥梁是 effect

它的职责是:

  • 先执行一遍函数
  • 在执行过程中触发依赖收集
  • 后续数据变化时可以再次执行

也就是说,没有 effect,响应式系统就只是一个会记账的壳子

九、为什么需要依赖清理

看一个分支例子:

effect(() => {
    document.body.innerText = state.ok ? state.text : 'not'
})

第一次执行时,如果 state.oktrue,那这段 effect 会依赖:

  • state.ok
  • state.text

但如果下一次 state.ok 变成 false,理论上这段逻辑就不应该继续依赖 state.text 了。

如果不清理旧依赖,会发生:

  • state.text 改了
  • effect 还是会重新执行
  • 但实际上这次执行根本不需要它

这就是为什么 Vue 在每次 effect 重新执行前,都会做 cleanup。

十、为什么需要 effect 栈

真实项目里,副作用函数可能嵌套执行。

例如:

effect(() => {
    console.log(state.foo)
    effect(() => {
        console.log(state.bar)
    })
})

如果只用一个全局 activeEffect,就很容易乱掉。

因此 Vue 会用一个栈来保存 effect 执行上下文:

  • 入栈当前 effect
  • 执行函数
  • 执行结束后出栈
  • 恢复上一层 activeEffect

这可以避免嵌套时依赖收集错位。

十一、为什么更新不能每次都同步执行

如果一改数据就立刻执行 effect,会有几个问题:

  • 重复执行太多
  • 多次修改不能合并
  • 组件更新会变得低效

因此 Vue 不只是“收集依赖再触发”,它还会配合调度器 scheduler 去控制执行时机。

这也是为什么响应式系统会自然连接到:

  • 组件更新队列
  • nextTick
  • 批量刷新机制

十二、reactiverefcomputedwatch 的关系

很多人容易把这些 API 拆开记,其实它们底层关系很近。

reactive

让对象变成响应式对象。

ref

让一个值拥有响应式能力,本质上可以理解成:

{ value: xxx }

只是 Vue 帮你做了包装。

computed

本质上是“带缓存的 effect”。

它不会每次都重新计算,而是:

  • 依赖没变时直接返回缓存
  • 依赖变了才重新计算

watch

本质上是“观察某个响应式来源变化后执行回调”。

它和 effect 的关系非常近,只是语义更偏“监听变化”。

十三、源码阅读时最值得抓的主线

如果你准备读 Vue 3 响应式源码,建议抓这条主线:

  1. reactive() 如何创建代理对象
  2. get 里如何 track
  3. set 里如何 trigger
  4. effect() 如何记录 activeEffect
  5. cleanup、effect 栈、scheduler 如何把系统补完整

只要主线清楚,后面的各种优化和边界处理就不容易把你绕晕。

十四、一个简化版源码脉络

下面这段伪代码,基本能串起整个响应式主流程:

const targetMap = new WeakMap()
let activeEffect

function effect(fn) {
    const effectFn = () => {
        activeEffect = effectFn
        fn()
        activeEffect = undefined
    }
    effectFn()
    return effectFn
}

function track(target, key) {
    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, key) {
    const depsMap = targetMap.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)
    effects && effects.forEach(effect => effect())
}

function reactive(obj) {
    return new Proxy(obj, {
        get(target, key, receiver) {
            track(target, key)
            return Reflect.get(target, key, receiver)
        },
        set(target, key, value, receiver) {
            const result = Reflect.set(target, key, value, receiver)
            trigger(target, key)
            return result
        },
    })
}

真实源码当然会复杂得多,但核心理解不会离这条主线太远。

十五、你真正应该理解什么

学响应式,不是为了背 API,而是要真正理解下面这件事:

Vue 不是“神奇地自动更新”,而是把“依赖收集”和“变更通知”这两件事做成了稳定的数据结构和执行流程。

总结

响应式系统最核心的三件事是:

  • 读取时收集依赖
  • 修改时触发更新
  • 用 effect 把状态和逻辑连接起来

当你理解了这条主线,再去看 computedwatch、组件更新、nextTick,会顺畅很多。

上次更新:
贡献者: Joe, joe