一、如何在 React 中实现类似 Vue 的响应式?
面试者真正应该说出的答案:
核心就是自己实现一套
Proxy + 依赖收集 + 更新通知,然后把数据变化转换成 React 的更新信号,让依赖这个数据的组件重新渲染。
如果继续追问:
Vue 是响应式数据主动通知组件更新,React 本身不是这种模型,所以关键不是把 Vue 的代码搬过来,而是解决“数据变化之后,怎么接入 React 的更新机制”。
二、Vue 的响应式到底在做什么?
先别急着讲 React。Vue 3 的核心可以压缩成:
Proxy ↓ get ↓ 依赖收集 track ↓ 建立: 数据 → 哪些响应式副作用依赖它 ↓ set ↓ 触发 trigger ↓ 重新执行相关副作用例如:
conststate=reactive({count:0});effect(()=>{console.log(state.count);});第一次执行:
effect()↓ 读取 state.count ↓ Proxy.get ↓track(state,'count')↓ 记录当前 effect于是内部相当于形成:
state.count ↓ effect1以后:
state.count++;进入:
Proxy.set ↓ trigger(state, 'count') ↓ 找到依赖 count 的 effect1 ↓ 重新执行 effect1所以 Vue 响应式真正核心不是Proxy。而是:
读取时收集依赖,修改时找到依赖并触发它。
三、真正重要的是“依赖关系”怎么存
这是这道题真正开始拉开差距的地方。不能简单说:
constdeps=[];因为实际要解决的是:
state.user.name state.user.age state.count state.list每一个属性可能对应完全不同的依赖。可以抽象成:
WeakMap └── target └── Map ├── key: count │ └── Set(effectA, effectB) │ ├── key: user │ └── Set(effectA) │ └── key: list └── Set(effectC)也就是:
target ↓ key ↓ Dep ↓ effects伪代码:
consttargetMap=newWeakMap();functiontrack(target,key){letdepsMap=targetMap.get(target);if(!depsMap){depsMap=newMap();targetMap.set(target,depsMap);}letdep=depsMap.get(key);if(!dep){dep=newSet();depsMap.set(key,dep);}dep.add(activeEffect);}修改:
functiontrigger(target,key){constdepsMap=targetMap.get(target);constdep=depsMap?.get(key);if(!dep)return;dep.forEach(effect=>{effect();});}这就是最核心的:
track ↓ 收集“谁依赖我” trigger ↓ 通知“依赖我的人”四、React 为什么不能直接照搬?
这是面试里最值得讲的一层。Vue 的模型更接近:
数据 ↓ 响应式系统 ↓ 找到依赖它的 effect ↓ 重新执行 effect ↓ 更新 UIReact 更接近:
state / external store ↓ React 更新机制 ↓ 重新 render ↓ React Reconciliation ↓ commit所以:
proxy.count++;不会天然让 React 组件重新渲染。
React 不会因为你改了一个普通对象属性,就自动知道:
“哦,这个组件依赖这个属性,我应该重新 render。”因此必须自己建立一座桥:
Proxy 响应式系统 ↓ 数据发生变化 ↓ 通知 React ↓ React 重新渲染五、最简单的 React 实现方式
可以先用useState理解。例如:
functionuseReactive(store){const[,forceUpdate]=React.useState(0);React.useEffect(()=>{returnstore.subscribe(()=>{forceUpdate(v=>v+1);});},[store]);returnstore.proxy;}然后:
functionCounter(){conststate=useReactive(store);return(<button onClick={()=>{state.count++;}}>{state.count}</button>);}数据变化:
state.count++ ↓ Proxy.set ↓ trigger ↓ 通知订阅者 ↓ forceUpdate ↓ React render这样就把两套模型接起来了。
六、但这里还有一个非常重要的问题:到底谁应该收到通知?
假设:
function A() { return <div>{store.count}</div>; } function B() { return <div>{store.name}</div>; }现在:
store.count++;我们当然希望:
A 更新 B 不更新而不是:
store 任意属性变化 ↓ 所有组件重新渲染否则所谓响应式就失去了意义。所以依赖收集必须做到:
count ↓ ComponentA name ↓ ComponentB而不是:
store ↓ ComponentA ComponentB ComponentC ComponentD七、React 中怎么收集“当前组件”?
这是最容易说错的地方。
不能简单说:
Proxy.get 的时候记录当前组件实例。
因为函数组件没有 Vue 那种可以直接拿来 forceUpdate 的组件实例模型。
更准确的思路是:
React 开始执行组件 ↓ 建立当前订阅上下文 ↓ 执行 render ↓ render 访问 store.count ↓ Proxy.get ↓ 记录这个订阅者依赖 count ↓ render 完成 ↓ 清理/更新订阅关系例如可以抽象成:
letactiveSubscriber=null;functionrenderComponent(component){activeSubscriber=component;try{returncomponent();}finally{activeSubscriber=null;}}然后:
get(target,key){if(activeSubscriber){track(target,key,activeSubscriber);}returnReflect.get(target,key);}这样:
function User() { return <div>{store.user.name}</div>; }访问:
store.user.name就可以建立:
user.name ↓ User八、嵌套对象怎么办?
这也是面试官非常容易继续追问的地方。例如:
conststate=reactive({user:{name:'Tom',age:20}});如果只代理第一层:
newProxy(state,...)那么:
state.user.name='Jack';里面的:
user.name并没有经过第一层 Proxy 的set。
所以必须做深层代理。
例如:
functionreactive(target){returnnewProxy(target,{get(target,key,receiver){constvalue=Reflect.get(target,key,receiver);track(target,key);if(value&&typeofvalue==='object'){returnreactive(value);}returnvalue;},set(target,key,value,receiver){constoldValue=target[key];constresult=Reflect.set(target,key,value,receiver);if(!Object.is(oldValue,value)){trigger(target,key);}returnresult;}});}实际工程实现还需要缓存 Proxy,否则:
state.user===state.user可能变成:
false所以通常还需要:
原对象 ↓ WeakMap ↓ Proxy缓存代理。
九、真正完整的架构应该是什么?
如果让你现场画图,我建议直接画这个:
React │ 执行组件 render │ ▼ 访问 reactive store │ ▼ Proxy.get │ ▼ track() │ ▼ 建立“数据 → 订阅者”关系 │ │ ┌───────────┴───────────┐ │ │ state.count state.user.name │ │ ▼ ▼ ComponentA ComponentB 数据修改 │ ▼ Proxy.set │ ▼ trigger() │ ▼ 找到对应订阅者 │ ├──────────────┐ ▼ ▼ ComponentA ComponentB │ ▼ 通知 React │ ▼ React 更新 │ ▼ 重新 render这里真正需要自己实现的是:
Proxy + 依赖收集 + 依赖触发 + 订阅管理而 React 负责:
调度更新 + 重新 render + Diff + Commit十、生产级实现为什么更推荐useSyncExternalStore?
如果面试官继续问:
“你这个
useState + forceUpdate能不能用于生产?”
这里就要升级答案。
不建议把forceUpdate当成最终方案。
React 官方针对“React 之外存在一个可订阅的数据源”提供了:
useSyncExternalStore它的定位就是:
让 React 组件安全地订阅外部 store。
因此架构可以变成:
Reactive Store │ ┌────────┴────────┐ │ │ Proxy.get Proxy.set │ │ ▼ ▼ track() trigger() │ ▼ notify listeners │ ▼ useSyncExternalStore │ ▼ React │ ▼ render这就比:
forceUpdate()更加符合 React 的设计。
十一、但这里还有一个更难的问题:粒度
假设:
function App() { return ( <> <User /> <Counter /> </> ); }其中:
function User() { return <div>{store.user.name}</div>; }function Counter() { return <div>{store.count}</div>; }修改:
store.count++;理想情况:
count ↓ Counter ↓ 重新 render而不是:
count ↓ App ↓ User ↓ Counter ↓ 全部重新 render所以响应式系统真正高级的地方不是:
“能不能更新。”
而是:
“更新能不能足够精准。”
这就是所谓的更新粒度。
十二、Selector 是一种常见解决方案
例如:
constcount=useStore(store,state=>state.count);constuserName=useStore(store,state=>state.user.name);内部可以理解成:
ComponentA ↓ selector(state) ↓ state.count ComponentB ↓ selector(state) ↓ state.user.name当:
state.count++只检查:
ComponentA是否需要更新。这就是:
让组件订阅自己真正关心的数据,而不是订阅整个 store。
十三、响应式系统最容易踩的坑
如果面试官继续追问,可以直接展开这些。
1. 嵌套对象
state.user.name需要深层 Proxy。
2. Proxy 重复创建
不能每次:
get(){returnnewProxy(value,...)}否则:
state.user===state.user都可能不成立。需要:
WeakMap<object,Proxy>缓存。
3. 数组
需要处理:
push pop splice shift unshift以及:
length index这些变化对应的依赖。
4. 删除属性
例如:
deletestate.user.name;不能只监听:
getset还需要处理:
deleteProperty5. 组件卸载
如果:
ComponentA ↓ count后来 ComponentA 卸载了:
count ↓ 还保存着 ComponentA就可能造成:
无效订阅 + 内存泄漏 + 无意义更新所以必须在订阅生命周期结束时清理依赖。
6. 派生数据
例如:
consttotal=computed(()=>{returnstate.price*state.count;});这又会引入:
依赖缓存 + 失效 + 重新计算7. 更新死循环
例如:
effect(()=>{state.count++;});就可能形成:
effect ↓ 修改 state ↓ trigger ↓ effect ↓ 修改 state ↓ trigger ...因此实际响应式系统还需要处理:
递归更新 + 重复触发 + 批量更新 + 调度十四、MobX 和自己实现有什么区别?
MobX 已经把 Proxy/Observable、依赖收集、更新触发以及 React 订阅这些事情封装好了,所以业务侧只需要声明哪些数据是 observable,再让组件订阅它。
自己造轮子:
Proxy + track + trigger + 订阅管理 + React 更新桥接 + 嵌套对象 + 数组 + 生命周期 + 批量更新 + 内存管理所以:
自己实现不难,难的是把边界情况和更新粒度做完整。
十五、这道题真正考什么?
这道题表面问:
React 怎么实现 Vue 的响应式?
实际上考的是三层能力。
第一层:懂 Vue
知道:
Proxy ↓ track ↓ trigger而不是只知道:
reactive()第二层:懂 React
知道 React 不是:
对象变了 ↓ 组件自动更新而是需要把外部数据变化接入 React 的更新机制。
第三层:能做跨框架设计
能够把:
Vue 的响应式模型转换成:
React 的外部 store + React 更新机制这才是这道题真正筛选高级工程师的地方。
十六、面试官追问链
这道题可以一路追成:
React 怎么实现 Vue 类似的响应式? ↓ Vue 响应式核心是什么? ↓ Proxy 做了什么? ↓ 依赖怎么收集? ↓ 依赖数据结构怎么设计? ↓ 数据变化怎么通知组件? ↓ React 函数组件怎么 forceUpdate? ↓ 为什么不直接 forceUpdate? ↓ useSyncExternalStore 是干什么的? ↓ 多个组件依赖同一个数据怎么办? ↓ 如何做到精准更新? ↓ 嵌套对象怎么办? ↓ 数组怎么办? ↓ 组件卸载后怎么清理? ↓ 如何避免重复 Proxy? ↓ 如何避免死循环? ↓ 如何批量更新? ↓ 自己实现和 MobX 有什么区别?这条追问链下来,基本就能判断一个人到底是:
只会用 MobX还是:
理解响应式系统十七、最后真正建议背的答案
不要把上面所有东西都塞进第一句话。
真正面试时先说:
React 里如果想做类似 Vue 的响应式,核心就是自己实现
Proxy + 依赖收集 + 触发更新,然后把数据变化通过订阅机制接入 React,让真正依赖这份数据的组件重新渲染。
如果面试官继续问:
Vue 是在数据读取时收集依赖,数据修改时找到对应依赖并触发;React 本身没有这种对象属性变化自动驱动组件的模型,所以我们需要自己维护“数据 → 订阅者”的关系,再通过
useSyncExternalStore之类的方式把外部 store 的变化接入 React。真正难的不是 Proxy,而是依赖粒度、嵌套对象、生命周期清理以及如何避免无关组件更新。
这比:
“Proxy 劫持 + forceUpdate”
高一个层级。
最关键的一句话其实是:
Proxy 解决“我什么时候知道数据变了”,依赖收集解决“谁依赖这个数据”,React 订阅机制解决“数据变了以后怎么让组件更新”。
这三件事一串起来,这道题就真正讲透了。