ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

请检查名称的拼写与两票系统对比选型 面试官问Vue响应式原理,别只背源码,这5个关键点才是拿分关键 代码从博客复制过来,npm run dev 一跑,页面白了,控制台报错 Cannot read properties of undefined (reading 'effect')。你盯着屏幕,心里直骂街:这教程怎么写的?明明步骤没错啊。这种“复制粘贴失效”的困境,是无数前端开发者在进阶路上的拦路虎。更扎心的是,当你去准备面试,被问到“Vue 3 响应式原理”时,你只能背诵“Proxy 替代 Object.defineProperty”,却说不清 track 和 trigger 具体怎么配合,更无法解释为什么有时候依赖收集失效。 面试必问 的 Vue 响应式原理,绝不仅仅是背几个 API 名字。真正的考点,在于你懂不懂底层数据流,能不能在源码层面定位问题。今天咱们不聊虚的,直接拆解 Vue 3 核心响应式模块 @vue/reactivity 的源码逻辑。我会带你从入口文件切入,剖析 reactive 和 ref 的核心实现,最后手写一个迷你版响应式系统,让你彻底搞懂这套机制。 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 WeakMapT, any(); const shallowReactiveMap = new WeakMapT, any(); const rawMap = new WeakMapany, T();export function reactiveT extends object(target: T): ReactiveT {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: ProxyHandlerany,collectionHandlers: ProxyHandlerany,proxyMap: WeakMapobject, 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,它包含 get 和 set 两个核心方法。让我们深入 baseHandlers 中的 get 操作,看看依赖收集是如何发生的: // packages/reactivity/src/baseHandlers.ts const mutableGet: ProxyHandlerobject['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; };逐行解析:Reflect.get(target, key, receiver):原生读取属性值。 if (isSymbol(key)):Symbol 类型的 key 通常用于内部标记(如 __v_isRef),不需要响应式,直接返回。 track(target, TrackOpTypes.GET, key):这是核心中的核心。它告诉响应式系统:“嘿,当前执行的 effect 函数读取了 target 的 key 属性,请把这个 key 记录到当前 effect 的依赖列表里。” if (isRef(res)):如果属性值本身是一个 ref 对象(比如 const count = ref(0)),访问 state.count 时,get 拦截器会直接返回 res.value,实现了自动解包。 if (isObject(res)):如果属性值是一个对象(嵌套对象),递归调用 reactive(res)。这保证了深层对象也是响应式的,且同样有缓存机制,避免重复创建 Proxy。再看 set 操作,这是触发更新的关键: // packages/reactivity/src/baseHandlers.ts const mutableSet: ProxyHandlerobject['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); };关键点解析:hasChanged(value, oldValue):Vue 3 做了优化,只有当新值和旧值不相等(使用 Object.is 比较)时,才触发 trigger。这避免了不必要的渲染开销。 trigger(target, TriggerOpTypes.SET, key, value, oldValue):通知所有依赖了 target.key 的 effect 函数重新执行。3. 设计思想:Effect 与 Track 的双向绑定 很多初学者只关注 reactive 和 Proxy,却忽略了 effect。实际上,没有 effect,track 就是无头苍蝇。 effect 是一个副作用函数,它包裹了你希望自动更新的逻辑。在 Vue 3 中,组件的 setup 函数或 render 函数内部,都会被 effect 包裹。 // packages/reactivity/src/effect.ts export class ComputedRefImpl {// ... }export function effectT = 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; }核心逻辑流程:激活 Effect:当 effect(fn) 执行时,全局变量 activeEffect 被设置为当前的 effect 实例。 执行 Fn:fn() 开始执行。如果 fn 内部读取了 state.count,就会触发 Proxy 的 get 拦截器。 依赖收集:get 拦截器调用 track(target, key)。track 函数内部会检查 activeEffect 是否存在。如果存在,就把当前的 key 添加到 activeEffect.deps 数组中,同时把 activeEffect 添加到 target 对应的依赖集合(Map)中。 触发更新:当 state.count 被修改时,触发 set 拦截器,调用 trigger(target, key)。trigger 会找到所有依赖了 target.key 的 effect,并调用它们的 run() 方法。 重新执行:effect 重新执行 fn(),再次读取 state.count(此时值已更新),从而更新 DOM 或计算结果。这种设计实现了数据驱动视图的自动化。你不需要手动监听变化,只需要声明“我依赖哪些数据”,框架会自动处理“数据变化时该做什么”。 4. 手写简化版:从零实现迷你响应式系统 为了真正吃透原理,我们手写一个简化版的响应式系统,支持 reactive 和 effect。代码尽量贴近 Vue 3 核心逻辑,但去掉了非核心特性(如 Map/Set、数组优化等)。 // mini-reactivity.ts// 全局激活的 effect let activeEffect: any = null; // 依赖存储结构: WeakMapTarget, MapPropertyKey, SetEffect 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 reactiveT 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通过这个简化版,你可以清晰地看到 track 和 trigger 是如何通过 activeEffect 和 targetMap 建立联系的。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 和索引的更新,从而通知视图刷新。 场景二:watch 和 computed 的区别?computed:基于 effect 的惰性执行,只在依赖变化时重新计算,且结果有缓存。适合派生数据。 watch:监听特定数据变化,执行回调。可以监听深层对象(通过 deep: true,内部会递归 track)。场景三:如何调试依赖收集失效? 当发现某个数据变化没有触发更新时,可以检查:是否使用了 raw 对象而非 proxy 对象? 是否在 setup 或 render 之外的地方读取了数据?(必须在 effect 执行期间读取才能收集依赖) 是否使用了 Symbol 类型的 key? 值是否真正发生了变化?(Object.is 判断相等则不触发)场景四:性能优化避免不必要的响应式:对于大型静态数据,可以使用 shallowReactive 或 markRaw,避免深层代理的开销。 使用 computed 代替 watch:如果只是为了派生数据,computed 更轻量,且有缓存机制。 拆分状态:将大型对象拆分为多个小的响应式对象,减少依赖收集的范围。结语 Vue 3 的响应式系统看似简单,实则蕴含着精妙的设计。从 Proxy 的拦截能力,到 effect 的副作用管理,再到 track 和 trigger 的双向绑定,每一个环节都环环相扣。 你在项目里踩过这个坑吗?评论区聊聊:你是否遇到过依赖收集失效的情况?或者你在生产环境中是如何优化响应式性能的?分享你的经验,我们一起避坑!
返回列表