Skip to content

深入响应式

Vue 响应式原理

Vue 的响应式的核心就是“拦截”


在 js 中,有两种劫持 property 访问的方式:

  1. Object.defineProperty
  2. Proxy

  • Vue 2:使用 Object.defineProperty 完全是出于支持旧版本浏览器的限制。
  • Vue 3:使用 Proxy 来创建响应式对象(IE 不支持)。

但需要注意,两者都不能直接让视图同步更新。

Proxy:拦截“行为”

劫持对象

js
const state = { count: 0 }

const p = new Proxy(state, {
  get(t, k) {
    console.log('get:', k) // 任何读取都会触发
    return t[k]
  },
  set(t, k, v) {
    console.log('set:', k, v) // 任何写入都会触发
    t[k] = v
    return true
  },
})

p.count // get: count
p.count = 1 // set: count 1
p.count++ // get: count、set: count 1
  • 行为级拦截:不需要逐个定义属性。
  • 新增/删除、数组下标 也能够感知。

数组下标和长度

Proxy 对于下标写入与 length 变化都能拦截。

js
const list = ['a']
const p = new Proxy(list, {
  set(t, k, v) {
    console.log('set:', k, v)
    t[k] = v
    return true
  },
})

p[0] = 'b' // set: 0 b
p.push('c') // set: 1 c、set: length 2

Object.defineProperty:拦截“属性”

劫持对象

js
const state = { count: 0 }

Object.defineProperty(state, 'count', {
  get() {
    console.log('get: count') // 读取时触发
    return value
  },
  set(v) {
    console.log('set: count', v) // 写入时触发
    value = v
  },
})

state.count // get: count
state.count = 1 // set: count 1
state.count++ // get: count、set: count 1
  • 只能拦截 已有属性,且需要 逐个定义
  • 新增/删除属性、数组下标 无法感知。

数组下标和长度

Object.defineProperty 需要 覆盖每个下标 或者 改写方法

依赖追踪(依赖收集)

为什么需要依赖追踪?

虽然 ProxyObject.defineProperty 都能劫持数据访问,但此时仅实现了对数据的“拦截”,此时还没有 同步数据变化到视图 的能力。

于是就引入了一种新的机制:依赖追踪

依赖追踪的核心问题

数据变化 !== 视图自动更新

需要建立 数据 -> 依赖它的副作用函数 的映射关系

解决问题的和核心思路

副作用函数:会读取响应式数据,并在数据变化时重新执行的函数。

  1. 依赖收集(track):当 effect 读取了某个属性时,把它登记到属性的依赖集合中。
  2. 派发更新(trigger):当属性变化时,从依赖集合中找到所有的 effect,并依次执行。

依赖收集(track)

什么时候发生?

在某个副作用执行期间,如果读取了响应式数据,就把这个副作用记录到数据对应的依赖集合中。

  • 目的:建立响应式数据 -> 副作用函数 的映射关系
  • 关键点:只有在有 activeEffect 的情况下,才会把依赖收集起来。

巧记:谁在用我?我记下来

派发更新(trigger)

什么时候发生?

当某个响应式数据被修改时,找到它的依赖集合,把里面的副作用函数全部重新执行一次。

  • 目的:让“用过这个数据的地方”重新运行,从而更新视图或执行其他逻辑。
  • 关键点:精准触发,只更新真正受影响的副作用。

巧记:我变了?通知用我的人重新执行

执行流程

采取"发布/订阅模式"

  1. effect(render):
    • 设置 activeEffect = render
    • 执行 render 时读取 state.count -> track 把 render 存到 count 的依赖集合
  2. 修改 state.count:
    • 触发 Proxy 的 set -> trigger 从 count 的依赖集合取出 render 并执行

依赖追踪的实现

js
let activeEffect = null // 当前正在执行的副作用函数
const bucket = new WeakMap() // 依赖收集(存储副作用函数的容器),target -> key -> effects

function effect(fn) {
  activeEffect = fn
  fn() // 执行时触发 get,从而完成依赖收集
  activeEffect = null
}

const state = new Proxy(
  { count: 0 },
  {
    get(t, k) {
      // === 依赖收集(track) ===
      if (activeEffect) {
        let depsMap = bucket.get(t)
        if (!depsMap) {
          bucket.set(t, (depsMap = new Map()))
        }
        let deps = depsMap.get(k)
        if (!deps) {
          depsMap.set(k, (deps = new Set()))
        }
        deps.add(activeEffect)
      }
      return t[k]
    },
    set(t, k, v) {
      t[k] = v
      // === 触发更新(trigger) ===
      const depsMap = bucket.get(t)
      if (!depsMap) return
      const effects = depsMap.get(k)
      effects && effects.forEach((fn) => fn())
      return true
    },
  },
)

基于 MIT 许可发布