当前位置: 首页 > news >正文

vue的响应式原理深度解读

这篇文章将深入探讨 Vue 的响应式原理。面试官若只听到 “Vue2 用 Object.defineProperty,Vue3 用 Proxy” 就算回答完成了,那只是门面——真正能打动人的是你把两者的“能力边界 / 规范映射 / 设计权衡 / 源码机制”讲清楚,并且把 Vue2/3 的依赖收集与调度流程串起来。

一、defineProperty 和 Proxy

要理解 Vue 的响应式,首先必须透彻理解其赖以实现的两种 JavaScript API。

1. 内部方法(internal methods)

ECMAScript 把对象操作抽象成一组内部方法,常见的有:

[[Get]](读取 obj[p])
[[Set]](赋值 obj[p] = v)
[[DefineOwnProperty]](Object.defineProperty(obj, p, desc))
[[Delete]](delete obj[p])
[OwnPropertyKeys]](Object.keys/for...in/Reflect.ownKeys)
[[Has]](in)
[[GetPrototypeOf]]、[[SetPrototypeOf]]、[[IsExtensible]]、[[PreventExtensions]]

调用相关的 [[Call]](函数调用)与 [[Construct]](new)

要点:所有 JS 代码的各种操作(点读写、in、Object.keys、delete、Object.defineProperty、函数调用等)最终对应到这些内部方法之一。

2. defineProperty

Object.defineProperty 是 ES5 提供的方法,它可以精确地为一个对象添加或修改属性,并允许开发者定义该属性的元信息(enumerable, configurable)和访问描述符(get, set)。

核心机制:

  • get: 当访问该属性时,此函数会被调用。
  • set: 当该属性被赋值时,此函数会被调用。

Vue2 中的实现思路:
Vue2 通过递归遍历 data 函数返回对象的所有属性,将每个属性都转换为 gettersetter

// 极简的 Vue2 响应式原理模拟
function defineReactive(obj, key, val) {// 递归处理嵌套对象if (typeof val === 'object' && val !== null) {observe(val);}const dep = new Dep(); // 每个属性都拥有自己的依赖管理器Object.defineProperty(obj, key, {enumerable: true,configurable: true,get() {// 依赖收集:如果当前有 Watcher(例如渲染 Watcher),则将其加入依赖if (Dep.target) {dep.depend();}return val;},set(newVal) {if (newVal === val) return;val = newVal;// 触发更新:通知所有依赖的 Watcher 进行更新dep.notify();}});
}function observe(obj) {if (typeof obj !== 'object' || obj == null) return;// ... 遍历 obj 的每个 key,执行 defineReactive
}

缺陷:

  1. 无法检测对象属性的添加或删除:由于 Object.defineProperty 是在初始化时对已有属性进行拦截,后续新增的属性(obj.newProperty = ...)或删除的属性(delete obj.property)无法被追踪。这就是为什么 Vue2 需要 Vue.setVue.delete 这些 API 的原因。
  2. 数组拦截需要黑客手段:无法直接拦截数组的索引操作(arr[index] = newValue)和 length 的变化。Vue2 通过重写数组的 7 个变更方法(push, pop, shift, unshift, splice, sort, reverse)来实现响应式,这在实现上显得有些“补丁”感。
  3. 性能开销:初始化时需要递归遍历整个对象,将每一层属性都转化为 getter/setter,如果对象嵌套很深,性能开销较大。

3. Proxy

Proxy 是 ES6 引入的全新概念,用于创建一个对象的代理,从而实现对目标对象基本操作的拦截和自定义。它不是定义属性的特性,而是直接代理整个对象。

核心机制:
Proxy 提供了一系列“捕捉器”(Trap),如 get, set, deleteProperty, has 等,可以拦截对代理对象的几乎所有操作。

优势:

  1. 全面的拦截能力:可以拦截包括属性添加、删除在内的更多操作,从根本上解决了 Object.defineProperty 的先天不足。
  2. 原生数组支持:完美拦截对数组的任何操作,包括索引赋值和 length 修改。
  3. 更好的性能:Proxy 代理整个对象,无需在初始化时递归遍历所有属性。只有在某个属性被访问时,才会递归代理其内部值(惰性代理),这在性能和内存占用上更有优势。
  4. 更简洁的 API:无需额外的 Vue.set/Vue.delete API,操作方式更符合 JavaScript 原生习惯。
