news 2026/10/8 6:47:18

面试题:如何在 React 中实现类似 Vue 的响应式?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
面试题:如何在 React 中实现类似 Vue 的响应式?

一、如何在 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 ↓ 更新 UI

React 更接近:

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

还需要处理:

deleteProperty

5. 组件卸载

如果:

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 订阅机制解决“数据变了以后怎么让组件更新”。

这三件事一串起来,这道题就真正讲透了。

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

2027创新计算机选题:SmartCampusOS 智慧校园一体化服务平台

1. 选题背景 2027年&#xff0c;高校数字化正从"多系统堆砌"走向"一体化智能服务"。学生、教师、管理者面对数十个APP&#xff0c;数据孤岛严重、体验割裂。随着大模型与Agent技术成熟&#xff0c;"一句话办事"的校园服务成为可能。同时&#xf…

作者头像 李华
网站建设 2026/10/8 6:47:01

程序员到架构师工作和生活反思

一转眼&#xff0c;2026年国庆节马上要结束了&#xff0c;时间过的真快&#xff0c;就像2010年毕业到现在已经在我厂已经干超过16年了&#xff0c;真快啊。记得2010年研究生毕业坐火车十几个小时到深圳入职&#xff0c;第一次收到工资大几千&#xff0c;对我这个穷学生来说真是…

作者头像 李华
网站建设 2026/10/8 6:46:06

时序Transformer长期预测实战:工业级多步端到端建模

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 6:45:51

自己写一个内核(一):最小引导扇区

我的专栏直达《机器人学导论》专栏点击进入《自己写一个内核》专栏点击进入自己写一个内核&#xff08;一&#xff09;&#xff1a;最小引导扇区 按下电源。 CPU 醒过来&#xff0c;干的第一件事不是加载操作系统——它先去跑 BIOS。BIOS 是焊在主板 ROM 里的程序&#xff0c…

作者头像 李华