news 2026/9/22 12:19:10

请检查名称的拼写与两票系统对比选型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
请检查名称的拼写与两票系统对比选型

面试官问Vue响应式原理,别只背源码,这5个关键点才是拿分关键

代码从博客复制过来,npm run dev 一跑,页面白了,控制台报错 Cannot read properties of undefined (reading 'effect')。你盯着屏幕,心里直骂街:这教程怎么写的?明明步骤没错啊。这种“复制粘贴失效”的困境,是无数前端开发者在进阶路上的拦路虎。更扎心的是,当你去准备面试,被问到“Vue 3 响应式原理”时,你只能背诵“Proxy 替代 Object.defineProperty”,却说不清 tracktrigger 具体怎么配合,更无法解释为什么有时候依赖收集失效。

面试必问 的 Vue 响应式原理,绝不仅仅是背几个 API 名字。真正的考点,在于你懂不懂底层数据流,能不能在源码层面定位问题。今天咱们不聊虚的,直接拆解 Vue 3 核心响应式模块 @vue/reactivity 的源码逻辑。我会带你从入口文件切入,剖析 reactiveref 的核心实现,最后手写一个迷你版响应式系统,让你彻底搞懂这套机制。

1. 入口定位:响应式系统的“大门”在哪?

很多人觉得 Vue 源码庞大,无从下手。其实响应式核心逻辑非常集中,就在 packages/reactivity 目录下。如果你下载了 Vue 3 的 GitHub 开源仓库,打开 src 文件夹,你会发现几个关键文件:

  • reactive.ts:处理对象类型的响应式转换。
  • ref.ts:处理基本类型(数字、字符串、布尔)的响应式包装。
  • effect.ts:副作用函数,这是响应式系统的“引擎”,负责收集依赖和执行更新。
  • computed.ts:计算属性,基于 effect 的派生数据。

在 Vue 3 中,响应式系统的入口是 reactive 函数。当你写 const state = reactive({ count: 0 }) 时,实际调用的是 packages/reactivity/src/reactive.ts 中的 createReactiveObject 方法。

这里有一个容易踩坑的细节:reactive 是有缓存的

// packages/reactivity/src/reactive.ts
const reactiveMap = new WeakMap<T, any>();
const shallowReactiveMap = new WeakMap<T, any>();
const rawMap = new WeakMap<any, T>();export function reactive<T extends object>(target: T): Reactive<T> {if (isProxy(target)) return target;return createReactiveObject(target,false,mutableHandlers,mutableKeys,reactiveMap);
}

注意 if (isProxy(target)) return target; 这一行。这意味着如果你对一个已经是响应式的对象再次调用 reactive,它不会创建新的 Proxy,而是直接返回原对象。这个设计保证了响应式对象的唯一性。如果你在代码里不小心对同一个数据源调用了两次 reactive,虽然不会报错,但如果你手动修改了其中一个 Proxy 的引用,可能会导致依赖收集混乱。这也是为什么官方建议不要直接修改 raw 对象,而要始终通过 reactive 返回的代理对象进行操作。

2. 核心片段:Proxy 是如何拦截并触发更新的?

理解了入口,接下来看核心机制。Vue 3 放弃了 Vue 2 的 Object.defineProperty,全面拥抱 Proxy。为什么?因为 Proxy 可以拦截整个对象的操作,包括新增属性、删除属性、数组索引修改等,而 Object.defineProperty 无法监听数组下标变化和对象新增属性。

我们来看 createReactiveObject 的核心逻辑:

// packages/reactivity/src/reactive.ts
function createReactiveObject(target: object,isShallow: boolean,baseHandlers: ProxyHandler<any>,collectionHandlers: ProxyHandler<any>,proxyMap: WeakMap<object, any>
) {if (!isObject(target)) {if (__DEV__) {warn(`value cannot be made reactive: ${String(target)}`);}return target;}if (target[ReactiveFlags.RAW] &&!(isShallow && target[ReactiveFlags.IS_SHALLOW])) {return target;}const existingProxy = proxyMap.get(target);if (existingProxy) return existingProxy;// 关键:创建 Proxy 实例const proxy = new Proxy(target,isObject(target) ? baseHandlers : collectionHandlers);proxyMap.set(target, proxy);rawMap.set(proxy, target);return proxy;
}

