react面试题
约 15997 字大约 53 分钟
2025-06-01
- 18不在支持IE
- 18 之前,批处理
只限于 React 原生事件内部
的更新。18 中,批处理支持处理的操作范围扩大了:Promise,setTimout,native event handler
等这些非 React 原生事件。
1.并发渲染
React 18的核心更新是引入并发渲染能力,允许React在渲染过程中中断并优先处理更高优先级的任务(比如用户输入),从而提升响应速度。
关键机制
- 时间切片:将渲染任务拆分成小块,避免长时间阻塞主线程
- 优先级调度:用户交互(如点击)优先于数据更新(如API响应)
相关API
startTransition
:标记非紧急更新,可被高优先级任务打断
js 体验AI代码助手 代码解读复制代码import { startTransition } from 'react';
const handleSearch = (query) => {
startTransition(() => {
setSearchQuery(query); // 非紧急更新
});
};
useTransition
:跟踪过渡状态,显示加载指示
js 体验AI代码助手 代码解读复制代码const [isPending, startTransition] = useTransition();
return (
<button onClick={() => startTransition(handleClick)}>
{isPending ? '加载中...' : '提交'}
</button>
);
2.自动批处理
React 18默认将多次状态更新合并为单次渲染,减少不必要的重复渲染
更新场景对比
场景 | React 17 及之前 | React 18 |
---|---|---|
事件处理器中的更新 | 批量处理 | 批量处理 |
setTimeout/Promise 中的更新 | 不批量处理 | 批量处理 |
强制同步更新
js 体验AI代码助手 代码解读复制代码import { flushSync } from 'react-dom';
flushSync(() => {
setCount(c => c + 1); // 立即同步渲染
});
3.流式服务端渲染
通过分块传输HTML和选择性水合,显著提升服务端渲染性能
核心改进
- 流式传输:服务器逐步发送HTML到客户端,缩短首屏时间
- 并行水合:客户端在接收HTML时逐步激活交互功能,无需等待全部内容加载
js 体验AI代码助手 代码解读复制代码import { renderToPipeableStream } from 'react-dom/server';
app.get('/', (req, res) => {
const stream = renderToPipeableStream(<App />, {
onShellReady() {
res.setHeader('Content-type', 'text/html');
stream.pipe(res);
},
bootstrapScripts: ['/main.js'] // 客户端脚本
});
});
4.新根API与严格模式增强
1.新根API createRoot
替换原有的ReactDOM.render
,启用并发功能
js 体验AI代码助手 代码解读复制代码// React 17
ReactDOM.render(<App />, document.getElementById('root'));
// React 18
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
2.严格模式
- 模拟卸载-挂载:开发环境下组件挂载后立即卸载再重新挂载,检测副作用清理问题
- 重复渲染检测:故意双调用组件函数和Hook,暴露潜在副作用
5.新Hook与API
1.useId
- 生成唯一ID,解决服务端与客户端ID不一致的问题
js 体验AI代码助手 代码解读复制代码const id = useId(); // 生成如 :r1: 的稳定 ID
return <label htmlFor={id}>用户名</label>;
2.useDeferredValue
- 延迟更新非关键UI,保持高优先级交互流程
js 体验AI代码助手 代码解读复制代码const deferredQuery = useDeferredValue(query);
return <SearchResults query={deferredQuery} />;
3.useSyncExternalStore
- 简化外部状态库(如Redux)集成,避免并发渲染下的撕裂问题
js
体验AI代码助手
代码解读
复制代码const state = useSyncExternalStore(store.subscribe, store.getState);
6.改进的2Suspense功能
- 支持在SSR中与流式渲染结合,优化体验
js 体验AI代码助手 代码解读复制代码<Suspense fallback={<Spinner />}>
<Comments /> {/* 异步加载的组件 */}
</Suspense>
2.React事件机制和原生DOM事件流有什么区别
react中的事件是绑定到document上面的,
而原生的事件是绑定到dom上面的,
因此相对绑定的地方来说,dom上的事件要优先于document上的事件执行
3.JSX
1.JSX的本质
- 定义:
JSX(JavaScript XML)
是React提供的语法扩展,允许在JavaScript中编写类似HTML的结构 - 核心作用:提升代码可读性,直观描述UI的层次结构,同时保留JavaScript的全部编程能力
- 底层实现:JSX会被Babel或TypeScript编译器转换为
React.createElement()
调用,生成React元素(即虚拟DOM对象)
// JSX代码
const element = <div className="title">Hello React</div>;
// 编译后的JavaScript
const element = React.createElement(
'div',
{ className:'title' },
'Hello React'
)
1.为什么JSX中组件首字母必须大写?
- React通过首字母大小写区分原生DOM标签(如
<div>
)和自定义组件(如<MyComponent>
)
2.如何避免JSX回调中的闭包陷阱?
- 使用
useCallback
缓存函数,或通过函数参数传递最新值
4.生命周期
1.生命周期阶段
1.Mounting阶段
- constructor:初始化state、绑定方法
- getDerivedStateFromProps:props初始化时同步到state
- render:生成虚拟DOM
- componentDidMount:网络请求、DOM操作、订阅事件
2.Updating阶段
- getDerviedStateFromProps:props变化时更新state
- shouldComponentUpdate:返回false可阻止渲染(性能优化核心)
- render:生成新虚拟DOM
- getSnapshotBeforeUpdate:获取DOM更新前的状态(如滚动位置)
- componentDidUpdate:DOM更新后操作、网络请求
3.Unmounting阶段
- componentWillUnmount:清除定时器、取消订阅、释放资源等
2.函数组件生命周期模拟
通过useEffect Hook实现生命周期控制:
function Example() {
// Mounting
useEffect(() => {
// componentDidMount
const timer = setInterval(...);
return () => { // componentWillUnmount
clearInterval(timer);
}
}, []);
// Updating
useEffect(() => {
// componentDidUpdate(任意状态变化时)
});
useEffect(() => {
// 特定状态变化时执行(替代 componentDidUpdate)
}, [count]);
// getDerivedStateFromProps 模拟
const [derivedState, setDerivedState] = useState();
useEffect(() => {
setDerivedState(props.input);
}, [props.input]);
}
3.高频面试题
1.为什么componentWillXXX系列方法被标记为UNSAFE
- 异步渲染模式(Concurrent Mode)下可能被多次调用
- 副作用操作可能会导致渲染不一致
2.useEffect与生命周期方法的对应关系
useEffect(fn, []) -> componentDidMount + componentWillUnmount
useEffect(fn) -> componentDidUpdate
useEffect(fn, [dep]) -> 特定依赖更新时的副作用
3.getDerivedStateFromProps的正确使用场景
- 仅当需要根据props变化被动更新state时使用
- 避免在此方法中触发副作用
5.受控组件与非受控组件
1.核心定义
组件类型 | 数据管理方式 | 控制权 |
---|---|---|
受控组件 (Controlled) | 表单数据由 React 组件状态(state)驱动 | React 完全控制 |
非受控组件 (Uncontrolled) | 表单数据由 DOM 节点自身维护 | DOM 原生控制 |
2.实现原理对比
1.受控组件实现
js 体验AI代码助手 代码解读复制代码function ControlledForm() {
const [value, setValue] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log('提交值:', value);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button type="submit">提交</button>
</form>
);
}
核心特点
- value绑定到React state
- onChange同步更新state
- 数据流:
React state -> DOM
显示
2.非受控组件实现
js 体验AI代码助手 代码解读复制代码function UncontrolledForm() {
const inputRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
console.log('提交值:', inputRef.current.value);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
defaultValue="初始值"
ref={inputRef}
/>
<button type="submit">提交</button>
</form>
);
}
核心特点
- 使用ref访问DOM节点值
- defaultValue设置初始值(非动态更新)
- 数据流:DOM节点 -> 手动获取值
3.核心差异分析
维度 | 受控组件 | 非受控组件 |
---|---|---|
数据存储位置 | React 组件状态 | DOM 节点 |
值更新机制 | 通过 onChange 事件同步更新 state | 用户输入直接修改 DOM,需手动获取 |
表单验证时机 | 实时验证(每次输入触发) | 提交时验证 |
动态表单控制 | 支持动态禁用/启用字段 | 需要手动操作 DOM |
性能影响 | 高频输入场景可能引发多次渲染 | 无额外渲染开销 |
文件上传支持 | 不支持(文件输入天生不可控) | 必须使用 |
4.最佳实践场景
受控组件适用场景
- 实时表单验证(如密码强度提示)
- 条件禁用提交按钮
- 动态表单字段(根据输入增减表单项)
- 复杂表单联动(多个输入相互依赖)
非受控组件适用场景
- 一次性表单提交(只需要最终值)
- 文件上传
<input type="file">
- 第三方库集成(需要直接操作DOM)
5.进阶
1.为什么文件输入必须用非受控组件?
js
体验AI代码助手
代码解读
复制代码<input type="file" onChange={handleFile} />
- 浏览安全限制:JavaScript无法以编程方式设置文件输入的值
- 只读属性:文件路径由用户选择,无法通过React state控制
2.如何给非受控组件设置初始值?
- 使用
defaultValue/defaultChecked
属性(类似原生HTML)
js
体验AI代码助手
代码解读
复制代码<input type="text" defaultValue="初始值" ref={inputRef} />
3.受控组件性能优化策略
- 防抖处理(避免高频触发渲染)
js 体验AI代码助手 代码解读复制代码const debouncedSetValue = useMemo(() =>
_.debounce(setValue, 300), []
);
<input onChange={e => debouncedSetValue(e.target.value)} />
- 精细化渲染控制:使用React.memo隔离表单组件
6.混用模式
js 体验AI代码助手 代码解读复制代码function HybridInput({ value: propValue, onChange }) {
const [internalValue, setInternalValue] = useState(propValue);
const ref = useRef();
// 同步外部传入的值
useEffect(() => {
if (ref.current.value !== propValue) {
ref.current.value = propValue;
setInternalValue(propValue);
}
}, [propValue]);
const handleChange = (e) => {
setInternalValue(e.target.value);
onChange?.(e.target.value);
};
return <input ref={ref} value={internalValue} onChange={handleChange} />;
}
7.高频面试题
1.如何避免受控组件的value变成undefined?
- 确保value的值始终为受控值(字符串/数字),避免null或undefined
2.受控组件中如何实现文本域(textarea)的换行符保留
- 使用
value={ text.replace(/\n/g, '\\n') }
处理,展示时转换回\n
3.为什么非受控组件不需要onChange处理?
- 非受控组件的数据流是单向的(DOM -> React),仅在需要时通过ref获取值
6.类组件和函数式组件
1.生命周期与Hooks映射关系
类组件生命周期方法
class Example extends React.Component {
componentDidMount() { /* 挂载完成 */ }
componentDidUpdate() { /* 更新完成 */ }
componentWillUnmount() { /* 卸载前 */ }
shouldComponentUpdate() { /* 决定是否渲染 */ }
}
函数式组件等效实现
function Example() {
// 模拟 componentDidMount + componentWillUnmount
useEffect(() => {
// 挂载逻辑
return () => { /* 卸载逻辑 */ };
}, []);
// 模拟 componentDidUpdate
useEffect(() => { /* 更新逻辑 */ });
// 模拟 shouldComponentUpdate
const memoizedComponent = React.memo(() => (/* 组件 */), (prevProps, nextProps) => {
return shallowEqual(prevProps, nextProps);
});
}
2.性能优化策略
优化手段 | 类组件 | 函数式组件 |
---|---|---|
浅比较控制渲染 | PureComponent 或 shouldComponentUpdate | React.memo + 自定义比较函数 |
计算缓存 | 手动缓存计算结果 | useMemo |
函数引用稳定性 | 箭头函数或 bind | useCallback |
渲染节流 | 手动实现防抖/节流 | useDebounce 自定义 Hook |
3.闭包陷阱(函数式组件)
js 体验AI代码助手 代码解读复制代码function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// 始终捕获初始值 count=0
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // ❌ 错误:缺少依赖项
// 修复方案:
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // ✅ 使用函数式更新
}, 1000);
return () => clearInterval(timer);
}, []);
}
4.高频面试题
1.为什么推荐使用函数式组件?
- 代码简洁:避免this绑定和类语法冗余
- 逻辑复用:自定义Hooks比HOC更灵活
- 性能优化:Hooks提供更细颗粒度的控制(如useMemo)
- 未来兼容:新特性(如并发模式)优先支持Hooks
2.如何选择组件类型?
- 新项目:100%函数式组件+Hooks
- 旧项目:逐步迁移至函数式组件
- 特殊需求:需要getSnapshotBeforeUpdate或componentDidCatch时使用类组件
3.Hooks的限制和突破
- 规则:只能在函数顶层调用Hooks
- 原理:依赖调用顺序的链表结构记录状态
- 解决方案:使用eslint-plugin-react-hooks强制规范
7.路由高级技巧与最佳实践
1.代码分割与懒加载
- 结合
React.lazy
和Suspense
实现按需加载
js 体验AI代码助手 代码解读复制代码const Users = React.lazy(() => import('./Users'));
<Route path="/users" element={
<Suspense fallback={<Loading />}>
<Users />
</Suspense>
} />
2.传递状态至路由组件
- 使用state属性传递
js 体验AI代码助手 代码解读复制代码navigate('/profile', { state: { userData: data } });
// 在目标组件中获取
const location = useLocation();
const userData = location.state?.userData;
3.使用useRoutes配置式路由(V6)
- 集中管理路由配置
js 体验AI代码助手 代码解读复制代码const routes = useRoutes([
{ path: '/', element: <Home /> },
{ path: 'users', element: <Users />, children: [
{ path: ':id', element: <UserDetail /> }
]}
]);
return routes;
6.高频面试题
1.BrowserRouter和HashRouter有什么区别?
- BrowserRouter使用
HTML5 History API
,URL格式为example.com/path
- HashRouter使用
URL Hash(example/#/path)
,兼容旧浏览器但不够美观
2.React-Router V6有哪些重大改进
- 引入Routes代替Switch,提升匹配逻辑
- 嵌套路由配置更简洁,支持相对路径
- 移除Redirect,改用Navigate
- element属性直接传递JSX元素,支持更灵活的组件组合
3.如何实现路由鉴权
- 使用高阶组件或自定义路由组件包裹需要保护的路径,检查用户权限后决定渲染目标组件还是重定向到登录页
4.为什么需要exact属性(V5)
- 确保路径完全匹配。例如,path="/" 没有exact时会匹配所有以/开头的URL。v6中默认精确匹配,无需此属性。
8、性能优化
1.优化核心思路
- 减少不必要的渲染:避免组件因无关状态/props变化而重新渲染
- 降低计算开销:缓存高成本计算和函数引用
- 优化DOM操作:减少实际DOM的无效变更
- 合理加载资源:按需加载关键代码与数据
1.组件渲染控制
- React.memo:缓存函数组件渲染结果
js 体验AI代码助手 代码解读复制代码const MemoComponent = React.memo(Component, (prevProps, nextProps) => {
return prevProps.id === nextProps.id; // 自定义比较函数
});
2.高效数据管理
- useMemo:缓存复杂计算结果
js
体验AI代码助手
代码解读
复制代码const processedData = useMemo(() => transformData(rawData), [rawData]);
- useCallback:稳定函数引用
js 体验AI代码助手 代码解读复制代码const handleClick = useCallback(() => {
submitData(id);
}, [id]); // id 变化时生成新函数
- 状态结构优化:扁平化数据,避免深层嵌套
js 体验AI代码助手 代码解读复制代码// ❌ 避免深层嵌套
const [data, setData] = useState({ user: { profile: { name: '' } } });
// ✅ 拆分为独立状态
const [userName, setUserName] = useState('');
3.列表渲染优化
- key属性优化:使用唯一稳定标识
js 体验AI代码助手 代码解读复制代码{items.map(item => (
<ListItem key={item.id} data={item} /> // 避免使用索引作为 key
))}
- 虚拟列表技术:使用react-window实现
js 体验AI代码助手 代码解读复制代码import { FixedSizeList as List } from 'react-window';
<List height={600} itemSize={35} itemCount={1000}>
{({ index, style }) => <div style={style}>Row {index}</div>}
</List>
4.资源加载优化
- 代码分割:动态加载非关键组件
js 体验AI代码助手 代码解读复制代码const LazyComponent = React.lazy(() => import('./HeavyComponent'));
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>
- 图片懒加载:使用
Intersection Observer
const LazyImage = ({ src }) => { // [2,3](@ref)
const imageRef = useRef(); // [3,4](@ref)
useEffect(() => {
// 1. 创建 IntersectionObserver 实例
const observer = new IntersectionObserver((entries) => { // [2,3,4](@ref)
// 2. 检查目标元素是否进入视口
if (entries[0].isIntersecting) { // [3,4](@ref)
// 3. 当图片进入视口时,设置真实 src
imageRef.current.src = src; // [2,3](@ref)
// 4. 停止观察该元素(避免重复加载)
observer.unobserve(imageRef.current); // [3,4](@ref)
}
});
// 5. 开始观察目标元素
observer.observe(imageRef.current); // [3,4](@ref)
// 6. 清理函数:组件卸载时断开观察器
return () => observer.disconnect(); // [3,4](@ref)
}, []); // 空依赖数组:仅在挂载时执行一次
// 7. 返回未设置 src 的 img 标签(初始不加载图片)
return <img ref={imageRef} alt="Lazy loaded" />; // [2,3](@ref)
}
关键代码解析:
useRef()
- 创建 DOM 引用对象,用于获取
<img>
元素实例 - 替代直接操作 DOM,符合 React 范式
- 创建 DOM 引用对象,用于获取
IntersectionObserver
核心逻辑new IntersectionObserver((entries) => {...}, options)
entries[0]
:观察的目标元素(本例仅观察一个元素)isIntersecting
:布尔值,表示目标是否进入视口unobserve()
:加载后解除观察,优化性能
useEffect
依赖与清理空依赖数组
[]
:确保观察器只在组件挂载时初始化一次返回 :组件卸载时释放资源,避免内存泄漏
observer.disconnect()
初始渲染策略
- 首屏渲染时
<img>
不带src
属性,避免立即加载 - 当元素进入视口后动态设置
src
触发加载
- 首屏渲染时
4.高级优化技术
1.并发模式优化(React18+)
- 自动批处理:合并多个状态更新
js 体验AI代码助手 代码解读复制代码// React 18 前:setCount + setValue 触发两次渲染
// React 18:自动合并为一次渲染
const handleClick = () => {
setCount(c => c + 1);
setValue(v => v + 1);
};
- useTransition:区分紧急/非紧急更新
js 体验AI代码助手 代码解读复制代码const [isPending, startTransition] = useTransition();
const handleSearch = (query) => {
startTransition(() => {
setSearchQuery(query); // 标记为非紧急更新
});
};
2.服务端渲染优化
- 流式SSR:分块传输HTML
js 体验AI代码助手 代码解读复制代码// Next.js示例
export async function getServerSideProps() {
return {
props: { data: await fetchData() },
};
}
- 静态生成:预渲染静态页面
js 体验AI代码助手 代码解读复制代码export async function getStaticProps() {
return { props: { /***/ }, recalidate: 60 }; // ISR 增量静态再生
}
5.常见性能陷阱与方案
问题 | 解决方案 |
---|---|
不必要的 Context 更新 | 拆分多个 Context,使用 useMemo 缓存 Provider 值 |
过度使用 useMemo | 仅对高开销计算使用,避免对小计算进行不必要的缓存 |
内存泄漏 | 及时清理定时器、事件监听器、订阅等副作用 |
大状态对象更新 | 使用 Immer 实现不可变更新,避免深拷贝性能问题 |
6.性能优化黄金法则
- 优先解决关键瓶颈:使用Profiler定位主要性能
- 避免过早优化:在出现可测量的性能问题后再实施优化
- 保持组件简单:单一职责组件更易优化
- 渐进增强:优先优化首屏关键路径,再处理次要内容
9. React render 方法原理?在什么时候触发?
render函数里面可以编写JSX,转化成createElement这种形式,用于生成虚拟DOM,最终转化成真实DOM
在 React 中,类组件只要执行了 setState 方法,就一定会触发 render 函数执行,函数组件使用useState更改状态不一定导致重新render
组件的 props 改变了,不一定触发 render 函数的执行,
但是如果 props 的值来自于父组件或者祖先组件的 state,在这种情况下,父组件或者祖先组件的 state 发生了改变,就会导致子组件的重新渲染
所以,一旦执行了setState就会执行render方法,useState 会判断当前值有无发生改变确定是否执行render方法,一旦父组件发生渲染,子组件也会渲染
10. React JSX 转换成真实 DOM 的过程?
- 使用
React.createElement
或JSX编写React组件,实际上所有的 JSX 代码最后都会转换成React.createElement(...) ,Babel
帮助我们完成了这个转换的过程。 - createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM对象
ReactDOM.render
将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制
并且对特定浏览器进行了性能优化,最终转换为真实DOM
11. React 服务端渲染(SSR)原理?
- node server 接收客户端请求,得到当前的请求 url 路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props、context或者store 形式传入组件
- 然后基于 react 内置的服务端渲染方法 renderToString() 把组件渲染为 html 字符串在把最终的 html 进行输出前需要将数据注入到浏览器端
- 浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束
12. 常用的 React Hooks
状态钩子 (useState)
: 用于定义组件的 State,类似类定义中 this.state 的功能useReducer
:用于管理复杂状态逻辑的替代方案,类似于 Redux 的 reducer。生命周期钩子 (useEffect)
: 类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做componentDidMount、componentDidUpdate和componentWillUnmount的结合。useLayoutEffect
:与 useEffect 类似,但在浏览器完成绘制之前同步执行。useContext
: 获取 context 对象,用于在组件树中获取和使用共享的上下文。useCallback
: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;useMemo
: 用于缓存计算结果,避免重复计算昂贵的操作。useRef
: 获取组件的真实节点;用于在函数组件之间保存可变的值,并且不会引发重新渲染。useImperativeHandle
:用于自定义暴露给父组件的实例值或方法。useDebugValue
:用于在开发者工具中显示自定义的钩子相关标签。
13. useEffect VS useLayoutEffect
使用场景
:
useEffect
在 React 的渲染过程中是被异步调用的,用于绝大多数场景;useLayoutEffect
会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。- 也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较大的耗时任务从而造成阻塞。
使用效果
:
useEffect
是按照顺序执行代码的,改变屏幕像素之后执行(先渲染,后改变DOM),当改变屏幕内容时可能会产生闪烁;useLayoutEffect
是改变屏幕像素之前就执行了(会推迟页面显示的事件,先改变DOM后渲染),不会产生闪烁。useLayoutEffect总是比useEffect先执行。
在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。
14. JSX 的本质是什么?
JSX(JavaScript XML) 是一个 JavaScript 的语法扩展,允许在 JavaScript 代码中通过类 HTML 语法创建 React 元素。它需要通过 Babel 等工具编译为标准的 JavaScript 代码,最终生成 React 元素对象(React Element),这些元素共同构成虚拟 DOM(Virtual DOM)树。
核心原理
JSX 编译为 React 元素 JSX 会被转换为
React.createElement()
调用(或 React 17+ 的_jsx
函数),生成描述 UI 结构的对象(React 元素),而非直接操作真实 DOM。jsx 体验AI代码助手 代码解读复制代码// JSX const element = <h1 className="title">Hello, world!</h1> // 编译后(React 17 之前) const element = React.createElement('h1', { className: 'title' }, 'Hello, world!') // 编译后(React 17+,自动引入 _jsx) import { jsx as _jsx } from 'react/jsx-runtime' const element = _jsx('h1', { className: 'title', children: 'Hello, world!' })
虚拟 DOM 的运作
- React 元素组成虚拟 DOM 树,通过 Diff 算法对比新旧树差异,最终高效更新真实 DOM。
- 虚拟 DOM 是内存中的轻量对象,避免频繁操作真实 DOM 的性能损耗。
JSX 的核心特性
类 HTML 语法与 JavaScript 的融合
表达式嵌入
:通过
{}
嵌入 JavaScript 表达式(如变量、函数调用、三元运算符):
jsx 体验AI代码助手 代码解读复制代码const userName = 'Alice' const element = <p>Hello, {userName.toUpperCase()}</p>
禁止语句:
{}
内不支持
if
/
for
等语句,需改用表达式(如三元运算符或逻辑与):
jsx 体验AI代码助手 代码解读 复制代码<div>{isLoggedIn ? 'Welcome' : 'Please Login'}</div>
语法规则
- 属性命名:使用驼峰命名(如
className
代替class
,htmlFor
代替for
)。 - 闭合标签:所有标签必须显式闭合(如
<img />
)。 - 单一根元素:JSX 必须有唯一根元素(或用
<></>
空标签包裹)。
- 属性命名:使用驼峰命名(如
安全性
默认 XSS 防护:JSX 自动转义嵌入内容中的特殊字符(如
<
转为<
)。例外场景
:如需渲染原始 HTML,需显式使用
dangerouslySetInnerHTML
(需谨慎):
jsx 体验AI代码助手 代码解读 复制代码<div dangerouslySetInnerHTML={{ __html: userContent }} />
编译与工具链
编译流程 JSX 需通过 Babel 编译为浏览器可执行的 JavaScript。典型配置如下:
json 体验AI代码助手 代码解读复制代码// .babelrc { "presets": ["@babel/preset-react"] }
React 17+ 的优化
- 无需手动导入 React:编译器自动引入
_jsx
函数。 - 更简洁的编译输出:减少代码体积,提升可读性。
- 无需手动导入 React:编译器自动引入
15. 如何理解 React Fiber 架构?
- Fiber 架构的本质与设计目标
Fiber 是 React 16+ 的核心算法重写,本质是基于链表的增量式协调模型。其核心目标并非单纯提升性能,而是重构架构以实现:
- 可中断的异步渲染:将同步递归的调和过程拆解为可暂停/恢复的异步任务。
- 优先级调度:高优先级任务(如用户输入)可打断低优先级任务(如数据更新)。
- 并发模式基础:为
Suspense
、useTransition
等特性提供底层支持。
- Fiber 节点的核心设计
每个组件对应一个 Fiber 节点,构成双向链表树结构,包含以下关键信息:
- 组件类型:函数组件、类组件或原生标签。
- 状态与副作用:Hooks 状态(如
useState
)、生命周期标记(如useEffect
)。 - 调度信息:任务优先级(
lane
模型)、到期时间(expirationTime
)。 - 链表指针:
child
(子节点)、sibling
(兄弟节点)、return
(父节点)。
javascript 体验AI代码助手 代码解读复制代码// Fiber 节点结构简化示例
const fiberNode = {
tag: FunctionComponent, // 组件类型
stateNode: ComponentFunc, // 组件实例或 DOM 节点
memoizedState: {
/* Hooks 链表 */
},
pendingProps: {
/* 待处理 props */
},
lanes: Lanes.HighPriority, // 任务优先级
child: nextFiber, // 子节点
sibling: null, // 兄弟节点
return: parentFiber, // 父节点
}
- Fiber 协调流程(两阶段提交)
阶段 1:Reconciliation(协调/渲染阶段)
可中断的增量计算
:
React 将组件树遍历拆解为多个
Fiber 工作单元
,通过循环(而非递归)逐个处理。
- 每次循环执行一个 Fiber 节点,生成子 Fiber 并连接成树。
- 通过
requestIdleCallback
(或 Scheduler 包)在浏览器空闲时段执行,避免阻塞主线程。
对比策略: 根据
key
和type
复用节点,标记Placement
(新增)、Update
(更新)、Deletion
(删除)等副作用。
阶段 2:Commit(提交阶段)
不可中断的 DOM 更新: 同步执行所有标记的副作用(如 DOM 操作、生命周期调用),确保 UI 一致性。
副作用分类
:
- BeforeMutation:
getSnapshotBeforeUpdate
。 - Mutation:DOM 插入/更新/删除。
- Layout:
useLayoutEffect
、componentDidMount
/Update
。
- BeforeMutation:
- 优先级调度机制
React 通过 Lane 模型 管理任务优先级(共 31 个优先级车道):
事件优先级
:
javascript 体验AI代码助手 代码解读复制代码// 优先级从高到低 ImmediatePriority(用户输入) UserBlockingPriority(悬停、点击) NormalPriority(数据请求) LowPriority(分析日志) IdlePriority(非必要任务)
调度策略
:
- 高优先级任务可抢占低优先级任务的执行权。
- 过期任务(如 Suspense 回退)会被强制同步执行。
- Fiber 架构的优势与局限性
优势
- 流畅的用户体验:异步渲染避免主线程阻塞,保障高优先级任务即时响应。
- 复杂场景优化:支持大规模组件树的高效更新(如虚拟滚动、动画串联)。
- 未来特性基础:为并发模式(Concurrent Mode)、离线渲染(SSR)提供底层支持。
局限性
- 学习成本高:开发者需理解底层调度逻辑以优化性能。
- 内存开销:Fiber 树的双向链表结构比传统虚拟 DOM 占用更多内存。
- 与旧架构的关键差异
特性 | Stack Reconciler(React 15-) | Fiber Reconciler(React 16+) |
---|---|---|
遍历方式 | 递归(不可中断) | 循环(可中断 + 恢复) |
任务调度 | 同步执行,阻塞主线程 | 异步分片,空闲时段执行 |
优先级控制 | 无 | 基于 Lane 模型的优先级抢占 |
数据结构 | 虚拟 DOM 树 | Fiber 链表树(含调度信息) |
16. Fiber 结构和普通 VNode 有什么区别?
- 本质差异
维度 | 普通 VNode(虚拟 DOM) | Fiber 结构 |
---|---|---|
设计目标 | 减少真实 DOM 操作,提升渲染性能 | 实现可中断的异步渲染 + 优先级调度 |
数据结构 | 树形结构(递归遍历) | 双向链表树(循环遍历) |
功能范畴 | 仅描述 UI 结构 | 描述 UI 结构 + 调度任务 + 副作用管理 |
- 数据结构对比
普通 VNode(React 15 及之前)
javascript 体验AI代码助手 代码解读复制代码const vNode = {
type: 'div', // 节点类型(组件/原生标签)
props: { className: 'container' }, // 属性
children: [vNode1, vNode2], // 子节点(树形结构)
key: 'unique-id', // 优化 Diff 性能
// 无状态、调度、副作用信息
}
- 核心字段:仅包含 UI 描述相关属性(type、props、children)。
Fiber 节点(React 16+)
javascript 体验AI代码助手 代码解读复制代码const fiberNode = {
tag: HostComponent, // 节点类型(函数组件/类组件/DOM元素)
type: 'div', // 原生标签或组件构造函数
key: 'unique-id', // Diff 优化标识
stateNode: domNode, // 关联的真实 DOM 节点
pendingProps: { className: 'container' }, // 待处理的 props
memoizedProps: {}, // 已生效的 props
memoizedState: {
// Hooks 状态(函数组件)
hooks: [state1, effectHook],
},
updateQueue: [], // 状态更新队列(类组件)
lanes: Lanes.HighPriority, // 调度优先级(Lane 模型)
child: childFiber, // 第一个子节点
sibling: siblingFiber, // 下一个兄弟节点
return: parentFiber, // 父节点(构成双向链表)
effectTag: Placement, // 副作用标记(插入/更新/删除)
nextEffect: nextEffectFiber, // 副作用链表指针
}
核心扩展
:
- 调度控制:
lanes
优先级、任务到期时间。 - 状态管理:Hooks 链表(函数组件)、类组件状态队列。
- 副作用追踪:
effectTag
标记和副作用链表。 - 遍历结构:
child
/sibling
/return
构成双向链表。
- 调度控制:
- 协调机制对比
流程 | VNode(Stack Reconciler) | Fiber Reconciler |
---|---|---|
遍历方式 | 递归遍历(不可中断) | 循环遍历链表(可中断 + 恢复) |
任务调度 | 同步执行,阻塞主线程 | 异步分片,空闲时间执行 |
优先级控制 | 无 | Lane 模型(31 个优先级车道) |
副作用处理 | 统一提交 DOM 更新 | 构建副作用链表,分阶段提交 |
Fiber 两阶段提交
:
协调阶段
(可中断):
- 增量构建 Fiber 树,标记副作用(
effectTag
)。 - 通过
requestIdleCallback
或 Scheduler 包分片执行。
- 增量构建 Fiber 树,标记副作用(
提交阶段
(同步不可中断):
- 遍历副作用链表,执行 DOM 操作和生命周期方法。
能力扩展示例
a. 支持 Hooks 状态管理
- Fiber 节点通过
memoizedState
字段存储 Hooks 链表:
javascript 体验AI代码助手 代码解读复制代码// 函数组件的 Hooks 链表
fiberNode.memoizedState = {
memoizedState: 'state value', // useState 的状态
next: {
// 下一个 Hook(如 useEffect)
memoizedState: { cleanup: fn },
next: null,
},
}
- VNode 无状态管理能力,仅描述 UI。
b. 优先级调度实战
高优先级任务抢占
:
javascript 体验AI代码助手 代码解读复制代码// 用户输入触发高优先级更新 input.addEventListener('input', () => { React.startTransition(() => { setInputValue(e.target.value) // 低优先级 }) // 高优先级更新立即执行 })
VNode 架构无法实现任务中断和优先级插队。
c. 副作用批处理
Fiber 通过
effectList
链表收集所有变更,统一提交:
javascript 体验AI代码助手 代码解读复制代码// 提交阶段遍历 effectList let nextEffect = fiberRoot.firstEffect while (nextEffect) { commitWork(nextEffect) nextEffect = nextEffect.nextEffect }
VNode 架构在 Diff 后直接操作 DOM,无批处理优化。
- 性能影响对比
场景 | VNode 架构 | Fiber 架构 |
---|---|---|
大型组件树渲染 | 主线程阻塞导致掉帧 | 分片渲染,保持 UI 响应 |
高频更新(如动画) | 多次渲染合并困难 | 基于优先级合并或跳过中间状态 |
SSR 水合(Hydration) | 全量同步处理 | 增量水合,优先交互部分 |
17. 简述 React diff 算法过程
React Diff 算法通过 分层对比策略 和 启发式规则 减少树对比的时间复杂度(从 O(n³) 优化至 O(n))。其核心流程如下:
1. 分层对比策略
React 仅对 同一层级的兄弟节点 进行对比,若节点跨层级移动(如从父节点 A 移动到父节点 B),则直接 销毁并重建,而非移动。 原因:跨层操作在真实 DOM 中成本极高(需递归遍历子树),而实际开发中跨层移动场景极少,此策略以概率换性能。
2. 节点类型比对规则
a. 元素类型不同
若新旧节点类型不同(如 <div>
→ <span>
或 ComponentA
→ ComponentB
),则:
- 销毁旧节点及其子树。
- 创建新节点及子树,并插入 DOM。
jsx 体验AI代码助手 代码解读复制代码// 旧树
<div>
<ComponentA />
</div>
// 新树 → 直接替换
<span>
<ComponentB />
</span>
b. 元素类型相同
若类型相同,则复用 DOM 节点并更新属性:
原生标签:更新
className
、style
等属性。组件类型
:
- 类组件:保留实例,触发
componentWillReceiveProps
→shouldComponentUpdate
等生命周期。 - 函数组件:重新执行函数,通过 Hooks 状态判断是否需更新。
- 类组件:保留实例,触发
jsx 体验AI代码助手 代码解读复制代码// 旧组件(保留实例并更新 props)
<Button className="old" onClick={handleClick} />
// 新组件 → 复用 DOM,更新 className 和 onClick
<Button className="new" onClick={newClick} />
3. 列表节点的 Key 优化
处理子节点列表时,React 依赖 key 进行最小化更新:
a. 无 key 时的默认行为
默认使用 索引匹配(index-based diff),可能导致性能问题:
jsx 体验AI代码助手 代码解读复制代码// 旧列表
;[<div>A</div>, <div>B</div>][
// 新列表(首部插入)→ 索引对比导致 B 被误判更新
((<div>C</div>), (<div>A</div>), (<div>B</div>))
]
此时 React 会认为索引 0 从 A → C(更新),索引 1 从 B → A(更新),并新增索引 2 的 B,实际应仅插入 C。
b. 使用 key 的优化匹配
通过唯一 key 标识节点身份,React 可精准识别移动/新增/删除:
jsx 体验AI代码助手 代码解读复制代码// 正确使用 key(如数据 ID)
<ul>
{items.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
匹配规则:
- 遍历新列表,通过 key 查找旧节点:
- 找到且类型相同 → 复用节点。
- 未找到 → 新建节点。
- 记录旧节点中未被复用的节点 → 执行删除。
c. 节点移动优化
若新旧列表节点仅顺序变化,React 通过 key 匹配后,仅执行 DOM 移动操作(非重建),例如:
jsx 体验AI代码助手 代码解读复制代码// 旧列表:A (key=1), B (key=2)
// 新列表:B (key=2), A (key=1)
// React 仅交换 DOM 顺序,而非销毁重建
4. 性能边界策略
- 子树跳过:若父节点类型变化,其子节点即使未变化也会被整体销毁。
- 相同组件提前终止:若组件
shouldComponentUpdate
返回false
,则跳过其子树 Diff。
18. 简述 React 和 Vue diff 算法的区别
React 和 Vue 的 Diff 算法均基于虚拟 DOM,但在实现策略、优化手段和设计哲学上存在显著差异:
1. 核心算法策略对比
维度 | React | Vue 2/3 |
---|---|---|
遍历方式 | 单向递归(同层顺序对比) | 双端对比(头尾指针优化) |
节点复用 | 类型相同则复用,否则销毁重建 | 类型相同则尝试复用,优先移动而非重建 |
静态优化 | 需手动优化(如 React.memo ) | 编译阶段自动标记静态节点 |
更新粒度 | 组件级更新(默认) | 组件级 + 块级(Vue3 Fragments) |
2. 列表 Diff 实现细节
a. React 的索引对比策略
无 key 时
:按索引顺序对比,可能导致无效更新
jsx 体验AI代码助手 代码解读复制代码// 旧列表:[A, B, C] // 新列表:[D, A, B, C](插入头部) // React 对比结果:更新索引 0-3,性能低下
有 key 时
:通过 key 匹配节点,减少移动操作
jsx 体验AI代码助手 代码解读 复制代码// key 匹配后,仅插入 D,其他节点不更新
b. Vue 的双端对比策略
分四步优化对比效率(Vue2 核心逻辑,Vue3 优化为最长递增子序列):
- 头头对比:新旧头指针节点相同则复用,指针后移
- 尾尾对比:新旧尾指针节点相同则复用,指针前移
- 头尾交叉对比:旧头 vs 新尾,旧尾 vs 新头
- 中间乱序对比:建立 key-index 映射表,复用可匹配节点
js 体验AI代码助手 代码解读复制代码// 旧列表:[A, B, C, D]
// 新列表:[D, A, B, C]
// Vue 通过步骤3头尾对比,仅移动 D 到头部
3. 静态优化机制
a. Vue 的编译时优化
静态节点标记: 模板中的静态节点(无响应式绑定)会被编译为常量,跳过 Diff
html 体验AI代码助手 代码解读复制代码<!-- 编译前 --> <div>Hello Vue</div> <!-- 编译后 --> _hoisted_1 = createVNode("div", null, "Hello Vue")
Block Tree(Vue3): 动态节点按区块(Block)组织,Diff 时仅对比动态部分
b. React 的运行时优化
手动控制更新
:
需通过
React.memo
、
shouldComponentUpdate
或
useMemo
避免无效渲染
jsx 体验AI代码助手 代码解读 复制代码const MemoComp = React.memo(() => <div>Static Content</div>)
4. 响应式更新触发
框架 | 机制 | Diff 触发条件 |
---|---|---|
React | 状态变化触发组件重新渲染 | 父组件渲染 → 子组件默认递归 Diff |
Vue | 响应式数据变更触发组件更新 | 依赖收集 → 仅受影响组件触发 Diff |
javascript 体验AI代码助手 代码解读复制代码// Vue:只有 data.value 变化才会触发更新
const vm = new Vue({ data: { value: 1 } })
// React:需显式调用 setState
const [value, setValue] = useState(1)
5. 设计哲学差异
维度 | React | Vue |
---|---|---|
控制粒度 | 组件级控制(开发者主导) | 细粒度依赖追踪(框架主导) |
优化方向 | 运行时优化(Fiber 调度) | 编译时优化(模板静态分析) |
适用场景 | 大型动态应用(需精细控制) | 中小型应用(快速开发) |
19. 为何 React JSX 循环需要使用 key
?
- 元素的高效识别与复用
React 通过 key
唯一标识列表中的每个元素。当列表发生变化(增删改排序)时,React 会通过 key
快速判断:
- 哪些元素是新增的(需要创建新 DOM 节点)
- 哪些元素是移除的(需要销毁旧 DOM 节点)
- 哪些元素是移动的(直接复用现有 DOM 节点,仅调整顺序)
如果没有 key
,React 会默认使用数组索引(index
)作为标识,这在动态列表中会导致 性能下降 或 状态错误。
- 避免状态混乱
如果列表项是 有状态的组件(比如输入框、勾选框等),错误的 key
会导致状态与错误的内容绑定。例如:
jsx 体验AI代码助手 代码解读复制代码// 如果初始列表是 [A, B],用索引 index 作为 key:
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
// 在头部插入新元素变为 [C, A, B] 时:
// React 会认为 key=0 → C(重新创建)
// key=1 → A(复用原 key=0 的 DOM,但状态可能残留)
// 此时,原本属于 A 的输入框状态可能会错误地出现在 C 中。
- 提升渲染性能
通过唯一且稳定的 key
(如数据 ID),React 可以精准判断如何复用 DOM 节点。如果使用随机数或索引,每次渲染都会强制重新创建所有元素,导致性能浪费。
20. React 事件和 DOM 事件有什么区别?
- 事件绑定方式
React 事件 使用驼峰命名法(如
onClick
、onChange
),通过 JSX 属性直接绑定函数:jsx 体验AI代码助手 代码解读 复制代码<button onClick={handleClick}>点击</button>
DOM 事件 使用全小写命名(如
onclick
、onchange
),通过字符串或addEventListener
绑定:html 体验AI代码助手 代码解读 复制代码<button onclick="handleClick()">点击</button>
javascript 体验AI代码助手 代码解读 复制代码button.addEventListener('click', handleClick)
- 事件对象(Event Object)
React 事件 使用合成事件(SyntheticEvent),是原生事件对象的跨浏览器包装。
- 通过
e.nativeEvent
访问原生事件。 - 事件对象会被复用(事件池机制),异步访问需调用
e.persist()
。
jsx 体验AI代码助手 代码解读复制代码const handleClick = (e) => { e.persist() // 保持事件对象引用 setTimeout(() => console.log(e.target), 100) }
- 通过
DOM 事件 直接使用浏览器原生事件对象,无复用机制。
javascript 体验AI代码助手 代码解读复制代码button.addEventListener('click', (e) => { console.log(e.target) // 直接访问 })
- 事件传播与默认行为
React 事件
- 阻止默认行为:必须显式调用
e.preventDefault()
。 - 阻止冒泡:调用
e.stopPropagation()
。
jsx 体验AI代码助手 代码解读复制代码const handleSubmit = (e) => { e.preventDefault() // 阻止表单默认提交 e.stopPropagation() // 阻止事件冒泡 }
- 阻止默认行为:必须显式调用
DOM 事件
- 阻止默认行为:可调用
e.preventDefault()
或return false
(在 HTML 属性中)。 - 阻止冒泡:调用
e.stopPropagation()
或return false
(仅部分情况)。
html 体验AI代码助手 代码解读复制代码<form onsubmit="return false"> <!-- 阻止默认提交 --> <button onclick="event.stopPropagation()">按钮</button> </form>
- 阻止默认行为:可调用
- 性能优化
- React 事件 采用事件委托机制:
- React 17 之前将事件委托到
document
层级。 - React 17+ 改为委托到渲染的根容器(如
ReactDOM.render
挂载的节点)。 - 减少内存占用,动态添加元素无需重新绑定事件。
- React 17 之前将事件委托到
- DOM 事件 直接绑定到元素,大量事件监听时可能导致性能问题。
- 跨浏览器兼容性
- React 事件 合成事件抹平了浏览器差异(如
event.target
的一致性),无需处理兼容性问题。 - DOM 事件 需手动处理浏览器兼容性(如 IE 的
attachEvent
vs 标准addEventListener
)。
this
绑定
React 事件 类组件中需手动绑定
this
或使用箭头函数:jsx 体验AI代码助手 代码解读复制代码class MyComponent extends React.Component { handleClick() { console.log(this) // 需绑定,否则为 undefined } render() { return <button onClick={this.handleClick.bind(this)}>点击</button> } }
DOM 事件 事件处理函数中的
this
默认指向触发事件的元素:javascript 体验AI代码助手 代码解读复制代码button.addEventListener('click', function () { console.log(this) // 指向 button 元素 })
特性 | React 事件 | DOM 事件 |
---|---|---|
命名规则 | 驼峰命名(onClick ) | 全小写(onclick ) |
事件对象 | 合成事件(SyntheticEvent ) | 原生事件对象 |
默认行为阻止 | e.preventDefault() | e.preventDefault() 或 return false |
事件委托 | 自动委托到根容器 | 需手动实现 |
跨浏览器兼容 | 内置处理 | 需手动适配 |
this 指向 | 类组件中需手动绑定 | 默认指向触发元素 |
React 事件系统通过抽象和优化,提供了更高效、一致的事件处理方式,避免了直接操作 DOM 的繁琐和兼容性问题。
21. 简述 React batchUpdate 机制
React 的 batchUpdate(批处理更新)机制 是一种优化策略,旨在将多个状态更新合并为一次渲染,减少不必要的组件重新渲染次数,从而提高性能。
核心机制
- 异步合并更新 当在 同一执行上下文(如同一个事件处理函数、生命周期方法或 React 合成事件)中多次调用状态更新(如
setState
、useState
的setter
函数),React 不会立即触发渲染,而是将多个更新收集到一个队列中,最终合并为一次更新,统一计算新状态并渲染。 - 更新队列 React 内部维护一个更新队列。在触发更新的代码块中,所有状态变更会被暂存到队列,直到代码执行完毕,React 才会一次性处理队列中的所有更新,生成新的虚拟 DOM,并通过 Diff 算法高效更新真实 DOM。
触发批处理的场景
React 合成事件 如
onClick
、onChange
等事件处理函数中的多次状态更新会自动批处理。jsx 体验AI代码助手 代码解读复制代码const handleClick = () => { setCount(1) // 更新入队 setName('Alice') // 更新入队 // 最终合并为一次渲染 }
React 生命周期函数 在
componentDidMount
、componentDidUpdate
等生命周期方法中的更新会被批处理。React 18+ 的自动批处理增强 React 18 引入
createRoot
后,即使在异步操作(如setTimeout
、Promise
、原生事件回调)中的更新也会自动批处理:jsx 体验AI代码助手 代码解读复制代码setTimeout(() => { setCount(1) // React 18 中自动批处理 setName('Alice') // 合并为一次渲染 }, 1000)
绕过批处理的场景
React 17 及之前的异步代码 在
setTimeout
、Promise
或原生事件回调中的更新默认不会批处理,每次setState
触发一次渲染:jsx 体验AI代码助手 代码解读复制代码// React 17 中会触发两次渲染 setTimeout(() => { setCount(1) // 渲染一次 setName('Alice') // 渲染第二次 }, 1000)
手动强制同步更新 使用
flushSync
(React 18+)可强制立即更新,绕过批处理:jsx 体验AI代码助手 代码解读复制代码import { flushSync } from 'react-dom' flushSync(() => { setCount(1) // 立即渲染 }) setName('Alice') // 再次渲染
设计目的
- 性能优化 避免频繁的 DOM 操作,减少浏览器重绘和回流,提升应用性能。
- 状态一致性 确保在同一个上下文中多次状态变更后,组件最终基于最新的状态值渲染,避免中间状态导致的 UI 不一致。
示例对比
自动批处理(React 18+)
jsx 体验AI代码助手 代码解读复制代码const handleClick = () => { setCount((prev) => prev + 1) // 更新入队 setCount((prev) => prev + 1) // 更新入队 // 最终 count 增加 2,仅一次渲染 }
非批处理(React 17 异步代码)
jsx 体验AI代码助手 代码解读复制代码setTimeout(() => { setCount((prev) => prev + 1) // 渲染一次 setCount((prev) => prev + 1) // 再渲染一次 // React 17 中触发两次渲染,count 仍为 2 }, 1000)
场景 | React 17 及之前 | React 18+(使用 createRoot ) |
---|---|---|
合成事件/生命周期 | 自动批处理 | 自动批处理 |
异步操作 | 不批处理 | 自动批处理 |
原生事件回调 | 不批处理 | 自动批处理 |
React 的批处理机制通过合并更新减少了渲染次数,但在需要即时反馈的场景(如动画)中,可通过 flushSync
强制同步更新。
:::
22. 简述 React 事务机制
React 的 事务机制(Transaction) 是早期版本(React 16 之前)中用于 批量处理更新 和 管理副作用 的核心设计模式,其核心思想是通过“包装”操作流程,确保在更新过程中执行特定的前置和后置逻辑(如生命周期钩子、事件监听等)。随着 React Fiber 架构的引入,事务机制逐渐被更灵活的调度系统取代。
核心概念
事务的定义 事务是一个包含 初始化阶段、执行阶段 和 收尾阶段 的流程控制单元。每个事务通过
Transaction
类实现,提供initialize
和close
方法,用于在操作前后插入逻辑。例如:javascript 体验AI代码助手 代码解读复制代码const MyTransaction = { initialize() { /* 前置操作(如记录状态) */ }, close() { /* 后置操作(如触发更新) */ }, }
包装函数 事务通过
perform
方法执行目标函数,将其包裹在事务的生命周期中:javascript 体验AI代码助手 代码解读复制代码function myAction() { /* 核心逻辑(如调用 setState) */ } MyTransaction.perform(myAction)
在 React 中的应用场景
批量更新(Batching Updates) 在事件处理或生命周期方法中,多次调用
setState
会被事务合并为一次更新。例如:javascript 体验AI代码助手 代码解读复制代码class Component { onClick() { // 事务包裹下的多次 setState 合并为一次渲染 this.setState({ a: 1 }) this.setState({ b: 2 }) } }
生命周期钩子的触发 在组件挂载或更新时,事务确保
componentWillMount
、componentDidMount
等钩子在正确时机执行。事件系统的委托 合成事件(如
onClick
)的处理逻辑通过事务绑定和解绑,确保事件监听的一致性和性能优化。
事务的工作流程
- 初始化阶段 执行所有事务的
initialize
方法(如记录当前 DOM 状态、锁定事件监听)。 - 执行目标函数 运行核心逻辑(如用户定义的
setState
或事件处理函数)。 - 收尾阶段 执行所有事务的
close
方法(如对比 DOM 变化、触发更新、解锁事件)。
事务机制的局限性
- 同步阻塞 事务的执行是同步且不可中断的,无法支持异步优先级调度(如 Concurrent Mode 的时间切片)。
- 复杂性高 事务的嵌套和组合逻辑复杂,难以维护和扩展。
Fiber 架构的演进 React 16 引入的 Fiber 架构 替代了事务机制,核心改进包括:
- 异步可中断更新 通过 Fiber 节点的链表结构,支持暂停、恢复和优先级调度。
- 更细粒度的控制 将渲染拆分为多个阶段(如
render
和commit
),副作用管理更灵活。 - 替代批量更新策略 使用调度器(Scheduler)和优先级队列实现更高效的批处理(如 React 18 的自动批处理)。
特性 | 事务机制(React <16) | Fiber 架构(React 16+) |
---|---|---|
更新方式 | 同步批量更新 | 异步可中断、优先级调度 |
副作用管理 | 通过事务生命周期控制 | 通过 Effect Hook、提交阶段处理 |
复杂度 | 高(嵌套事务逻辑复杂) | 高(但更模块化和可扩展) |
适用场景 | 简单同步更新 | 复杂异步渲染(如动画、懒加载) |
事务机制是 React 早期实现批量更新的基石,但其同步设计无法满足现代前端应用的复杂需求。Fiber 架构通过解耦渲染过程,为 Concurrent Mode 和 Suspense 等特性奠定了基础,成为 React 高效渲染的核心。 :::
23. 如何理解 React concurrency 并发机制
React 的并发机制(Concurrency)是 React 18 引入的一项重要特性,旨在提升应用的响应性和性能。
1. 什么是 React 的并发机制?
React 的并发机制允许 React 在渲染过程中根据任务的优先级进行调度和中断,从而确保高优先级的更新能够及时渲染,而不会被低优先级的任务阻塞。
2. 并发机制的工作原理:
- 时间分片(Time Slicing): React 将渲染任务拆分为多个小片段,每个片段在主线程空闲时执行。这使得浏览器可以在渲染过程中处理用户输入和其他高优先级任务,避免长时间的渲染阻塞用户交互。
- 优先级调度(Priority Scheduling): React 为不同的更新分配不同的优先级。高优先级的更新(如用户输入)会被优先处理,而低优先级的更新(如数据预加载)可以在空闲时处理。
- 可中断渲染(Interruptible Rendering): 在并发模式下,React 可以中断当前的渲染任务,处理更高优先级的任务,然后再恢复之前的渲染。这确保了应用在长时间渲染过程中仍能保持响应性。
3. 并发机制的优势:
- 提升响应性: 通过优先处理高优先级任务,React 能够更快地响应用户输入,提升用户体验。
- 优化性能: 将渲染任务拆分为小片段,避免长时间的渲染阻塞,提升应用的整体性能。
- 更好的资源利用: 在主线程空闲时处理低优先级任务,充分利用系统资源。
4. 如何启用并发模式:
要在 React 应用中启用并发模式,需要使用 createRoot
API:
javascript 体验AI代码助手 代码解读复制代码import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)
在并发模式下,React 会自动根据任务的优先级进行调度和渲染。
24. React 组件渲染和更新的全过程
React 组件的渲染和更新过程涉及多个阶段,包括 初始化、渲染、协调、提交、清理 等。以下是 React 组件渲染和更新的全过程,结合源码逻辑和关键步骤进行详细分析。
1. 整体流程概述 React 的渲染和更新过程可以分为以下几个阶段:
- 初始化阶段:创建 Fiber 树和 Hooks 链表。
- 渲染阶段:生成新的虚拟 DOM(Fiber 树)。
- 协调阶段:对比新旧 Fiber 树,找出需要更新的部分。
- 提交阶段:将更新应用到真实 DOM。
- 清理阶段:重置全局变量,准备下一次更新。
2. 详细流程分析
(1)初始化阶段
触发条件:组件首次渲染或状态/属性更新。
关键函数:
render
、createRoot
、scheduleUpdateOnFiber
。逻辑
:
- 通过
ReactDOM.render
或createRoot
初始化应用。 - 创建根 Fiber 节点(
HostRoot
)。 - 调用
scheduleUpdateOnFiber
,将更新任务加入调度队列。
- 通过
(2)渲染阶段
触发条件:调度器开始执行任务。
关键函数:
performSyncWorkOnRoot
、beginWork
、renderWithHooks
。逻辑
:
- 调用
performSyncWorkOnRoot
,开始渲染任务。 - 调用
beginWork
,递归处理 Fiber 节点。 - 对于函数组件,调用
renderWithHooks
,执行组件函数并生成新的 Hooks 链表。 - 对于类组件,调用
instance.render
,生成新的虚拟 DOM。 - 对于 Host 组件(如
div
),生成对应的 DOM 节点。
- 调用
(3)协调阶段
触发条件:新的虚拟 DOM 生成后。
关键函数:
reconcileChildren
、diff
。逻辑
:
- 调用
reconcileChildren
,对比新旧 Fiber 节点。 - 根据
diff
算法,找出需要更新的节点。 - 为需要更新的节点打上
Placement
、Update
、Deletion
等标记。
- 调用
(4)提交阶段
触发条件:协调阶段完成后。
关键函数:
commitRoot
、commitWork
。逻辑
:
- 调用
commitRoot
,开始提交更新。 - 调用
commitWork
,递归处理 Fiber 节点。 - 根据节点的标记,执行 DOM 操作(如插入、更新、删除)。
- 调用生命周期钩子(如
componentDidMount
、componentDidUpdate
)。
- 调用
(5)清理阶段
触发条件:提交阶段完成后。
关键函数:
resetHooks
、resetContext
。逻辑
:
- 重置全局变量(如
currentlyRenderingFiber
、currentHook
)。 - 清理上下文和副作用。
- 准备下一次更新。
- 重置全局变量(如
25. 为何 React Hooks 不能放在条件或循环之内?
一个组件中的hook会以链表的形式串起来, FiberNode 的 memoizedState 中保存了 Hooks 链表中的第一个Hook。
在更新时,会复用之前的 Hook,如果通过了条件或循环语句,增加或者删除 hooks,在复用 hooks 过程中,会产生复用 hooks 状态和当前 hooks 不一致的问题。
26.什么是虚拟DOM 和真实DOM? 它们是如何搭建的? 有节点删改的时候会不会进行重绘和重排?
真实DOM
真实DOM:是浏览器对页面元素的抽象表示
假设有以下HTML:
html 体验AI代码助手 代码解读复制代码<div>
<h1>Hello, Real DOM</h1>
<p>This is a paragraph.</p>
</div>
对应的真实DOM 树结构
css 体验AI代码助手 代码解读复制代码HTML
└── BODY
└── DIV
├── H1
└── P
真实DOM的搭建: 通过HTML和CSS的解析,浏览器构建DOM树和CSSOM,然后合并成渲染树(render tree),最后进行布局和绘制。
虚拟DOM
虚拟DOM:是React 对DOM 的抽象表示,它是一个轻量化的Js对象,描述了UI结构与真实DOM对应。
虚拟DOM的搭建: 通过JSX
或 React.createElement
在内存中创建JavaScript对象树,作为真实DOM的抽象表示。
二者区别
特性 | 虚拟 DOM | 真实 DOM |
---|---|---|
存储位置 | 内存中,JS 对象 | 浏览器环境,HTML 结构 |
性能 | 快速构建、对比和更新 | 频繁操作会影响性能 |
修改方式 | 比较差异后,批量更新到真实 DOM | 直接修改 DOM 节点 |
直接交互 | 不可直接显示或操作 | 可直接交互或展示 |
真实DOM 会进行重绘和重排,虚拟DOM 不会(React通过其高效的Diff算法和批量更新策略来最小化重绘和重排的次数。)
虚拟DOM 存在的意义是为了弥补真实DOM 操作的性能问题。
简单介绍下Diff算法
Diff
算法 是 React 虚拟 DOM 的核心部分,用于高效地找出新旧虚拟 DOM 树之间的差异,并将最小更新应用到真实 DOM。
React 的 Diff 算法基于以下两点优化:
- 最小化对真实 DOM 的操作:频繁操作真实 DOM 会导致性能问题,因此 React 通过 Diff 算法尽量减少不必要的 DOM 操作。
- 局部更新:只对有变动的部分进行更新,而非重新渲染整个 UI。
Diff 算法的原理
React Diff 算法有以下重要特性:
分层对比
React 假设 DOM 的跨层级操作很少发生。如果发现节点层级改变(比如一个节点被移到了完全不同的层级),React 会直接删除原来的节点并重新创建,而不会尝试优化。
同层比较
在同一层中,React 按顺序比较子节点。如果节点的顺序发生变化,React 会将节点移除并重新插入。
Key 属性的优化
- React 使用 key 属性来标识元素的唯一性。
- 在子节点的顺序发生变化时,key 能帮助 React 快速定位变化的节点,避免不必要的重新创建。
三种变更类型
- 节点替换:当新旧节点类型不同时,直接替换整个节点。
- 属性更新:当节点类型相同时,只更新属性而不替换节点。
- 文本更新:如果一个文本节点发生变化,直接修改文本内容。
React 是如何通过虚拟DOM 把改变的部分传递给真实DOM的?
构建虚拟 DOM
当组件的 state(状态)或 props(属性)发生变化时,React 会重新调用组件的 render 方法,生成一个新的虚拟 DOM 树。这棵虚拟 DOM 树是组件在当前状态或属性下的理想结构的抽象表示。
比较虚拟 DOM
React 使用高效的 Diff 算法(由 React Fiber 实现)比较新旧两棵虚拟 DOM 树之间的差异(diff)。这一过程旨在识别哪些部分发生了变化、哪些部分保持不变,从而避免不必要的更新。
计算最小变更集
基于虚拟 DOM 的对比结果,React 计算出从旧虚拟 DOM 更新为新虚拟 DOM 所需的最小变更集。这些变更包括:
- 新增节点:新增的虚拟 DOM 节点会被映射到真实 DOM 中并插入相应位置。
- 删除节点:从虚拟 DOM 中删除的节点会被从真实 DOM 中移除。
- 更新节点:已存在的节点如果属性或内容发生变化,React 会直接修改对应的真实 DOM 节点。
更新真实 DOM
React 将计算出的最小变更集应用到真实 DOM。由于只更新变化部分,避免了整棵 DOM 树的重绘,大大提高了性能。这个过程通常使用
document.createElement
、appendChild
或removeChild
等 DOM 操作来实现。优化和复用
为了进一步提升性能,React 会尝试复用和重排现有的 DOM 节点。
例如:如果节点只是更改了属性(如
className
或style
),React 会直接更新属性,而不会重新创建节点。 如果列表元素的顺序变化,React 会通过 key 属性来标识和移动节点,而不是删除后重新插入。
React 渲染机制是什么
React 的渲染机制以 虚拟 DOM 和 Reconciliation (协调算法)为核心,通过将应用状态与UI映射的过程高效化,实现快速、直观的UI构建和更新
为什么hooks能重用和共享组件状态?
React 提供的 Hooks(特别是自定义 Hooks)让开发者可以抽象出可复用的状态逻辑。通过自定义 Hooks,开发者能够将组件中的特定逻辑(如状态管理、数据获取、事件处理等)封装为独立的函数,并在多个组件中共享这些逻辑。
自定义 Hooks 的设计与特点
逻辑封装与复用
自定义 Hooks 是以
use
开头的普通函数,可以接收参数,返回状态或逻辑。它们允许开发者将复杂的逻辑抽象出来,从而在多个组件中共享。例如,一个管理表单状态的自定义 Hook,可以在不同的表单组件中重用,而无需重复实现状态管理逻辑。
js 体验AI代码助手 代码解读复制代码// 一个自定义 Hook 示例
const useForm = (initialValues) => {
const [values, setValues] = useState(initialValues);
const handleChange = (e) => setValues({ ...values, [e.target.name]: e.target.value });
return [values, handleChange];
};
// 在多个组件中重用
const FormComponent1 = () => {
const [formValues, handleChange] = useForm({ name: '', email: '' });
return <input name="name" value={formValues.name} onChange={handleChange} />;
};
const FormComponent2 = () => {
const [formValues, handleChange] = useForm({ username: '', password: '' });
return <input name="username" value={formValues.username} onChange={handleChange} />;
};
- 逻辑解耦: Hooks 将逻辑从组件的渲染逻辑中分离出来,使得代码更模块化和可读。同时,状态和副作用的管理不再依赖类组件的生命周期方法,组件逻辑更加集中且易于维护。
- 与函数式组件的融合: 自定义 Hooks 可以充分利用函数式组件的特性,比如函数的参数传递、返回值复用等,使逻辑复用和组合更加自然。Hooks 的函数式特性还避免了类组件中的复杂继承和 this 绑定问题。
状态共享
通过 useContext
Hook 配合 React 的 Context API,可以实现跨组件的状态共享。例如,定义一个全局状态管理的 Context,并在任意组件中通过自定义 Hook 来消费:
js 体验AI代码助手 代码解读复制代码const GlobalContext = React.createContext();
const useGlobalState = () => {
const context = useContext(GlobalContext);
if (!context) throw new Error('useGlobalState must be used within a GlobalProvider');
return context;
};
const GlobalProvider = ({ children }) => {
const [state, setState] = useState({ user: null });
const value = { state, setState };
return <GlobalContext.Provider value={value}>{children}</GlobalContext.Provider>;
};
// 在组件中共享状态
const ComponentA = () => {
const { state, setState } = useGlobalState();
return <button onClick={() => setState({ user: 'John' })}>Login</button>;
};
const ComponentB = () => {
const { state } = useGlobalState();
return <div>User: {state.user}</div>;
};
优势总结
- 模块化与可复用性: Hooks 让开发者可以将组件逻辑提取到独立的函数中,避免重复代码,提高开发效率。
- 可维护性与可读性: 自定义 Hooks 将组件的逻辑组织得更加集中,降低了耦合度,使代码易于理解和维护。
- 支持更灵活的共享方式:
useContext
实现全局状态共享;- Redux 或其他状态管理工具结合 Hooks(如
useSelector
和useDispatch
)实现复杂场景的状态共享。
- 没有类组件的限制: Hooks 是函数式编程的体现,无需类组件的继承、生命周期方法和 this 的复杂绑定,更加灵活。