// 极简的 Vue3 响应式原理模拟 (reactive)
function reactive(obj) {if (typeof obj !== 'object' || obj == null) return obj;const handler = {get(target, key, receiver) {const res = Reflect.get(target, key, receiver);track(target, key); // 依赖收集// 惰性代理:只有当访问的是对象时,才返回它的 reactive 版本return typeof res === 'object' ? reactive(res) : res;},set(target, key, value, receiver) {const oldValue = target[key];const result = Reflect.set(target, key, value, receiver);if (result && 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 (result && hadKey) {trigger(target, key); // 触发更新}return result;}};return new Proxy(obj, handler);
}

简单总结一下,defineProperty 只能对已有的具体属性通过 get/set 描述符做拦截,因而对复杂或嵌套对象必须递归遍历每个属性来代理,导致性能开销大;而且它的描述符只有读写拦截,无法捕获属性的新增、删除或数组的原生变异方法。Vue 2 的做法是加一层中间层:通过原型链改写数组方法、提供 $set/$delete 等工具并在必要时包装属性来弥补 defineProperty 的缺陷。相比之下,Proxy 能直接拦截对象的底层/基本操作(如 get、set、deleteProperty、ownKeys 等),不需要对每个属性递归代理,从而能更全面、自然地处理新增、删除和数组方法等场景。

那么,明明Proxy是2015年出的,而Vue2是2016年有的,为什么 Vue2 不采用 Proxy?
答案很简单:浏览器兼容性。Vue2 的开发始于2016 年,其设计目标需要覆盖包括 IE9+ 在内的主流浏览器。而 Proxy 是一个无法被 polyfill 的 API(它的功能无法用旧的 JavaScript 语法完全模拟)。为了最广泛的适用性,尤大大也就选择了 Object.defineProperty 这条更稳妥但更具挑战的道路。Vue3 诞生时,现代浏览器的普及度已足够高,使其可以毫无顾虑地拥抱 Proxy 这一更先进的方案。

二、Vue 的响应式系统

在讲解Vue响应式系统之前,我们先来思考一个问题,什么是响应式?是MVVM架构(数据驱动视图)?还是Proxy的数据代理?
这些说法都不准确,简单来说,响应式就是数据和函数得关联,数据变化,关联的函数重新运行

Vue 的响应式系统本质是一个自动化依赖追踪和派发更新的引擎。其核心目标是:当数据变化时,能自动、精准地找到依赖于这些数据的代码(副作用),并通知它们重新执行。

1. Vue2 的响应式系统:基于 [[Get]]/[[Set]] 的精密模拟

Vue2 的核心挑战在于:仅凭 Object.defineProperty(只能拦截 [[Get]][[Set]])来模拟一个完整的响应式代理。其架构围绕三大核心部件构建,形成了一个精巧的发布-订阅模型。

核心部件深度解析:

  • Observer (观察者)

    • 职责:将一个普通的 JavaScript 对象转换为每个层级的属性都是响应式的对象。
    • 实现机制:递归地遍历对象的所有自有属性(Object.keys(obj)),对每个属性执行 defineReactive 函数。对于数组,Vue2 选择重写其 7 个变更方法(push, pop, shift, unshift, splice, sort, reverse),在这些方法被调用时手动通知更新。这是一种对无法拦截 [[DefineOwnProperty]](用于设置数组长度和索引)的妥协方案。
  • Dep (依赖管理器)

    • 职责:为每个响应式属性管理一个依赖列表(订阅者列表)。它是连接数据和 Watcher 的桥梁。
    • 实现机制:每个 Dep 实例有一个 subs 数组,用于存储订阅它的所有 Watcher。它提供 depend() 方法来收集当前活跃的 Watcher,以及 notify() 方法来通知所有订阅者更新。
    • 关键细节Dep 类有一个全局唯一的静态属性 Dep.target,它指向当前正在计算的 Watcher(例如正在渲染的组件)。这是一个巧妙的设计,避免了将 Watcher 实例在函数间传递。
  • Watcher (观察者)

    • 职责:代表一个副作用,如组件的渲染函数、computed 属性或用户定义的 watch。它负责执行副作用,并在执行过程中收集依赖。
    • 实现机制
      1. 实例化时,它会将自身设置为 Dep.target
      2. 然后执行其 getter 函数(例如,执行组件的 render 函数)。
      3. 在执行 getter 的过程中,必然会访问响应式数据,从而触发数据的 [[Get]] 内部方法,即我们定义的 getter 拦截器。
      4. 在数据的 getter 拦截器中,会检查 Dep.target 是否存在,如果存在,则调用当前属性 depdepend() 方法,将当前 WatcherDep.target)添加到自己的订阅列表中。同时,Watcher 也会记录这个 dep,形成双向关系。
      5. getter 执行完毕,WatcherDep.target 恢复为上一个状态(可能是另一个 Watcher,因为有嵌套计算的情况),完成依赖收集。
      6. 当数据变化触发 setter,最终会调用 dep.notify(),遍历 subs 中的所有 Watcher,调用它们的 update 方法。Watcherupdate 方法通常会将其放入一个异步队列(nextTick)中,等待所有同步数据变更完成后批量执行,避免不必要的重复计算和渲染。

完整流程与内部方法映射:

  1. 初始化 (observe): 遍历对象,对每个属性调用 Object.defineProperty,重写其 [[Get]][[Set]] 行为。
  2. 依赖收集 (render): 组件渲染 -> 创建 渲染Watcher -> 设置 Dep.target -> 执行 render() -> 触发 [[Get]] -> getter 拦截器调用 dep.depend() -> Watcher 记录 dep
  3. 派发更新 (data change): 修改数据 -> 触发 [[Set]] -> setter 拦截器调用 dep.notify() -> 通知所有 Watcher -> Watcher 排队更新 -> 下一个事件循环执行 getter(重新渲染)并重新收集依赖(因为模板中用到的数据可能变化了)。

Vue2 的局限性根源(从内部方法看):
由于其实现基于 Object.defineProperty,它只能拦截 [[Get]][[Set]]。对于 [[DefineOwnProperty]](属性添加)、[[Delete]](属性删除)、[[OwnPropertyKeys]]for...in)等操作无能为力,必须借助 Vue.set/Vue.delete 等外部 API 来“手动”触发更新,破坏了 JavaScript 的操作原生性。

