
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 才指向代理对象。
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 复位。
let activeEffect
export const effect = (fn) => { const _effect = () => { activeEffect = _effect // 标记当前正在运行的函数 fn() // 执行时会读到响应式数据,触发 get activeEffect = undefined // 执行完复位,避免污染后面的 track } _effect()}track 收集依赖
依赖表是三层嵌套结构:WeakMap 按对象分,Map 按属性名分,Set 存副作用。第一层用 WeakMap,对象没人引用时整张表能被回收。
每一层都是“没有就先建”,建完把 activeEffect 加进最里面的 Set。
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,把里面的副作用挨个执行。
每改一次属性就会触发一轮,没有去重和批量。
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' 这种深层赋值也能触发更新。
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 缓存已经建好的代理。
Vue 3 学习笔记
连载中分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 214 天
部分内容可能已过时