1044 字
5 分钟
Vue 3 响应式的手写实现

Vue 2 和 Vue 3 的拦截方式#

Vue 2 用 Object.defineProperty 逐个改写属性的读写。这个实现有三处拦不住的地方:给对象新增或删除属性、给数组赋值下标、改数组的 length。所以 Vue 2 额外提供了 Vue.set 和 Vue.delete,用它们补上新增和删除。

Vue 3 换成 Proxy 代理整个对象,读写、删除、in 判断都能拦到,数组下标和 length 也一样。上面那三处限制都来自 Vue 2 的实现方式,换成 Proxy 后不再存在。

Proxy 还有一处改动是递归时机。Vue 2 在初始化时把整棵对象树走一遍,Vue 3 只在读取到嵌套对象时才把它转成代理,没被访问到的层级不会创建代理。

四个核心步骤#

这一节的代码是教学用的简化实现,不是 Vue 源码。

最小实现需要四个部分:

  • reactive:用 Proxy 包装对象,拦截读写
  • effect:注册副作用函数,让它先执行一次
  • track:读属性时把当前副作用存进依赖表
  • trigger:改属性时取出依赖表里的副作用并执行

reactive 拦截读写#

get 里先把值取出来,再交给 track 记录依赖;set 写回新值之后调用 trigger。两个都走 Reflect,receiver 一路带下去,this 才指向代理对象。

reactive.js
export const reactive = (target) => {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key) // 收集依赖
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return res
},
})
}

effect 记录当前副作用#

activeEffect 是一个模块级变量,指向当前正在执行的副作用函数。track 靠它知道该把依赖记在谁头上。

函数执行完要把它复位。不复位的话,在 effect 之外读响应式属性,track 会把上一次的副作用记进依赖表,或者把 undefined 记进去,让 trigger 抛错。真实实现用 try/finally 复位。

effect.js
let activeEffect
export const effect = (fn) => {
const _effect = () => {
activeEffect = _effect // 标记当前正在运行的函数
fn() // 执行时会读到响应式数据,触发 get
activeEffect = undefined // 执行完复位,避免污染后面的 track
}
_effect()
}

track 收集依赖#

依赖表是三层嵌套结构:WeakMap 按对象分,Map 按属性名分,Set 存副作用。第一层用 WeakMap,对象没人引用时整张表能被回收。

每一层都是“没有就先建”,建完把 activeEffect 加进最里面的 Set。

effect.js
const targetMap = new WeakMap()
export const track = (target, key) => {
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)
}

trigger 执行依赖#

改属性时反向查一遍:先按对象找到 Map,再按属性名找到 Set,把里面的副作用挨个执行。

每改一次属性就会触发一轮,没有去重和批量。

effect.js
export const trigger = (target, key) => {
const depsMap = targetMap.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
deps.forEach((effect) => {
effect() // 重新执行
})
}

让嵌套对象也有响应式#

上面的 get 直接把原始值返回。如果读到的是对象,就把读到的值再过一次 reactive,嵌套层级才会各自带代理。

改成递归之后,target.friend.name = 'zl' 这种深层赋值也能触发更新。

reactive.js
import { track, trigger } from './effect'
const isObject = (target) => target !== null && typeof target === 'object'
export const reactive = (target) => {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
// 读到的值是对象就再代理一层
if (isObject(res)) {
return reactive(res)
}
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key)
return res
},
})
}

每个嵌套对象访问一次就新建一个代理,同一份数据反复读会拿到不同代理。真实源码用 proxyMap 缓存已经建好的代理。

第 3 篇,共 19 篇本文属于系列

Vue 3 响应式的手写实现
https://www.choria.top/posts/vue3-reactivity/
作者
乔瑞雅·克林威尔
发布于
2026-04-08
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 214 天

部分内容可能已过时