2. Vue3 的响应式系统:基于完整内部方法拦截的原生代理

Vue3 利用 Proxy 能拦截几乎所有内部方法的特性,构建了一个更强大、更直观的响应式系统。其核心概念从“属性-Dep-Watcher”演变为“目标-效果-依赖映射”。

核心概念深度解析:

  • reactive():

    • 职责:创建对象的深度响应式代理。
    • 实现机制:直接 new Proxy(target, baseHandlers)baseHandlers 中定义了 get, set, deleteProperty, has, ownKeys 等捕捉器,对应拦截 [[Get]], [[Set]], [[Delete]], [[HasProperty]], [[OwnPropertyKeys]] 等内部方法。惰性代理:在 get trap 中,如果获取的值是对象,再递归地调用 reactive() 将其也转为代理。
  • ref():

    • 职责:为基本类型值创建一个响应式引用,也可用于持有对象引用(替换 .value 会触发更新)。
    • 实现机制:创建一个包装对象 { value: ... },然后对这个包装对象调用 reactive()。在模板和 reactive 对象中访问时会自动解包(触发 [[Get]] 并返回 .value 的值)。
  • effect (副作用):

    • 职责:替代 Vue2 的 Watcher,是依赖追踪和执行的单元。组件的 rendercomputedwatchwatchEffect 都是基于 effect 实现的。
    • 实现机制
      1. 执行 effect(fn) 时,会设置一个全局的 activeEffect
      2. 立即执行传入的副作用函数 fn
      3. fn 执行过程中,访问响应式数据会触发 Proxy 的 get trap。
      4. get trap 中,调用 track(target, key) 建立依赖关系。
  • track() (追踪)trigger() (触发):

    • 职责:替代 Vue2 中分散在各个属性 dep 中的逻辑,集中管理整个应用的依赖关系图。
    • 实现机制
      • 数据结构:依赖关系存储在一个嵌套的全局数据结构中:
        targetMap: WeakMap<Target, Map<Key, Set<Effect>>>
        
        WeakMap 的键是原始对象 target,值是一个 Map。这个 Map 的键是 target 的属性 key,值是一个 Set,里面存储了所有依赖于 target[key]effect
      • track(target, key): 如果当前有 activeEffect,则将它添加到 targetMap[target][key] 对应的 Set 中。同时,effect 也会记录它所有的依赖 Set,用于清理失效依赖。
      • trigger(target, key, type): 根据 targetkeytargetMap 中找到对应的 effect 集合,并执行它们。type 参数(如 ‘ADD’, ‘DELETE’)对于优化 for...inArray 的迭代操作至关重要。