这段代码展示了创建 Proxy 的过程。重点在于 baseHandlers,它包含 getset 两个核心方法。让我们深入 baseHandlers 中的 get 操作,看看依赖收集是如何发生的:

// packages/reactivity/src/baseHandlers.ts
const mutableGet: ProxyHandler<object>['get'] = (target, key, receiver) => {const res = Reflect.get(target, key, receiver);if (isSymbol(key)) {// 符号 key 不触发依赖收集return res;}track(target, TrackOpTypes.GET, key);if (isShallow) {return res;}if (isRef(res)) {// 如果是 ref 对象,解包return key === 'value' ? res.value : res;}if (isObject(res)) {// 嵌套对象,递归转为 reactivereturn reactive(res);}return res;
};

逐行解析:

  1. Reflect.get(target, key, receiver):原生读取属性值。
  2. if (isSymbol(key)):Symbol 类型的 key 通常用于内部标记(如 __v_isRef),不需要响应式,直接返回。
  3. track(target, TrackOpTypes.GET, key)这是核心中的核心。它告诉响应式系统:“嘿,当前执行的 effect 函数读取了 targetkey 属性,请把这个 key 记录到当前 effect 的依赖列表里。”
  4. if (isRef(res)):如果属性值本身是一个 ref 对象(比如 const count = ref(0)),访问 state.count 时,get 拦截器会直接返回 res.value,实现了自动解包
  5. if (isObject(res)):如果属性值是一个对象(嵌套对象),递归调用 reactive(res)。这保证了深层对象也是响应式的,且同样有缓存机制,避免重复创建 Proxy。

再看 set 操作,这是触发更新的关键:

// packages/reactivity/src/baseHandlers.ts
const mutableSet: ProxyHandler<object>['set'] = (target, key, value, receiver) => {let hadKey = hasOwn(target, key);const oldValue = target[key];if (!hadKey) {// 新增属性,先触发一次 GET 收集依赖(如果之前没被读取过)track(target, TrackOpTypes.ADD, key);} else if (hasChanged(value, oldValue)) {// 值发生变化,才触发更新trigger(target, TriggerOpTypes.SET, key, value, oldValue);}return Reflect.set(target, key, value, receiver);
};

关键点解析:

  1. hasChanged(value, oldValue):Vue 3 做了优化,只有当新值和旧值不相等(使用 Object.is 比较)时,才触发 trigger。这避免了不必要的渲染开销。
  2. trigger(target, TriggerOpTypes.SET, key, value, oldValue):通知所有依赖了 target.key 的 effect 函数重新执行。

3. 设计思想:Effect 与 Track 的双向绑定

很多初学者只关注 reactiveProxy,却忽略了 effect。实际上,没有 effecttrack 就是无头苍蝇

effect 是一个副作用函数,它包裹了你希望自动更新的逻辑。在 Vue 3 中,组件的 setup 函数或 render 函数内部,都会被 effect 包裹。

// packages/reactivity/src/effect.ts
export class ComputedRefImpl {// ...
}export function effect<T = any>(fn: () => T,options?: ReactiveEffectOptions
): ReactiveEffect {const effect = createReactiveEffect(fn, options);if (!options || !options.lazy) {effect.run();}return effect;
}function createReactiveEffect(fn, options) {const effect = function () {// ...activeEffect = effect; // 设置当前激活的 effectconst result = fn();    // 执行函数,期间会触发 get 拦截activeEffect = null;    // 清空激活的 effectreturn result;};effect.id = uid++;effect.deps = []; // 存储该 effect 依赖的所有 key// ...return effect;
}

核心逻辑流程:

  1. 激活 Effect:当 effect(fn) 执行时,全局变量 activeEffect 被设置为当前的 effect 实例。
  2. 执行 Fnfn() 开始执行。如果 fn 内部读取了 state.count,就会触发 Proxy 的 get 拦截器。
  3. 依赖收集get 拦截器调用 track(target, key)track 函数内部会检查 activeEffect 是否存在。如果存在,就把当前的 key 添加到 activeEffect.deps 数组中,同时把 activeEffect 添加到 target 对应的依赖集合(Map)中。
  4. 触发更新:当 state.count 被修改时,触发 set 拦截器,调用 trigger(target, key)trigger 会找到所有依赖了 target.key 的 effect,并调用它们的 run() 方法。
  5. 重新执行:effect 重新执行 fn(),再次读取 state.count(此时值已更新),从而更新 DOM 或计算结果。

这种设计实现了数据驱动视图的自动化。你不需要手动监听变化,只需要声明“我依赖哪些数据”,框架会自动处理“数据变化时该做什么”。

4. 手写简化版:从零实现迷你响应式系统

为了真正吃透原理,我们手写一个简化版的响应式系统,支持 reactiveeffect。代码尽量贴近 Vue 3 核心逻辑,但去掉了非核心特性(如 Map/Set、数组优化等)。

// mini-reactivity.ts// 全局激活的 effect
let activeEffect: any = null;
// 依赖存储结构: WeakMap<Target, Map<PropertyKey, Set<Effect>>>
const targetMap = new WeakMap();// 1. 依赖收集
function track(target: object, key: symbol | string | number) {// 如果没有激活的 effect,说明不在依赖收集阶段(如初始化)if (!activeEffect) return;let depsMap = targetMap.get(target);if (!depsMap) {depsMap = new Map();targetMap.set(target, depsMap);}let dep = depsMap.get(key);if (!dep) {dep = new Set();depsMap.set(key, dep);}// 将当前 effect 加入依赖集合dep.add(activeEffect);
}// 2. 触发更新
function trigger(target: object, key: symbol | string | number) {const depsMap = targetMap.get(target);if (!depsMap) return;const dep = depsMap.get(key);if (dep) {// 遍历所有依赖该 key 的 effect,并执行// 注意:这里简单处理,实际 Vue 3 有去重和错误处理dep.forEach(effect => effect());}
}// 3. 响应式代理
export function reactive<T extends object>(target: T): T {return new Proxy(target, {get(target, key, receiver) {const res = Reflect.get(target, key, receiver);// 只有非 Symbol 的 key 才进行依赖收集if (typeof key !== 'symbol') {track(target, key);}return res;},set(target, key, value, receiver) {const success = Reflect.set(target, key, value, receiver);if (success) {trigger(target, key);}return success;}});
}// 4. 副作用函数
export function effect(fn: () => void) {// 创建 effect 函数,包裹用户传入的 fnconst effectFn = () => {// 设置当前激活的 effectactiveEffect = effectFn;try {fn();} finally {// 执行完毕,清空激活的 effectactiveEffect = null;}};// 首次执行,收集依赖effectFn();return effectFn;
}

测试代码:

const state = reactive({ count: 0 });// 模拟组件渲染
effect(() => {console.log(`Count: ${state.count}`);
});// 修改数据,触发更新
state.count = 1; // 输出: Count: 1
state.count = 2; // 输出: Count: 2

运行结果:

Count: 0
Count: 1
Count: 2

通过这个简化版,你可以清晰地看到 tracktrigger 是如何通过 activeEffecttargetMap 建立联系的。Vue 3 源码在此基础上增加了栈式管理(支持嵌套 effect)、依赖去重错误边界以及异步调度(nextTick)等复杂逻辑,但核心骨架一致。

5. 应用场景:面试与实战中的高频考点

理解了源码,你在面试和项目中就能游刃有余。以下是几个高频场景:

场景一:为什么 v-for 中修改数组元素不更新?

在 Vue 2 中,直接修改数组下标(如 arr[0] = 1)不触发更新,因为 Object.defineProperty 无法监听。Vue 3 使用 Proxy,set 拦截器能捕获下标修改,所以 arr[0] = 1 是响应式的。但如果你用 state.list.push(item)push 是数组方法,内部修改了 length 和索引,Proxy 的 set 拦截器会分别触发 length 和索引的更新,从而通知视图刷新。

场景二:watchcomputed 的区别?

  • computed:基于 effect 的惰性执行,只在依赖变化时重新计算,且结果有缓存。适合派生数据。
  • watch:监听特定数据变化,执行回调。可以监听深层对象(通过 deep: true,内部会递归 track)。

场景三:如何调试依赖收集失效?

当发现某个数据变化没有触发更新时,可以检查:

  1. 是否使用了 raw 对象而非 proxy 对象?
  2. 是否在 setuprender 之外的地方读取了数据?(必须在 effect 执行期间读取才能收集依赖)
  3. 是否使用了 Symbol 类型的 key?
  4. 值是否真正发生了变化?(Object.is 判断相等则不触发)

场景四:性能优化

  • 避免不必要的响应式:对于大型静态数据,可以使用 shallowReactivemarkRaw,避免深层代理的开销。
  • 使用 computed 代替 watch:如果只是为了派生数据,computed 更轻量,且有缓存机制。
  • 拆分状态:将大型对象拆分为多个小的响应式对象,减少依赖收集的范围。

结语

Vue 3 的响应式系统看似简单,实则蕴含着精妙的设计。从 Proxy 的拦截能力,到 effect 的副作用管理,再到 tracktrigger 的双向绑定,每一个环节都环环相扣。

你在项目里踩过这个坑吗?评论区聊聊:你是否遇到过依赖收集失效的情况?或者你在生产环境中是如何优化响应式性能的?分享你的经验,我们一起避坑!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/22 12:18:51

3个坑坑住转岗人,手写实现阮玲玉故居查询接口优化

3个坑坑住转岗人,手写实现阮玲玉故居查询接口优化 是不是也这样:看了一堆阮玲玉故居相关的开发教程,视频里的代码敲得飞快,一回到自己电脑,面对真实的业务场景就傻眼?特别是那种涉及跨省数据同步、历史档案检索的复杂项目,教程里全是理想化的 SELECT * FROM table…

作者头像 李华
网站建设 2026/9/22 12:18:32

3个坑搞定安居客二手数据抓取,一文搞懂

3个坑搞定安居客二手数据抓取,一文搞懂 配置环境就卡半天?别急,这行代码救你。 很多兄弟一提到【安居客二手】房源数据抓取,第一反应就是头疼。不是被反爬拦截,就是解析出来的字段乱七八糟。我在掘金技术社区看到不少帖子吐槽,说用 Scrapy 跑半天,IP 封了,Cookie…

作者头像 李华
网站建设 2026/9/22 12:18:25

uu酷跑助手安卓性能优化避坑指南:3招解决卡顿与内存溢出

uu酷跑助手安卓性能优化避坑指南:3招解决卡顿与内存溢出 刚学会 Python 或 Java 语法,对着屏幕敲代码很顺手,但一搭真实项目就卡死、崩溃?别慌,这正是从“写 Demo”到“做产品”的鸿沟。在 Android 开发或相关工具链中, uu酷跑助手安卓…

作者头像 李华
网站建设 2026/9/22 12:18:19

别被忽悠了!安全技术类别证书避坑指南,从入门到精通只需这4步

别被忽悠了!安全技术类别证书避坑指南,从入门到精通只需这4步 刚毕业或者刚转行做水利工程的兄弟,是不是经常遇到这种尴尬:简历上写了精通 Python 或 Java,结果面试官问一句“你做过什么完整的安全项目”,你脑子直接一片空白。很多新人死磕语法,把 LeetCode…

作者头像 李华
网站建设 2026/9/22 12:18:15

9个九黎祠性能坑点:面试必问的源码级优化实战

9个九黎祠性能坑点:面试必问的源码级优化实战 面试被问“高并发下接口为什么慢”,你脑子里一片空白,只能背八股文。这不仅是技术短板,更是职业风险。在房建工程数字化领域,像九黎祠这样的BIM协同平台,一旦响应超时,现场进度数据丢失,工程师可能面临执业责任纠纷。面试官追问“原理”,答不上来,不仅丢工作,更…

作者头像 李华
网站建设 2026/9/22 12:17:48

3个死多头陷阱与完整示例:别再被K线骗了

3个死多头陷阱与完整示例:别再被K线骗了 刚学完K线形态,看着“死多头”三个字觉得高深莫测?其实90%的新手都栽在这里。你背下了所有语法,知道什么是均线、什么是MACD,但一到实盘,看着屏幕上的“死多头”形态,手抖着下单,结果第二天直接被套。为什么?因为你只看了形状,没看灵魂。今天不整虚的,直接上…

作者头像 李华