完整流程与内部方法映射:

  1. 初始化: const state = reactive({...}) -> 创建 Proxy 代理。
  2. 依赖收集 (setup/render): 组件setup -> 创建渲染 effect -> 执行渲染函数 -> 访问 state.key -> 触发 [[Get]] -> get trap -> 调用 track(state, 'key') -> 将当前 activeEffect (渲染effect) 存入 targetMap[state]['key']Set 中。
  3. 派发更新: 修改 state.key = newValue -> 触发 [[Set]] -> set trap -> 调用 trigger(state, 'key', 'SET') -> 从 targetMap 中找到所有依赖于 state['key']effect -> 将它们调度执行(异步队列)-> 重新运行 effect(重新渲染)-> 重新追踪依赖

Vue3 的优势体现:

  • 全面性:直接拦截 [[Delete]] (deleteProperty trap) 和 [[DefineOwnProperty]] (ownKeys trap 和 set trap 配合),无需 Vue.delete,操作即响应。
  • 性能:惰性代理减少初始化开销;依赖关系存储在全局 WeakMap 中,管理更高效;Proxy 本身由浏览器原生实现,速度更快。
  • 强大能力:可响应式地支持 Map, Set, WeakMap, WeakSet 等集合类型,因为它们的所有方法(如 map.get, map.set, set.add) 也最终映射到内部方法,可被 Proxy 捕获。

Vue 的响应式核心在于依赖的自动追踪与更新触发。Vue2 基于 Object.defineProperty,通过递归拦截对象的 getset 来工作:在 get 中收集依赖(将当前 Watcher 存入属性独有的 Dep 类),在 set 中通知 Dep 去触发所有 Watcher 更新。这套方案需要为每个属性创建 Dep,无法原生拦截数组和属性增删,存在先天不足。
而 Vue3 的精妙之处在于全面拥抱 ES6 的 Proxy。Proxy 能拦截对象几乎所有的操作(包括 get, set, deleteProperty, has 等),实现了真正的“代理”。其依赖收集(track)与更新触发(trigger)机制也因此变得更加统一和高效:所有依赖关系被集中存储在一个全局的 WeakMap 数据结构中,不再需要为每个属性实例化 Dep。当通过 Proxy 访问属性时,在 get 陷阱中调用 track 建立当前活跃的 effect(副作用,相当于 Watcher)与目标属性间的映射;当修改属性时,在 set 等陷阱中调用 trigger,根据全局依赖表精准找出所有需要重新执行的 effect。这使得 Vue3 获得了原生支持数组和属性增删、惰性代理、更优性能等巨大优势,响应式系统也变得更为强大和优雅。

三、Vue3 的进阶响应式 API

在Vue3中除了ref和reactive之外,还提供了更多细粒度的响应式API,像什么shallowReactive、shallowRef、toRef、toRefs,这些API的出现得归功于Proxy,正因为它可以拦截对象得基本操作,所以才能让Vue3实现更灵活的响应式系统。

  • shallowReactive:只代理对象的第一层属性,深层属性将保持原始状态。适用于性能敏感、不需要深度监听的大对象。
  • shallowRef:不代理 .value 指向的对象。如果 .value 是一个对象,其内部变化不会触发响应。
  • toRef / toRefs:为了解决在解构 reactive 对象时会丢失响应性的问题。
    • toRef:为 reactive 对象的某个属性创建一个 ref,这个 ref 会保持对其源属性的响应式连接。
    • toRefs:将 reactive 对象的每个属性都转换为一个 ref。这在从组合式函数返回响应式对象时非常有用,方便使用者解构而不会失去响应性。

四、依赖收集与副作用

1. 核心基础:Effect 与依赖追踪(Effect / track / trigger)

  • 要点:响应式的核心是副作用函数(effect与依赖追踪机制:读时收集依赖(track),写时触发更新(trigger)。运行中的 effect 被标记为 activeEffecttrack 把它注册到 targetMap[target][key]dep(Set);trigger 从该 dep 里取出所有 effect 并调度执行(直接或通过 scheduler)。
  • 下面通过一段极简的伪代码来看它是如何实现的:
const targetMap = new WeakMap();
let activeEffect = null;function effect(fn, options = {}) {const runner = () => {cleanup(runner);activeEffect = runner;fn();activeEffect = null;};runner.deps = []; runner.scheduler = options.scheduler;if (!options.lazy) runner();return runner;
}function track(target, key) {if (!activeEffect) return;let depsMap = targetMap.get(target) || new Map();let dep = depsMap.get(key) || new Set();dep.add(activeEffect);activeEffect.deps.push(dep);targetMap.set(target, depsMap);
}function trigger(target, key) {const depsMap = targetMap.get(target);if (!depsMap) return;const dep = depsMap.get(key);if (dep) dep.forEach(effect => effect.scheduler ? effect.scheduler(effect) : effect());
}function cleanup(effect) {effect.deps.forEach(dep => dep.delete(effect));effect.deps.length = 0;
}

track 在读时收集,trigger 在写时触发;effect 带 scheduler 用于实现批量/异步调度与去重。

2. watchEffect(自动依赖 + 立即执行 + 可停止)

  • 要点:立即执行传入函数,自动收集其在运行期间读取到的所有响应式依赖;依赖变化时重新运行。常用于副作用(日志、订阅、同步到外部)。返回 stop 停止并 cleanup。
  • 伪码 / 使用骨架
function watchEffect(fn, options={}) {const runner = effect(fn, { scheduler: () => queueJob(runner) }); // 不 lazy,默认立即 runreturn () => stop(runner); // stop 执行 cleanup 并移除依赖
}
  • 异步与取消:通过 onInvalidate(在 effect 内部注册 cleanup)来取消旧异步操作:框架在 effect 下次触发前会先调用已有的 cleanup。
  • 面试要点watchEffect=立即执行 + 自动依赖收集;不返回 oldValue,适合“不关心哪个依赖变了,只要有变化就做事”的场景。

3. watch(显式 source、惰性、old/new、onInvalidate、flush)

  • 要点

    • watch(source, cb, { immediate?, deep?, flush? }):先将 source 规范化为 getter(函数 / ref / reactive),创建 lazy effectlazy: true)。
    • 当依赖变化时由 scheduler 调用 job:在 job 中执行 runner 得到 newVal,并把 oldVal 传给用户回调 cb(newVal, oldVal, onInvalidate)oldVal 更新为 newVal。
    • immediate: true:注册时先执行一次 cb(old 可为 undefined 或初始化值)。deep: true:强制遍历收集深依赖(成本高)。flush 控制回调时机(sync/pre/post)。
  • 伪代码

function watch(source, cb, options = {}) {const getter = typeof source === 'function' ? source : () => traverse(source, options.deep);let oldValue, cleanup;const job = () => {const newValue = runner();if (cleanup) cleanup(); // optional cleanup before cbcb(newValue, oldValue, (fn)=> cleanup = fn);oldValue = newValue;};const runner = effect(getter, { lazy: true, scheduler: () => queueJob(job) });if (options.immediate) job(); else oldValue = runner();return () => stop(runner);
}
  • onInvalidate 模式:用户在 cb 中调用 onInvalidate(fn) 注册清理函数,框架会在下一次 job 执行前调用它以作废旧异步。

watch 适合“需要 old/new 值或精确控制副作用时机”的场景;它本质是一个 lazy 的 effect + 用户级 scheduler。

4. computed(lazy + cached + 可被依赖)

  • 要点computed 是一个带缓存的 lazy effect:内部用 effect(getter, { lazy: true, scheduler });首次读取时运行并缓存结果;依赖变化时 scheduler 将 dirty = truetrigger 出去,但 不会立即重算,下次读取时才重算。computed 也可有 setter。
  • 伪代码
function computed(getterOrOptions) {const hasSetter = typeof getterOrOptions === 'object' && getterOrOptions.set;const getter = typeof getterOrOptions === 'function' ? getterOrOptions : getterOrOptions.get;let value, dirty = true;const dep = new Set(); // consumers of this computedconst runner = effect(getter, {lazy: true,scheduler: () => {if (!dirty) {dirty = true;triggerComputed(dep); // notify consumers that computed changed}}});return {get value() {trackComputed(dep); // allow outer effects to depend on this computedif (dirty) { value = runner(); dirty = false; }return value;},set value(v) { if (hasSetter) getterOrOptions.set(v); else throw Error; }};
}

computed = “读时计算、依赖变化时打标记(dirty)、外部读取触发实际计算”;它是实现性能优化(避免重复计算)的关键工具。


文章转载自:

http://TDRPev9r.kkjqx.cn
http://zuonIF6t.kkjqx.cn
http://Rstli8sg.kkjqx.cn
http://joGQtZ3O.kkjqx.cn
http://lO9MyGR1.kkjqx.cn
http://4FvgaO81.kkjqx.cn
http://LY1CIS3c.kkjqx.cn
http://GjL339tW.kkjqx.cn
http://klPGmYnl.kkjqx.cn
http://v04DbnRL.kkjqx.cn
http://CyABNmBr.kkjqx.cn
http://2DhvVK87.kkjqx.cn
http://bDIMWNHJ.kkjqx.cn
http://I5I1CFu1.kkjqx.cn
http://3EJwaJfo.kkjqx.cn
http://sW9qezkW.kkjqx.cn
http://Ix81hjp9.kkjqx.cn
http://CEby4ZzD.kkjqx.cn
http://YB9PIJGI.kkjqx.cn
http://bnRbgLAs.kkjqx.cn
http://U8PImjl2.kkjqx.cn
http://kvrJ4rbM.kkjqx.cn
http://xBr5qEF2.kkjqx.cn
http://uzJVyDUm.kkjqx.cn
http://R6FHGO0f.kkjqx.cn
http://sHc6MqWu.kkjqx.cn
http://R06HBbDf.kkjqx.cn
http://3OmK4Kak.kkjqx.cn
http://OdNXSB2R.kkjqx.cn
http://qGusIZtH.kkjqx.cn
http://www.dtcms.com/a/382613.html

相关文章:

  • Python核心技术开发指南(061)——常用魔术方法
  • 简单概述操作系统的发展
  • 从0开始:STM32F103C8T6开发环境搭建与第一个LED闪烁
  • linux C 语言开发 (九) 进程间通讯--管道
  • LinuxC++项目开发日志——高并发内存池(5-page cache框架开发)
  • MATLAB基于组合近似模型和IPSO-GA的全焊接球阀焊接工艺参数优化研究
  • SpringSecurity的应用
  • 算法题(206):方格取数(动态规划)
  • 第十六周周报
  • [硬件电路-193]:双极型晶体管BJT与场效应晶体管FET异同
  • ID3v2的header中的扩展标头(Extended Header),其Size字段如何计算整个ID3的长度?
  • 【51单片机】【protues仿真】基于51单片机的篮球计时计分器系统
  • Linux -- 权限的理解
  • Java零基础学习Day10——面向对象高级1
  • 解析通过base64 传过来的图片
  • Redis 持久化策略
  • STM32---PWR
  • 0913刷题日记
  • Java基础面试篇(7)
  • 4-机器学习与大模型开发数学教程-第0章 预备知识-0-4 复数与指数形式(欧拉公式)
  • TA-VLA——将关节力矩反馈融入VLA中:无需外部力传感器,即可完成汽车充电器插入(且可多次自主尝试)
  • 从0到1开发一个商用 Agent(智能体),Agent零基础入门到精通!_零基础开发aiagent 用dify从0到1做智能体
  • android 消息队列MessageQueue源码阅读
  • Gtest2025大会学习记录(全球软件测试技术峰会)
  • oneshape acad数据集 sam-dataset
  • 堆(优先队列)
  • 【卷积神经网络详解与实例】7——经典CNN之AlexNet
  • Digital Clock 4,一款免费的个性化桌面数字时钟
  • mysql 必须在逗号分隔字符串和JSON字段之间二选一,怎么选
  • 分布式锁介绍与实现