36.Fiber的更新机制
React Fiber 更新机制详解
React Fiber 是 React 16 引入的核心架构重构,旨在解决可中断渲染和优先级调度问题,提升复杂应用的流畅性。其核心思想是将渲染过程拆分为可控制的工作单元,实现更细粒度的任务管理。以下是其核心机制:
一、Fiber 架构的设计目标
- 可中断与恢复:允许渲染过程被高优先级任务(如用户输入)打断,后续恢复。
- 增量渲染:将渲染任务拆分为多个小任务(时间分片),避免阻塞主线程。
- 优先级调度:根据任务类型(如动画、数据加载)分配不同优先级。
- 并发模式支持:为 Suspense、Transition 等特性提供底层支持。
二、Fiber 节点:工作单元的基础
每个 Fiber 节点对应一个组件或 DOM 节点,构成链表树结构,包含以下关键信息:
-
组件类型:函数/类组件、HTML 标签等。
-
状态与 Props:
state
、props
、context
。 -
副作用标记:增/删/更新 DOM、调用生命周期等(通过
flags
字段标识)。 -
链表指针:
child
:指向第一个子节点。sibling
:指向下一个兄弟节点。return
:指向父节点。
-
优先级:
lane
模型标记任务优先级(如 SyncLane、InputContinuousLane)。
三、更新流程:从触发到提交
1. 触发更新
- 来源:
setState
、useState
、父组件重渲染、Context 变更等。 - 创建更新对象:包含新状态、优先级等信息,添加到 Fiber 的更新队列。
2. 调度阶段(Scheduler)
- 任务分片:将整个渲染流程拆分为多个 Fiber 节点的处理单元。
- 优先级排序:使用
lane
模型分配优先级,高优先级任务可抢占低优先级。 - 时间切片:通过
requestIdleCallback
或MessageChannel
在浏览器空闲时段执行任务。
3. 协调阶段(Reconciler)
- 构建 WorkInProgress 树:在内存中生成新 Fiber 树(双缓存机制)。
- Diff 算法:对比新旧 Fiber 节点,标记变更(如
Placement
、Update
、Deletion
)。 - 生命周期触发:执行函数组件的渲染、类组件的
render
方法。
4. 提交阶段(Commit)
-
同步执行:不可中断,一次性将变更应用到 DOM。
-
副作用处理:
- DOM 操作:增删改节点。
- 生命周期:类组件的
componentDidMount/Update
。 - Hooks:
useLayoutEffect
回调。
-
切换当前树:将
WorkInProgress
树标记为current
树。
四、优先级调度与中断机制
- Lane 模型:用二进制位表示优先级(如
0b0001
和0b0010
可合并为0b0011
)。 - 高优先级抢占:用户交互触发的更新(如按钮点击)可中断正在进行的低优先级渲染(如大数据列表渲染)。
- 饥饿问题处理:长时间未执行的低优先级任务会被提升优先级。
示例场景:
用户输入搜索关键词时,输入框的即时响应(高优先级)会中断后台数据渲染(低优先级)。
五、双缓存技术
- Current 树:当前屏幕上显示的 Fiber 树。
- WorkInProgress 树:正在构建的新树,完成后替换 Current 树。
- 优势:减少渲染过程中的页面闪烁,确保原子性更新。
六、并发模式下的更新
-
过渡更新(Transition) :通过
startTransition
标记非紧急更新(如页面切换),可被用户交互打断。const [isPending, startTransition] = useTransition(); startTransition(() => { setPage(newPage); // 低优先级更新 });
-
Suspense:配合懒加载组件,在数据加载时显示 fallback UI。
七、性能优化启示
- 减少渲染粒度:使用
React.memo
、useMemo
避免无效渲染。 - 合理分配优先级:紧急操作使用高优先级,长任务用
startTransition
包裹。 - 优化 Fiber 树深度:扁平化组件结构,减少协调时间。
总结
React Fiber 通过可中断的异步渲染和优先级调度,彻底改变了 React 的渲染机制。其核心价值在于:
- 更流畅的交互:高优先级任务快速响应,避免界面卡顿。
- 更高效的渲染:增量更新减少主线程阻塞。
- 面向未来的扩展:为并发特性(如 Suspense、Server Components)奠定基础。
37.React18有哪些更新
React 18 主要更新详解
React 18 引入了多项重要改进和新特性,旨在提升性能、开发体验及扩展能力。以下是其核心更新内容:
1. 并发渲染(Concurrent Rendering)
-
核心机制:通过可中断的渲染过程,实现任务优先级调度与时间分片。
- 并发模式(Concurrent Mode) :现称为“并发特性”,无需全局开启,按需使用。
- API支持:
startTransition
、useDeferredValue
等。
-
优势:
- 高优先级任务(如用户输入)可中断低优先级渲染,提升交互流畅度。
- 支持复杂场景下的无缝过渡(如页面切换、数据加载)。
示例:
import { startTransition } from 'react';
// 标记非紧急更新
startTransition(() => {
setSearchQuery(input); // 延迟渲染搜索结果,保持输入响应
});
2. 自动批处理(Automatic Batching)
-
改进点:在更多场景下合并状态更新,减少渲染次数。
- React 17及之前:仅在事件处理函数中批处理。
- React 18:扩展至Promise、setTimeout等异步操作。
-
效果:降低不必要的重渲染,优化性能。
示例:
// React 18:两次setState合并为一次渲染
setTimeout(() => {
setCount(1);
setFlag(true);
}, 1000);
3. 新的根API(createRoot)
-
替换旧API:使用
createRoot
替代ReactDOM.render
,启用并发特性。 -
用法:
import { createRoot } from 'react-dom/client'; const root = createRoot(document.getElementById('root')); root.render(<App />);
4. Suspense 增强
-
服务端渲染(SSR)支持:
- 流式HTML传输:逐步发送HTML,加速首屏加载。
- 选择性Hydration:优先为交互部分注水,提升可交互时间(TTI)。
-
客户端扩展:支持在更多场景包裹异步组件或数据加载。
示例:
<Suspense fallback={<Loading />}>
<AsyncComponent />
</Suspense>
5. 新Hooks API
-
useId:生成唯一ID,解决SSR与客户端ID不一致问题。
const id = useId(); // 生成如 ":r1:"
-
useSyncExternalStore:简化外部状态库(如Redux)集成。
const state = useSyncExternalStore(store.subscribe, store.getState);
-
useInsertionEffect:适用于CSS-in-JS库动态插入样式。
useInsertionEffect(() => { const style = document.createElement('style'); style.innerHTML = `.css { color: red }`; document.head.appendChild(style); });
6. 过渡API(Transitions)
-
区分紧急/非紧急更新:通过
startTransition
延迟非关键渲染。 -
UI反馈:
useTransition
提供isPending
状态,显示加载指示。const [isPending, startTransition] = useTransition(); startTransition(() => { setTab(newTab); // 非紧急导航 }); return isPending ? <Spinner /> : <Content />;
7. 严格模式增强
-
开发环境行为:
- 双调用Effects:模拟组件卸载/挂载,暴露副作用问题。
- 组件重复挂载:检查是否正确处理清理逻辑(如定时器、订阅)。
8. 服务端组件(实验性)
-
核心能力:
- 服务端渲染组件:在服务端执行,减少客户端代码体积。
- 无缝数据获取:直接访问后端API,传递序列化数据至客户端。
-
使用场景:静态内容、SEO优化、性能敏感页面。
示例:
// ServerComponent.server.js
export default function ServerComponent() {
const data = fetchData(); // 服务端执行
return <div>{data}</div>;
}
9. 其他改进
- 性能优化:减少内存占用,提升大型应用渲染效率。
- TypeScript支持:更严格的类型推断,减少显式类型声明。
- 开发者工具:增强并发模式调试支持,可视化渲染优先级。
升级指南
-
兼容性:React 18 保持向后兼容,逐步采用新特性。
-
迁移步骤:
- 使用
createRoot
替换ReactDOM.render
。 - 按需引入并发API(如
startTransition
)。 - 测试严格模式下的副作用处理。
- 使用
总结
React 18 通过并发渲染、自动批处理、Suspense增强等特性,显著提升了应用性能与用户体验。开发者可通过渐进式升级,利用新API优化交互流畅度与渲染效率,同时为未来特性(如服务端组件)奠定基础。
38.Rect19有哪些新特性
具体详见官网:
中文:React 19 新特性
英文:React 19 新特性
核心新特性
1. Actions
解决问题:简化数据变更和状态更新流程
- 以前需要手动处理待定状态、错误、乐观更新和顺序请求
- 需要维护多个状态变量(isPending, error 等)
新特性:
function UpdateName() {
const [state, submitAction, isPending] = useActionState(
async (prevState, formData) => {
const error = await updateName(formData.get("name"));
if (error) return error;
redirect("/path");
return null;
},
null
);
return (
<form action={submitAction}>
<input name="name" />
<button disabled={isPending}>Update</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
主要改进:
- 自动处理待定状态
- 内置错误处理
- 支持乐观更新
- 简化表单处理
2. useFormStatus
解决问题:简化表单组件状态访问
- 避免通过 props 传递表单状态
- 提供统一的表单状态访问方式
function SubmitButton() {
const { pending, data, method } = useFormStatus();
return (
<button disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
3. useOptimistic
解决问题:提供更好的用户体验
- 立即显示操作结果
- 处理异步操作的状态更新
function LikeButton({ id }) {
const [likes, setLikes] = useState(0);
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(state, increment) => state + increment
);
async function handleLike() {
addOptimisticLike(1); // 立即更新 UI
await updateLikes(id); // 后台进行实际更新
}
}
4. use() Hook
解决问题:统一资源使用方式
- 简化 Promise 和 Context 的使用
- 支持条件性使用
- 提供更好的类型推断
function Comments({ commentsPromise }) {
const comments = use(commentsPromise); // 自动处理 Suspense
return comments.map(comment => <p>{comment}</p>);
}
架构改进
1. Document 流式渲染
解决问题:改善首次加载体验
- 支持 HTML 流式传输
- 优化资源加载顺序
function AsyncPage() {
return (
<Document>
<Suspense fallback={<Loading />}>
<AsyncContent />
</Suspense>
</Document>
);
}
2. 资源处理优化
样式表支持
解决问题:简化样式管理
- 自动处理样式表加载顺序
- 支持组件级样式声明
function Component() {
return (
<>
<link rel="stylesheet" href="styles.css" precedence="default" />
<div className="styled-content">...</div>
</>
);
}
异步脚本支持
解决问题:优化脚本加载
- 自动处理脚本去重
- 优化加载优先级
function MyComponent() {
return (
<div>
<script async={true} src="widget.js" />
<div>Widget Content</div>
</div>
);
}
开发体验改进
1. 错误处理增强
解决问题:提供更清晰的错误信息
- 消除重复错误日志
- 提供更详细的错误上下文
createRoot(container, {
onCaughtError: (error) => {
// 错误边界捕获的错误
},
onUncaughtError: (error) => {
// 未被捕获的错误
},
onRecoverableError: (error) => {
// 可恢复的错误
}
});
2. 自定义元素支持
解决问题:改善与 Web Components 的集成
- 完整支持自定义元素
- 正确处理属性和属性传递
最佳实践建议
-
渐进式采用
- 优先使用新的表单处理方式
- 在关键交互中使用乐观更新
- 利用新的资源加载优化
-
性能优化
- 使用流式渲染改善加载体验
- 合理使用资源预加载
- 优化并发更新
-
错误处理
- 使用新的错误边界
- 实现适当的降级策略
- 监控错误模式
服务器组件
1. 服务器组件基础
解决问题:优化应用性能和开发体验
- 减少客户端 bundle 大小
- 直接访问后端资源
- 改善数据获取模式
// 服务器组件
async function Notes() {
// 直接访问数据库,无需 API 层
const notes = await db.notes.getAll();
return (
<div>
{notes.map(note => (
<Expandable key={note.id}>
<p>{note.content}</p>
</Expandable>
))}
</div>
);
}
2. 服务器组件与客户端组件集成
解决问题:平滑处理服务器和客户端组件交互
- 支持渐进式增强
- 保持交互性
- 优化数据流
// 服务器组件
import Expandable from './Expandable'; // 客户端组件
async function NotesContainer() {
const notes = await db.notes.getAll();
return (
<div>
{/* 服务器组件可以渲染客户端组件 */}
<Expandable>
<NotesList notes={notes} />
</Expandable>
</div>
);
}
// 客户端组件
'use client'
function Expandable({ children }) {
const [expanded, setExpanded] = useState(false);
return (
<div>
<button onClick={() => setExpanded(!expanded)}>
{expanded ? 'Collapse' : 'Expand'}
</button>
{expanded && children}
</div>
);
}
3. 异步组件
解决问题:简化异步数据处理
- 支持 async/await 语法
- 自动处理 Suspense 集成
- 优化加载状态
// 服务器组件中的异步数据获取
async function Page({ id }) {
const note = await db.notes.get(id);
// 开始获取评论但不等待
const commentsPromise = db.comments.get(id);
return (
<div>
<h1>{note.title}</h1>
<Suspense fallback={<Loading />}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}
Refs 作为 Props
1. 将 ref 作为 prop
从 React 19 开始,你现在可以在函数组件中将 ref 作为 prop 进行访问:
function MyInput({placeholder, ref}) {
return <input placeholder={placeholder} ref={ref} />
}
//...
<MyInput ref={ref} />
新的函数组件将不再需要 forwardRef,我们将发布一个 codemod 来自动更新你的组件以使用新的 ref prop。在未来的版本中,我们将弃用并移除 forwardRef。
2. Ref 稳定性改进
解决问题:优化 ref 更新和同步
- 更可预测的 ref 更新时机
- 更好的并发模式支持
- 改进的性能特性
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
// ref 回调模式的改进
const setRef = useCallback((element: HTMLInputElement | null) => {
if (element) {
element.focus();
}
}, []);
return <input ref={setRef} />;
}
服务器组件最佳实践
-
数据获取策略
- 在服务器组件中直接访问数据源
- 使用流式传输处理大量数据
- 实现适当的缓存策略
-
组件分割
- 将有状态逻辑的组件标记为客户端组件
- 保持服务器组件纯粹
- 优化组件边界
-
性能优化
- 使用服务器组件减少客户端 bundle
- 实现渐进式加载
- 优化数据预取
// 示例:优化的服务器组件结构
async function BlogPost({ id }) {
const post = await db.posts.get(id);
const authorPromise = db.authors.get(post.authorId);
const commentsPromise = db.comments.get(id);
return (
<article>
<h1>{post.title}</h1>
<Suspense fallback={<AuthorSkeleton />}>
<Author authorPromise={authorPromise} />
</Suspense>
<Content>{post.content}</Content>
<Suspense fallback={<CommentsSkeleton />}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
</article>
);
}
注意事项
-
服务器组件限制
- 不能使用浏览器 API
- 不能使用状态和生命周期
- 需要正确处理数据获取错误
-
Ref 使用考虑
- 注意 ref 的生命周期
- 合理处理 ref 清理
- 避免过度依赖 ref
-
性能注意事项
- 平衡服务器和客户端渲染
- 合理使用 Suspense 边界
- 优化数据预加载策略
详细请看链接
39.React为什么不直接使用this.state改变数据
在 React 中,不能直接通过 this.state
来改变数据,而必须使用 this.setState()
来更新状态。这背后有几个关键的原因,主要是为了保证 组件的可预测性、性能优化 和 避免直接修改状态 所带来的潜在问题。接下来,我将详细解释这些原因。
1. 不可变数据和状态管理原则
React 提倡 不可变数据(immutable data)原则,即状态对象一旦创建,它的值就不应该直接修改。直接修改 this.state
会导致组件行为变得难以预测,难以追踪和调试。通过 this.setState()
,React 可以保证每次状态更新时,状态对象都是全新的对象,而不是直接修改原有对象。
为什么要避免直接修改 this.state
?
- 直接修改状态会破坏数据的不可变性,使得 React 无法检测到变化。
- 状态不再是新的引用,这使得 React 无法有效地进行比较,进而影响渲染效率。
举个例子,如果直接修改 this.state
:
this.state.someValue = newValue; // 不推荐
这样 React 就不会知道状态发生了变化,因此不会触发重新渲染,也就无法同步 UI 和状态。
而通过 this.setState()
:
this.setState({ someValue: newValue }); // 推荐
this.setState()
会创建一个新的状态对象,确保 React 能检测到状态变化,并触发 UI 更新。
2. 异步更新与批量更新
this.setState()
的更新是异步的,而直接修改 this.state
是同步的。React 内部有一种机制,用来批量更新状态,以减少不必要的重新渲染。这种机制不仅提高了性能,还避免了多次渲染的重复计算。
例如,假设你直接修改了 this.state
,并且立即访问了 this.state
来获取新值。由于 React 的 setState()
是异步的,直接修改 this.state
可能会导致你获取到的状态值不是更新后的值。
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 可能不会立即反映出最新的状态
React 会将多个 setState()
调用合并到一个批量更新中,以减少不必要的渲染和性能开销。通过使用 this.setState()
,React 可以处理这些合并和异步更新的操作。
3. 性能优化
this.setState()
触发的更新过程与直接修改 this.state
的过程有所不同。当调用 setState()
时,React 会合并当前的状态和新的状态,只有发生了变化的部分会被更新。这对于性能优化至关重要。
如果你直接修改 this.state
,React 就无法知道哪些部分发生了变化,也就无法进行智能的 diff 和批量更新。例如:
this.state.count = 10; // 直接修改
this.setState({ count: 10 }); // 通过 setState 更新
在 setState()
中,React 会比较前后的状态,判断是否需要重新渲染组件,而直接修改 this.state
则无法触发这种比较。
4. 组件的生命周期和渲染
this.setState()
触发状态更新时,React 会在合适的生命周期方法中触发组件的重新渲染。例如,在状态更新时,shouldComponentUpdate
、componentDidUpdate
等生命周期方法会被调用,以便开发者可以在状态变化时执行一些操作。如果直接修改 this.state
,React 不会知道组件状态发生变化,进而不会触发这些生命周期方法。
这会导致一些问题,比如无法对比新旧状态、无法做条件渲染等操作。
5. React 状态更新的队列机制
React 通过维护一个更新队列来优化状态的更新和渲染。当你调用 this.setState()
时,React 会把状态更新请求放入一个队列,批量处理这些更新。在队列中的多个状态更新可以合并,这避免了不必要的重新渲染,提高了性能。
直接修改 this.state
不会加入更新队列,React 不会触发它的重新渲染机制,因此无法享受 React 的批量更新和性能优化。
6. 追踪组件的变化
React 使用 this.setState()
来管理组件状态,并且通过对比旧的虚拟 DOM 和新的虚拟 DOM,来决定哪些部分需要重新渲染。这个过程需要 React 在底层追踪和比较状态,而直接修改 this.state
会使得这个追踪变得困难,影响性能优化。
通过 this.setState()
,React 能够确保组件状态的变化得到跟踪,并且在状态变化时触发重新渲染,确保 UI 始终与状态一致。
7. React 16+ 的 Fiber 架构
在 React 16 及更高版本中,React 引入了 Fiber 架构,这使得 React 的渲染变得更加灵活和高效。通过 this.setState()
,React 能够控制每一次渲染的优先级,并且通过增量更新来保持 UI 响应性。如果你直接修改 this.state
,这种增量更新机制就无法发挥作用。
总结:
- 数据不可变性:
this.setState()
遵循不可变数据的原则,避免直接修改原有状态。 - 异步和批量更新:
this.setState()
支持异步更新,React 可以批量处理多个状态更新,提升性能。 - 生命周期管理:通过
setState
,React 能够触发生命周期钩子,确保组件正确更新。 - 性能优化:
this.setState()
可以通过智能的差异化更新减少不必要的 DOM 操作,而直接修改this.state
会跳过这一过程。
因此,直接修改 this.state
会破坏 React 的更新机制,导致状态和 UI 不一致,影响性能和可维护性。React 的设计理念和架构要求我们通过 this.setState()
来更新状态,从而确保更高效和可预测的渲染过程。
40.原来的react虚拟dom树遍历是深度优先遍历还是层序遍历?存储是链表存储还是栈存储,fiber机制下的是如何存储
React 虚拟DOM遍历与存储机制详解
一、传统虚拟DOM(React 15及之前)
-
遍历方式:深度优先遍历(DFS)
-
递归处理:从根组件开始,递归处理每个组件及其子组件,直到叶子节点,再回溯处理兄弟节点。
-
顺序示例:
A → A.child B → B.child C → C.child D → 回溯到 B → B.sibling E → E.child F
-
-
存储结构:隐式调用栈
-
依赖调用栈:递归调用栈隐式管理遍历过程,无显式数据结构存储节点关系。
-
缺点:
- 不可中断:递归一旦开始必须执行完毕,导致主线程阻塞。
- 性能瓶颈:深层嵌套组件树易引发栈溢出或卡顿。
-
二、Fiber架构(React 16+)
-
遍历方式:可中断的迭代式深度优先遍历
-
顺序不变:仍按深度优先顺序处理节点(与之前一致)。
-
实现变化:从递归改为循环+链表指针手动遍历,支持暂停与恢复。
-
流程示例:
let fiber = rootFiber; while (fiber) { process(fiber); // 处理当前节点 if (fiber.child) { fiber = fiber.child; // 优先处理子节点 continue; } while (fiber) { completeWork(fiber); // 完成当前节点 if (fiber.sibling) { fiber = fiber.sibling; // 转向兄弟节点 break; } fiber = fiber.return; // 回溯父节点 } }
-
-
存储结构:显式链表树
-
Fiber节点结构:
interface Fiber { tag: ComponentType; // 组件类型 child: Fiber | null; // 第一个子节点 sibling: Fiber | null; // 下一个兄弟节点 return: Fiber | null; // 父节点 alternate: Fiber | null; // 指向另一棵树(双缓存) flags: number; // 副作用标记(增/删/更新) lanes: Lanes; // 优先级 // ...其他字段(stateNode、props等) }
-
双缓存机制:
- Current树:当前渲染的树(对应屏幕显示内容)。
- WorkInProgress树:正在构建的新树,完成后替换Current树。
- 优势:避免渲染中间状态导致的UI闪烁。
-
三、Fiber架构的核心改进
维度 | 传统虚拟DOM | Fiber架构 |
---|---|---|
遍历控制 | 递归(不可中断) | 迭代(可中断 + 恢复) |
数据结构 | 隐式调用栈 | 显式链表(child/sibling/return) |
任务调度 | 同步执行 | 优先级调度 + 时间分片 |
性能优化 | 易阻塞主线程 | 增量渲染,避免卡顿 |
扩展能力 | 有限 | 支持并发模式(Suspense/Transition) |
四、Fiber遍历流程示例
假设组件树结构为:
A
├─ B
│ ├─ C
│ └─ D
└─ E
└─ F
遍历顺序:
- 进入A → 处理A
- 进入A.child B → 处理B
- 进入B.child C → 处理C
- C无子节点 → 完成C,回溯到B
- 进入B.sibling D → 处理D
- D无子节点 → 完成D,回溯到B → 完成B,回溯到A
- 进入A.sibling E → 处理E
- 进入E.child F → 处理F
- F无子节点 → 完成F,回溯到E → 完成E,回溯到A → 完成A
五、Fiber架构的优势
- 可中断渲染:高优先级任务(如用户输入)可打断低优先级渲染。
- 增量更新:将渲染任务拆分为多个帧执行,避免主线程阻塞。
- 精准副作用提交:通过
flags
标记变更,一次性提交DOM操作。 - 并发模式支持:实现服务端渲染流式输出、Suspense等高级特性。
总结
- 传统虚拟DOM:深度优先遍历 + 递归调用栈,简单但不可中断。
- Fiber架构:深度优先遍历 + 显式链表结构,通过迭代实现可中断渲染,结合优先级调度与双缓存机制,为React带来革命性性能提升与扩展能力。
- 核心价值:将同步渲染转化为异步可调度任务,使复杂应用保持流畅交互。
41.React如何创建工程环境(js,ts),eject的作用是什么?
一、创建React工程环境
1. JavaScript项目
使用 Create React App (CRA) 快速搭建React项目:
npx create-react-app my-app
- 这会生成一个默认的JavaScript项目,包含基础配置(Webpack、Babel等),无需手动配置。
2. TypeScript项目
在创建时通过--template typescript
指定TypeScript模板:
npx create-react-app my-app --template typescript
-
或对已有JS项目添加TypeScript支持:
npm install --save typescript @types/react @types/react-dom
将文件后缀改为
.tsx
或.ts
,CRA会自动识别并配置TypeScript。
二、eject
的作用
1. 功能
- 暴露隐藏配置:运行
npm run eject
会将CRA封装的配置(如Webpack、Babel、ESLint)完全解压到项目目录,允许直接修改。 - 不可逆操作:一旦执行,无法回退到CRA的默认封装状态。
2. 使用场景
- 需要深度定制构建工具(如修改Webpack配置、添加插件)。
- CRA默认配置无法满足项目需求(如自定义代码分割规则)。
3. 注意事项
- 维护成本:需自行管理所有配置,增加复杂性。
- 替代方案:优先考虑非侵入式工具(如
react-app-rewired
、@craco/craco
)覆盖配置,避免eject
。
三、总结
- 创建项目:CRA是官方推荐工具,支持JS/TS开箱即用。
eject
:谨慎使用,仅在必要时暴露配置,否则优先选择灵活配置方案。
42.React常见hooks有哪些
React常见Hooks及用途
React Hooks 是函数组件中管理状态、副作用和其他功能的工具。以下是常用Hooks及其核心作用:
1. useState
-
用途:管理组件内部状态。
-
示例:
const [count, setCount] = useState(0);
2. useEffect
-
用途:处理副作用(如数据请求、订阅、DOM操作)。
-
依赖控制:通过第二个参数(依赖数组)控制执行时机。
useEffect(() => { fetchData(); return () => { /* 清理逻辑 */ }; }, [dependency]);
3. useContext
-
用途:跨组件共享数据(如主题、用户信息),避免逐层传递props。
-
示例:
const theme = useContext(ThemeContext);
4. useReducer
-
用途:管理复杂状态逻辑,类似Redux的Reducer模式。
-
适用场景:状态更新涉及多步骤或依赖之前状态。
const [state, dispatch] = useReducer(reducer, initialState);
5. useRef
-
用途:
- 访问DOM节点(如聚焦输入框)。
- 保存可变值(如定时器ID),不触发重渲染。
-
示例:
const inputRef = useRef(null); <input ref={inputRef} />
6. useMemo
-
用途:缓存计算结果,避免重复计算。
-
优化场景:依赖项未变化时复用缓存值。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
7. useCallback
-
用途:缓存函数,避免子组件因函数引用变化而重渲染。
-
示例:
const handleClick = useCallback(() => { doSomething(a) }, [a]);
8. useLayoutEffect
- 用途:与
useEffect
类似,但同步执行(在DOM更新后、浏览器绘制前)。 - 适用场景:需要直接操作DOM或避免视觉抖动。
其他Hooks
useImperativeHandle
:自定义暴露给父组件的ref实例。useDebugValue
:在React开发者工具中显示自定义Hook的标签。
总结
- 核心Hooks:
useState
、useEffect
、useContext
、useReducer
、useRef
。 - 性能优化:
useMemo
、useCallback
。 - 高级场景:
useLayoutEffect
、useImperativeHandle
。 - 自定义Hook:封装可复用的逻辑(如
useFetch
)。
43.React中函数式组件如何模拟生命周期
类组件生命周期 | 函数式组件实现方式 |
---|---|
componentDidMount | useEffect + 空依赖数组 |
componentDidUpdate | useEffect + 监听依赖项 |
componentWillUnmount | useEffect 的清理函数 |
shouldComponentUpdate | React.memo 或 useMemo |
getDerivedStateFromProps | useState + useEffect |
44.React如何通过hooks通过数据接口,访问数据?
React通过Hooks访问数据接口的实现步骤
在React中,使用Hooks访问数据接口通常涉及以下步骤,结合状态管理、副作用处理及异步操作:
1. 使用useState
管理数据状态
定义状态变量存储数据、加载状态及错误信息:
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
2. 使用useEffect
触发数据请求
在useEffect
中发起异步请求,处理数据获取逻辑:
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) throw new Error('请求失败');
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []); // 空依赖数组表示仅在组件挂载时执行
3. 处理组件卸载时的请求中断(可选)
使用AbortController
取消未完成的请求,避免内存泄漏:
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data', {
signal: abortController.signal,
});
// ...处理数据
} catch (err) {
if (err.name !== 'AbortError') setError(err.message);
}
};
fetchData();
return () => abortController.abort(); // 清理函数中中断请求
}, []);
4. 展示数据与状态
根据状态渲染UI:
return (
<div>
{isLoading ? (
<div>加载中...</div>
) : error ? (
<div>错误:{error}</div>
) : (
<div>{JSON.stringify(data)}</div>
)}
</div>
);
5. 自定义Hook封装(进阶)
将数据获取逻辑抽象为可复用的自定义Hook:
const useFetch = (url) => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [url]);
return { data, isLoading, error };
};
// 在组件中使用
const MyComponent = () => {
const { data, isLoading, error } = useFetch('https://api.example.com/data');
// ...渲染逻辑
};
6. 使用TypeScript定义数据类型(可选)
为接口返回数据添加类型约束:
interface ApiData {
id: number;
name: string;
}
const useFetch = (url: string) => {
const [data, setData] = useState<ApiData | null>(null);
// ...其他逻辑
};
关键点总结
- 状态管理:
useState
管理数据、加载状态和错误。 - 副作用控制:
useEffect
处理异步请求,依赖项控制触发时机。 - 请求中断:
AbortController
避免组件卸载后更新状态导致的内存泄漏。 - 自定义Hook:封装逻辑提升复用性。
- 类型安全:TypeScript确保数据结构正确性。
45.React hook的使用,有哪些注意方式?为什么会有hooks?
React Hooks 使用注意事项
1. 遵守Hooks的调用规则
-
只在顶层调用:
不可在条件、循环或嵌套函数中使用Hooks,确保每次渲染时Hooks的调用顺序一致。// 错误示例:条件中使用Hook if (condition) { const [state, setState] = useState(); // 会导致后续Hooks顺序错乱 }
-
仅用于React函数组件或自定义Hooks:
不可在普通JavaScript函数中调用Hooks。
2. 正确处理依赖数组
-
useEffect
、useMemo
、useCallback
的依赖项:
明确列出所有外部依赖,避免闭包陷阱或过时数据。useEffect(() => { fetchData(id); // 若依赖id,需将其加入依赖数组 }, [id]);
-
空依赖数组的用途:
仅在组件挂载时执行一次(模拟componentDidMount
)。
3. 性能优化
-
避免不必要的渲染:
使用React.memo
、useMemo
、useCallback
减少子组件重复渲染。const memoizedValue = useMemo(() => computeValue(a, b), [a, b]); const handleClick = useCallback(() => action(a), [a]);
-
避免滥用
useState
:
合并相关状态,减少渲染次数。// 合并为对象 const [user, setUser] = useState({ name: 'Alice', age: 20 });
4. 清理副作用
-
useEffect
的清理函数:
取消订阅、定时器或网络请求,防止内存泄漏。useEffect(() => { const timer = setInterval(() => {}, 1000); return () => clearInterval(timer); }, []);
5. 自定义Hooks规范
- 命名以
use
开头:
便于React识别并应用Hooks规则,例如useFetch
、useLocalStorage
。
为什么需要Hooks?
1. 解决类组件的痛点
- 逻辑复用困难:
类组件中复用状态逻辑需通过高阶组件(HOC)或Render Props,导致“嵌套地狱”。 - 生命周期方法分散逻辑:
相关代码分散在componentDidMount
、componentDidUpdate
等生命周期中,难以维护。 this
指向问题:
类组件中需要绑定this
,增加代码复杂度。
2. 函数组件的增强
- 赋予函数组件状态能力:
通过useState
、useEffect
等Hooks,函数组件可管理状态和副作用,无需转换为类组件。 - 逻辑聚合:
将相关逻辑集中到同一Hook中,提升代码可读性(如将数据请求与状态管理封装为useFetch
)。
3. 更简洁的代码结构
- 减少模板代码:
避免类组件的构造函数、生命周期方法等冗余代码。 - 函数式编程优势:
更易编写纯函数,方便测试和调试。
4. 社区与未来趋势
- 函数式编程普及:
Hooks推动React向函数式范式发展,与现代JavaScript生态更契合。 - 渐进式迁移:
支持在现有类组件中逐步引入Hooks,降低重构成本。
总结
注意事项 | 设计动机 |
---|---|
调用顺序一致性 | 解决类组件的逻辑复用与生命周期碎片化 |
依赖数组精确管理 | 简化状态管理与副作用控制 |
性能优化与副作用清理 | 提升代码可维护性与可读性 |
自定义Hooks规范 | 推动函数式组件成为主流开发模式 |
46.React是如何获取组件对应的DOM元素?
React获取组件对应DOM元素的方法
在React中,通常不推荐直接操作DOM,但在需要访问特定元素(如管理焦点、集成第三方库)时,可通过以下方式获取:
1. 使用ref
属性
核心方法:通过ref
绑定到JSX元素,获取其对应的DOM节点。
类组件
-
React.createRef()
创建ref
对象并附加到元素:class MyComponent extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); } componentDidMount() { // 访问DOM节点 console.log(this.myRef.current); // 输出对应的DOM元素 } render() { return <div ref={this.myRef}>Hello</div>; } }
函数组件
-
useRef
Hook
创建可持久化的ref
对象:import { useRef, useEffect } from 'react'; function MyComponent() { const myRef = useRef(null); useEffect(() => { console.log(myRef.current); // 组件挂载后访问 }, []); return <div ref={myRef}>Hello</div>; }
2. 回调Ref(动态绑定)
通过函数接收DOM元素,适用于动态绑定或类组件:
class MyComponent extends React.Component {
setRef = (element) => {
this.myRef = element; // 直接保存DOM元素
};
render() {
return <div ref={this.setRef}>Hello</div>;
}
}
3. 转发Ref(访问子组件DOM)
当需要获取子组件的DOM时,使用React.forwardRef
:
子组件(支持Ref转发)
const ChildComponent = React.forwardRef((props, ref) => {
return <div ref={ref}>{props.children}</div>;
});
父组件
function ParentComponent() {
const childRef = useRef(null);
useEffect(() => {
console.log(childRef.current); // 子组件的DOM元素
}, []);
return <ChildComponent ref={childRef}>Child</ChildComponent>;
}
4. 函数组件暴露方法(useImperativeHandle
)
控制子组件暴露给父组件的实例方法,而非直接暴露DOM:
const ChildComponent = React.forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
}));
return <input ref={inputRef} />;
});
// 父组件调用子组件的focus方法
function ParentComponent() {
const childRef = useRef();
return (
<>
<ChildComponent ref={childRef} />
<button onClick={() => childRef.current.focus()}>聚焦输入框</button>
</>
);
}
注意事项
- 访问时机:
Ref的current
属性在组件挂载后才有效,应在useEffect
或componentDidMount
中访问。 - 避免滥用:
直接操作DOM可能破坏React的声明式特性,优先通过状态(State/Props)控制UI。 - 清理Ref:
若在Ref中保存了订阅或定时器,需在useEffect
的清理函数或componentWillUnmount
中释放资源。
总结
场景 | 方法 |
---|---|
类组件获取DOM | React.createRef() |
函数组件获取DOM | useRef |
动态绑定DOM | 回调Ref(函数形式) |
访问子组件DOM | React.forwardRef |
控制子组件暴露的实例 | useImperativeHandle + forwardRef |
47.React什么是状态提升,子父组件如何通信?
在React中,状态提升(State Lifting)和子父组件通信是组件间数据交互的核心机制
1. 状态提升(State Lifting)
概念
状态提升是指将多个子组件需要共享的状态(State)移动到它们最近的共同父组件中,通过props
向下传递数据,再通过回调函数让子组件通知父组件更新状态。这种方式遵循单向数据流原则,确保状态管理的可预测性和一致性。
适用场景
- 多个组件需要同步同一份数据(如温度转换器、表单联动输入)。
- 需要集中管理状态以避免冗余或冲突。
实现步骤
- 提升状态:将共享状态定义在父组件中。
- 传递数据:通过
props
将状态传递给子组件。 - 传递回调:父组件将更新状态的函数通过
props
传给子组件,子组件触发回调以更新父组件状态。
示例
// 父组件
function TemperatureConverter() {
const [celsius, setCelsius] = useState(0);
return (
<div>
<CelsiusInput value={celsius} onChange={setCelsius} />
<FahrenheitDisplay celsius={celsius} />
</div>
);
}
// 子组件1:输入摄氏度
function CelsiusInput({ value, onChange }) {
return (
<input
type="number"
value={value}
onChange={(e) => onChange(Number(e.target.value))}
/>
);
}
// 子组件2:显示华氏度
function FahrenheitDisplay({ celsius }) {
const fahrenheit = celsius * 9 / 5 + 32;
return <div>{fahrenheit}°F</div>;
}
2. 子父组件通信
核心机制
子组件通过调用父组件传递的回调函数(通过props
)与父组件通信。父组件定义状态更新逻辑,子组件触发回调并传递数据。
实现步骤
- 父组件定义回调:在父组件中编写状态更新函数(如
handleChange
)。 - 传递回调给子组件:通过
props
将回调函数传递给子组件。 - 子组件触发回调:子组件在特定事件(如用户输入)中调用回调,并传递参数。
示例
// 父组件
function Parent() {
const [data, setData] = useState("");
const handleChildData = (childData) => {
setData(childData);
};
return <Child onSendData={handleChildData} />;
}
// 子组件
function Child({ onSendData }) {
const sendData = () => {
onSendData("Data from Child");
};
return <button onClick={sendData}>Send Data to Parent</button>;
}
关键点总结
- 状态提升:共享状态应置于共同父组件,通过
props
下发,保持单一数据源。 - 子父通信:子组件通过父组件传递的回调函数通知状态变更。
- 优势:遵循单向数据流,状态更易追踪,减少冗余和冲突。
通过这种方式,React应用能够实现高效、可维护的组件间通信,适用于复杂场景下的状态管理。
48.React 你用的react是什么版本,react版本有什么区别?
React 版本及区别
1. 我使用的 React 版本
目前使用的是 React 18,但如果项目需要最新特性,我也会关注 React 19 的更新和优化点。
2. 主要 React 版本的区别
版本 | 主要区别 |
---|---|
React 16 (2017) | - 引入 Fiber 架构,优化渲染性能 - React.createRef() 方式创建 ref - 新的生命周期方法,如 getDerivedStateFromProps 、componentDidCatch (错误边界) |
React 17 (2020) | - 主要是渐进式升级,没有新特性 - 事件系统更新,支持事件委托至 root 而非 document |
React 18 (2022) | - 并发渲染(Concurrent Rendering) - useTransition 、useDeferredValue 以优化渲染体验 - React.startTransition 用于标记非紧急更新 - createRoot() 替代 ReactDOM.render() |
React 19 (2024) | - useOptimistic :用于优化状态更新,使 UI 响应更流畅 - useFormStatus & useFormState :改进表单状态管理 - React Compiler(实验性):更智能的代码优化 - React Server Components (RSC) 更稳定,可与 Next.js 结合优化渲染 |
3. React 版本对项目的影响
- 如果是老项目(React 16/17),升级到 React 18 是较稳妥的选择,因为它带来了显著的性能优化,但不会影响现有代码。
- 如果是新项目,可以考虑直接使用 React 19,尤其是在 Server Components 方面有更好的支持,并能提升用户体验。
这样能展现出对 React 版本的全面了解,并结合实际项目需求进行分析。
49.React中setState到底是同步还是异步?
如果我是前端面试者,我会这样回答:
React 中 setState
是同步还是异步?
React 的 setState
既不是完全同步,也不是完全异步,它的行为取决于执行环境:
- 在 React 事件处理(如
onClick
)或生命周期函数中,setState
是异步的,React 会批量更新以提高性能。 - 在原生事件(如
setTimeout
、Promise.then
)或非 React 控制的回调中,setState
是同步的,状态会立即更新。
示例 1:React 事件中 setState
体现异步性
import React, { useState } from "react";
const Example = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log("Before setState:", count); // 0
setCount(count + 1);
console.log("After setState:", count); // 仍然是 0(未立即更新)
};
return <button onClick={handleClick}>Count: {count}</button>;
};
export default Example;
解释:
setState(count + 1)
触发更新,但不会立即修改count
变量,因为 React 在事件处理中会批量更新以优化渲染。console.log("After setState:", count);
仍然是旧值。
示例 2:在 setTimeout
或 Promise
中 setState
是同步的
import React, { useState, useEffect } from "react";
const Example = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log("Before setState:", count); // 0
setCount(count + 1);
console.log("After setState:", count); // 仍然是 0(闭包问题)
}, 1000);
}, []);
return <p>Count: {count}</p>;
};
export default Example;
解释:
- 在
setTimeout
里,setState
不会被批量处理,所以它的执行方式与 JavaScript 的普通函数类似。 - 但注意闭包问题,
count
在setTimeout
内部保持初始值(即 0),导致setCount(count + 1)
其实等于setCount(1)
。 - 解决方案:使用
setCount(prev => prev + 1)
,这样prev
总是最新值。
React 18 及以上:批量更新
React 18 默认开启批处理,即使 setTimeout
或 Promise
中 setState
也会异步执行:
import React, { useState } from "react";
const Example = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
Promise.resolve().then(() => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
});
};
return <button onClick={handleClick}>Count: {count}</button>;
};
export default Example;
- React 18 之前:两次
setCount
会执行两次更新,最终count
只加 1。 - React 18 之后:React 自动批处理,最终
count
会加 2。
总结
场景 | setState 是同步还是异步? | 说明 |
---|---|---|
React 事件处理 | 异步 | React 会批量更新,优化性能 |
生命周期函数 | 异步 | 如 componentDidMount 、useEffect |
setTimeout 、Promise | React 17 及以前同步,React 18 及以上异步 | React 18 开启了自动批处理 |
useTransition 、startTransition | 异步 | 标记低优先级更新,提高流畅度 |
最佳实践
- 要获取最新的 state,使用回调形式
setCount(prev => prev + 1);
- React 18 需要
flushSync()
强制同步
import { flushSync } from "react-dom";
flushSync(() => {
setCount(count + 1);
});
console.log(count); // 立即获取新值
总结一句话
setState
在 React 事件和生命周期中是 异步的,在 setTimeout
等原生异步任务中是 同步的(React 18 之后默认也是异步的)。
50.在React如何实现Vue中的expose的能力?
如何在 React 中实现 Vue 3 的 expose
能力?
在 Vue 3 中,expose
允许子组件显式暴露特定的属性或方法,避免父组件访问子组件整个实例。例如:
<script setup>
import { ref, defineExpose } from "vue";
const count = ref(0);
const increment = () => count.value++;
defineExpose({ count, increment }); // 仅暴露 count 和 increment
</script>
React 中如何实现类似能力?
React 没有 expose
这个 API,但可以通过 forwardRef
+ useImperativeHandle
组合实现类似的功能,使父组件只能访问子组件暴露的方法,而非整个子组件实例。
示例:使用 forwardRef
+ useImperativeHandle
import React, { useState, forwardRef, useImperativeHandle } from "react";
// 子组件
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0);
const increment = () => setCount((prev) => prev + 1);
// 仅暴露 `increment` 方法
useImperativeHandle(ref, () => ({
increment,
}));
return <p>Count: {count}</p>;
});
// 父组件
const Parent = () => {
const childRef = React.useRef(null);
return (
<div>
<Child ref={childRef} />
<button onClick={() => childRef.current?.increment()}>增加</button>
</div>
);
};
export default Parent;
对比 Vue expose
和 React useImperativeHandle
框架 | API | 作用 |
---|---|---|
Vue 3 | defineExpose | 仅暴露指定属性和方法给父组件 |
React | useImperativeHandle | 通过 ref 仅暴露特定方法,避免父组件访问不必要的状态 |
总结
在 React 中,useImperativeHandle
+ forwardRef
可以实现 Vue expose
的能力,确保父组件只访问必要的方法,而不是整个子组件实例。这在 封装组件库或希望控制组件暴露接口 时非常有用。
51.useLayoutEffect和useEffect 有什么区别呢?
1. useLayoutEffect 基本概念
useLayoutEffect 是 React 的一个 Hook,它的函数签名与 useEffect 完全相同,但它会在所有的 DOM 变更之后同步调用 effect。它可以用来读取 DOM 布局并同步触发重渲染。
2. useLayoutEffect vs useEffect
2.1 执行时机对比
Hook 名称 | 执行时机 | 执行方式 | 使用场景 |
---|---|---|---|
useEffect | DOM 更新后且浏览器重新绘制屏幕之后异步执行 (组件渲染完成后) | 异步执行,不阻塞浏览器渲染 | 大多数副作用,如数据获取、订阅 |
useLayoutEffect | DOM 更新后且浏览器重新绘制屏幕之前同步执行(组件将要渲染时) | 同步执行,会阻塞浏览器渲染 | 需要同步测量 DOM 或更新布局 |
2.2 执行顺序示例
function ExampleComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('useEffect 执行'); // 后执行
});
useLayoutEffect(() => {
console.log('useLayoutEffect 执行'); // 先执行
});
return (
<div onClick={() => setCount(c => c + 1)}>
点击次数:{count}
</div>
);
}
3.1 何时使用 useLayoutEffect
- 需要同步测量 DOM 元素
- 需要在视觉更新前进行 DOM 修改
- 需要避免闪烁或布局抖动
- 处理依赖于 DOM 布局的动画
3.2 何时使用 useEffect
- 数据获取
- 订阅事件
- 日志记录
- 其他不需要同步 DOM 测量或修改的副作用
4. 最佳实践
- 优先使用 useEffect
// ✅ 大多数情况下使用 useEffect 即可
useEffect(() => {
// 异步操作,不影响渲染
fetchData();
}, []);
- 仅在必要时使用 useLayoutEffect
// ✅ 需要同步 DOM 测量和更新时使用 useLayoutEffect
useLayoutEffect(() => {
// 同步操作,立即更新 DOM
updateDOMPosition();
}, []);
- 注意性能影响
// ❌ 避免在 useLayoutEffect 中进行耗时操作
useLayoutEffect(() => {
// 不要在这里进行大量计算或 API 调用
heavyComputation();
}, []);
// ✅ 耗时操作应该放在 useEffect 中
useEffect(() => {
heavyComputation();
}, []);
6. 注意事项
- useLayoutEffect 在服务器端渲染(SSR)中会收到警告,因为它只能在客户端执行
- 过度使用 useLayoutEffect 可能会导致性能问题
- 应该将耗时的操作放在 useEffect 中,只在 useLayoutEffect 中处理视觉相关的同步更新
52.React如何实现一个 withRouter?(React Router v6 之后,withRouter
被移除)选背
如果我是前端面试者,我会这样回答:
如何在 React 中实现 withRouter
?
在 React Router v5 及之前,withRouter
是一个高阶组件(HOC),用于向类组件提供 history
、location
和 match
这三个路由对象。
但在 React Router v6 之后,withRouter
被移除,官方推荐使用 useNavigate
、useLocation
等 Hook 直接在函数组件中使用。
1. 在 React Router v5 中手写 withRouter
实现一个 withRouter
高阶组件:
import { withRouter } from "react-router-dom";
const MyComponent = ({ history, location, match }) => {
return (
<div>
<p>Current Path: {location.pathname}</p>
<button onClick={() => history.push("/home")}>Go Home</button>
</div>
);
};
export default withRouter(MyComponent);
手写实现 withRouter
:
其实就是写一个叫withRouter的HOC,传入组件component,将history
、location
和 match
用属性代理的方式传入然后返回新的component,本质是一个高阶组件的用法
import { useNavigate, useLocation, useParams } from "react-router-dom";
const withRouter = (Component) => {
return (props) => {
const navigate = useNavigate();
const location = useLocation();
const params = useParams();
return <Component {...props} navigate={navigate} location={location} params={params} />;
};
};
export default withRouter;
使用方式
const MyComponent = ({ navigate, location, params }) => {
return (
<div>
<p>Current Path: {location.pathname}</p>
<button onClick={() => navigate("/home")}>Go Home</button>
</div>
);
};
export default withRouter(MyComponent);
2. 在 React Router v6 中替代 withRouter
React Router v6 移除了 withRouter
,推荐直接在函数组件中使用 useNavigate
和 useLocation
:
import { useNavigate, useLocation, useParams } from "react-router-dom";
const MyComponent = () => {
const navigate = useNavigate();
const location = useLocation();
const params = useParams();
return (
<div>
<p>Current Path: {location.pathname}</p>
<button onClick={() => navigate("/home")}>Go Home</button>
</div>
);
};
export default MyComponent;
3. withRouter
适用场景
虽然在 React Router v6 中推荐使用 Hook,但如果项目中仍然使用 类组件,可以用 withRouter
来注入路由信息:
import React from "react";
import { withRouter } from "./withRouter"; // 之前实现的手写 HOC
class ClassComponent extends React.Component {
render() {
return (
<div>
<p>Current Path: {this.props.location.pathname}</p>
<button onClick={() => this.props.navigate("/home")}>Go Home</button>
</div>
);
}
}
export default withRouter(ClassComponent);
总结
- React Router v5 及之前:
withRouter
是官方提供的 HOC,可用于类组件。 - React Router v6 及以后:推荐使用
useNavigate
、useLocation
等 Hook,直接在函数组件中获取路由信息。 - 如果必须支持类组件:可以自己手写
withRouter
,使用useNavigate
和useLocation
封装 HOC。
53.react-router-dom v6 提供了哪些新的API?
总结
API | 作用 | 替代的旧 API |
---|---|---|
createBrowserRouter | 新的路由创建方式 | <BrowserRouter> |
useRoutes | 动态生成路由 | <Routes> |
useNavigate | 进行页面跳转 | useHistory().push() |
useSearchParams | 处理 URL 查询参数 | window.location.search |
useLoaderData | 加载数据 | useEffect |
Outlet | 处理嵌套路由 | children |
useParams | 获取 URL 参数 | match.params |
Navigate | 重定向 | Redirect |
** |
React Router v6 新 API 介绍
React Router v6 于 2021 年发布,相比 v5 进行了重大改进,包括 API 变化、更简洁的写法以及更强大的功能。
1. createBrowserRouter
/ createHashRouter
用于替代
<BrowserRouter>
,支持数据加载和路由控制。
import { createBrowserRouter, RouterProvider } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/about",
element: <About />,
},
]);
export default function App() {
return <RouterProvider router={router} />;
}
✅ 优势:
- 允许在路由定义时加载数据
- 适用于 React 18 的流式渲染
2. useRoutes
动态创建路由,替代
<Switch>
import { useRoutes } from "react-router-dom";
const routes = [
{ path: "/", element: <Home /> },
{ path: "/about", element: <About /> },
];
const App = () => {
const element = useRoutes(routes);
return element;
};
✅ 优势:
- 更简洁,不需要手写
<Routes>
和<Route>
- 灵活可配置,适用于动态路由
3. useNavigate
替代
useHistory
,进行导航跳转
import { useNavigate } from "react-router-dom";
const Home = () => {
const navigate = useNavigate();
return <button onClick={() => navigate("/about")}>Go to About</button>;
};
✅ 优势:
- 更清晰,不再需要
history.push()
- 支持向前/向后跳转:
navigate(-1)
4. useSearchParams
用于管理 URL 查询参数
import { useSearchParams } from "react-router-dom";
const Users = () => {
const [searchParams, setSearchParams] = useSearchParams();
const userId = searchParams.get("id");
return (
<div>
<p>User ID: {userId}</p>
<button onClick={() => setSearchParams({ id: "123" })}>Set ID</button>
</div>
);
};
✅ 优势:
- 替代
query-string
,更简洁 - 直接修改 URL,无需手动拼接参数
5. useLoaderData
配合
loader
进行数据加载
import { createBrowserRouter, RouterProvider, useLoaderData } from "react-router-dom";
const fetchUser = async () => {
const res = await fetch("/api/user");
return res.json();
};
const Profile = () => {
const user = useLoaderData();
return <p>{user.name}</p>;
};
const router = createBrowserRouter([
{
path: "/profile",
element: <Profile />,
loader: fetchUser,
},
]);
const App = () => <RouterProvider router={router} />;
✅ 优势:
- SSR 友好,支持数据预加载
- 简化数据获取,无需
useEffect
6. Outlet
用于嵌套路由
const Layout = () => (
<div>
<h1>Header</h1>
<Outlet /> {/* 这里会渲染子路由 */}
</div>
);
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{ path: "home", element: <Home /> },
{ path: "about", element: <About /> },
],
},
]);
✅ 优势:
- 清晰管理嵌套路由
- 更符合组件化设计
7. useParams
获取动态路由参数
import { useParams } from "react-router-dom";
const User = () => {
const { id } = useParams();
return <p>User ID: {id}</p>;
};
// 路由配置: path: "/user/:id"
✅ 优势:
- 更简单,无需
match.params
- 与
useNavigate
结合使用,体验更佳
8. Navigate
组件
替代
Redirect
import { Navigate } from "react-router-dom";
const PrivateRoute = ({ isAuth }) => {
return isAuth ? <Dashboard /> : <Navigate to="/login" />;
};
✅ 优势:
- 更符合 JSX 语法
- 清晰的重定向逻辑
54.useRoutes是如何使用的?如何使用useRoutes进行动态路由加载
如果我是前端面试者,我会这样回答:
1. useRoutes
介绍
useRoutes
是 React Router v6 提供的一个 Hook,用于基于配置动态生成路由,它替代了 v5 版本的 <Routes>
+ <Route>
组合,让路由更加声明式和清晰。
2. useRoutes
基本用法
替代
<Routes>
直接定义路由
传统 <Routes>
方式(v5/v6)
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./Home";
import About from "./About";
const App = () => (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
使用 useRoutes
方式(v6 推荐)
import { BrowserRouter, useRoutes } from "react-router-dom";
import Home from "./Home";
import About from "./About";
const routes = [
{ path: "/", element: <Home /> },
{ path: "/about", element: <About /> },
];
const AppRoutes = () => {
return useRoutes(routes);
};
const App = () => (
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
);
✅ 优势:
- 让路由更加模块化,避免在
App.tsx
里堆积<Route>
- 适用于动态路由加载
3. useRoutes
处理嵌套路由
如果有子路由,可以使用 children
进行嵌套:
import { BrowserRouter, useRoutes, Outlet } from "react-router-dom";
const Layout = () => (
<div>
<h1>Header</h1>
<Outlet /> {/* 子路由会渲染到这里 */}
</div>
);
const routes = [
{
path: "/",
element: <Layout />,
children: [
{ path: "home", element: <Home /> },
{ path: "about", element: <About /> },
],
},
];
const AppRoutes = () => useRoutes(routes);
const App = () => (
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
);
✅ 优势:
Outlet
允许嵌套渲染子路由,替代this.props.children
- 更容易管理多级路由
4. useRoutes
进行动态路由加载
在实际项目中,通常需要按需加载路由组件,减少首屏加载时间。可以结合 React.lazy + Suspense 实现动态导入:
🚀 使用 React.lazy
进行按需加载
import { BrowserRouter, useRoutes } from "react-router-dom";
import { lazy, Suspense } from "react";
// 动态导入组件
const Home = lazy(() => import("./Home"));
const About = lazy(() => import("./About"));
const routes = [
{ path: "/", element: <Home /> },
{ path: "/about", element: <About /> },
];
const AppRoutes = () => useRoutes(routes);
const App = () => (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<AppRoutes />
</Suspense>
</BrowserRouter>
);
✅ 优势:
- 仅在访问对应页面时才加载组件,提升首屏加载速度
- 适用于大型项目,减少初始打包体积
5. useRoutes
结合后端动态生成路由
在某些场景下,前端的路由数据可能是由后端返回的。例如,后端返回用户可访问的菜单,我们可以根据 API 返回的路由表动态生成路由:
import { BrowserRouter, useRoutes } from "react-router-dom";
import { useEffect, useState } from "react";
const fetchRoutesFromAPI = async () => {
return [
{ path: "/", element: <Home /> },
{ path: "/about", element: <About /> },
];
};
const AppRoutes = () => {
const [routes, setRoutes] = useState([]);
useEffect(() => {
fetchRoutesFromAPI().then(setRoutes);
}, []);
return useRoutes(routes);
};
const App = () => (
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
);
✅ 优势:
- 适用于权限管理,根据用户权限动态生成可访问路由
- 后端驱动路由,前端无需硬编码
6. useRoutes
vs 传统 <Routes>
特性 | useRoutes | <Routes> |
---|---|---|
代码简洁度 | ✅ 更清晰,避免 JSX 嵌套 | ❌ 需要多个 <Route> |
动态加载 | ✅ 支持 React.lazy | ❌ 需要手动 Suspense |
动态路由 | ✅ 适用于 API 获取路由 | ❌ 需手动 map 生成 <Route> |
嵌套路由 | ✅ children 方式 | ✅ 但更麻烦 |
适用场景 | ✅ 组件化、大型项目 | ✅ 适合小项目 |
7. 总结
useRoutes
让路由声明更加清晰,避免 JSX 里嵌套<Route>
。- 适用于动态路由,可以从后端 API 加载路由表并渲染。
- 结合
React.lazy
实现懒加载,提高首屏加载速度。 - 推荐在 React Router v6 中使用,特别是当路由结构较复杂时。
✨ useRoutes
适用于大型项目,配合动态加载、权限管理等方案,能显著优化路由管理!
55.redux的中间件是如何实现的?
redux中间件是指在redux发出action后执行Reducer改变state之前劫持action做出其他开发者想操作的多余步骤如打印log,接受函数式dispatch,处理异步操作或者promise等然后再分发action到reducer改变 state。
实现原理是通过 **applyMiddleware
**方法将中间件从右左形成中间件函数调用链,一层一层的调用中间件,最好执行真正的dispatch去出发Reducer。同时将store和 dispatch重写放入调用链中增强dispatch功能。
子问题:
applyMiddleware为什么要从右往左进行反向组装中间件链:从右到左依次包装 dispatch
Redux的applyMiddleware
从右到左反向组装中间件链,是为了确保中间件的执行顺序与用户传入的顺序一致(从左到右) 。这种设计源于函数式编程的组合逻辑:每个中间件接收的next
参数代表链中下一个中间件处理后的dispatch
,通过从右到左依次包装,最终形成的调用链会按照中间件的声明顺序(从左到右)依次执行。例如,中间件[A, B, C]
的组合顺序是C → B → A
,但执行时A
最先处理Action,调用next
后触发B
,再调用next
触发C
,最后到达原始dispatch
,从而保证用户直观的中间件执行顺序。
具体的实现原理如下
Redux 中间件的作用
在 Redux 中,中间件(Middleware)用于扩展 dispatch
方法的功能,可以在派发(dispatch)Action 和 Reducer 处理之前执行额外的逻辑,比如:
- 处理 异步操作(如
redux-thunk
、redux-saga
) - 日志记录(如
redux-logger
) - 拦截/修改 action(如
redux-promise
) - 错误处理
Redux 中间件的核心在于增强 dispatch
方法,使其能够处理函数、Promise 等异步操作,而不仅仅是普通的对象。
Redux 中间件的实现原理
Redux 的中间件是基于函数式编程的高阶函数(Higher-Order Function) 。
一个 Redux 中间件的基本结构如下:
const exampleMiddleware = (store) => (next) => (action) => {
console.log("中间件触发:", action);
return next(action); // 继续传递 action
};
解析:
store
—— Redux store 对象,包含getState()
和dispatch()
方法。next
—— 代表下一个中间件,或者最终到达reducer
。action
—— 被 dispatch 的 action。
✅ 核心逻辑:
- 在
dispatch(action)
之后,中间件先拦截 action,可以对 action 进行修改、日志记录、异步处理等操作。 - 调用
next(action)
将 action 传递给下一个中间件,最终到达 reducer。 - 如果不调用
next(action)
,Redux 流程就会被中断(用于拦截某些 action)。
手写 Redux 中间件机制
1️⃣ Redux applyMiddleware
实现
Redux 提供 applyMiddleware
这个方法,它的核心逻辑如下:
const applyMiddleware = (...middlewares) => (createStore) => (reducer) => {
const store = createStore(reducer); // 创建 Redux store
let dispatch = store.dispatch; // 原始 dispatch 方法
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action), // 让中间件能够调用 dispatch
};
// 依次执行每个中间件,得到增强后的 dispatch
const chain = middlewares.map((middleware) => middleware(middlewareAPI));
dispatch = chain.reduceRight((next, middleware) => middleware(next), store.dispatch);
return { ...store, dispatch }; // 返回增强后的 store
};
✅ 核心流程:
- 先创建 Redux
store
,获取原始dispatch
方法。 - 给中间件传递
middlewareAPI
(getState
、dispatch
)。 - 链式调用所有中间件,最终生成增强版
dispatch
,替换 Redux 默认的dispatch
。
2️⃣ 使用 applyMiddleware
来增强 Redux
import { createStore, applyMiddleware } from "redux";
// 自定义一个日志中间件
const loggerMiddleware = (store) => (next) => (action) => {
console.log("当前状态:", store.getState());
console.log("派发 action:", action);
const result = next(action); // 继续执行 action
console.log("更新后状态:", store.getState());
return result;
};
// Reducer
const reducer = (state = { count: 0 }, action) => {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
default:
return state;
}
};
// 创建 Redux store,使用 applyMiddleware 处理中间件
const store = createStore(reducer, applyMiddleware(loggerMiddleware));
// 触发 action
store.dispatch({ type: "INCREMENT" });
✅ 运行结果:
当前状态: { count: 0 }
派发 action: { type: "INCREMENT" }
更新后状态: { count: 1 }
总结:
applyMiddleware(loggerMiddleware)
增强了dispatch
,在每次dispatch(action)
时打印日志。next(action)
负责传递 action,最终到达reducer
,否则 Redux 流程会被拦截。
3️⃣ Redux 异步中间件示例
Redux 默认不支持异步 action,我们需要用中间件来处理异步逻辑。
✅ Redux-Thunk(处理 dispatch
函数)
redux-thunk
允许 dispatch
支持函数,而不仅仅是对象:
const thunkMiddleware = (store) => (next) => (action) => {
if (typeof action === "function") {
return action(store.dispatch, store.getState);
}
return next(action);
};
// 使用 thunk 中间件
const store = createStore(reducer, applyMiddleware(thunkMiddleware));
// 异步 action
const fetchData = () => {
return (dispatch) => {
setTimeout(() => {
dispatch({ type: "INCREMENT" });
}, 1000);
};
};
// 触发异步 action
store.dispatch(fetchData());
✅ thunkMiddleware
逻辑:
- 如果
action
是一个函数,则执行这个函数,并传入dispatch
和getState
,支持异步操作。 - 如果
action
是普通对象,直接传递给next(action)
,进入 reducer 处理。
✅ Redux-Promise(处理 Promise
action)
redux-promise
让 dispatch
支持 Promise,比如:
const promiseMiddleware = (store) => (next) => (action) => {
if (action instanceof Promise) {
return action.then(next);
}
return next(action);
};
// 使用 promise 中间件
const store = createStore(reducer, applyMiddleware(promiseMiddleware));
// 触发 Promise action
store.dispatch(
new Promise((resolve) => {
setTimeout(() => resolve({ type: "INCREMENT" }), 1000);
})
);
✅ 原理:
- 如果
action
是一个Promise
,等Promise
解析后再执行next(action)
,使dispatch
能够直接处理异步请求。
4️⃣ 总结
-
Redux 中间件是
dispatch
的高阶增强,可以拦截、修改、异步处理 action。 -
核心实现是:
(store) => (next) => (action) => {}
这种 函数式组合。- 通过
applyMiddleware
增强dispatch
,形成中间件链。
-
常见 Redux 中间件:
redux-thunk
→ 让dispatch
支持function
,用于异步操作。redux-promise
→ 让dispatch
支持Promise
,简化异步请求。redux-logger
→ 记录dispatch
过程中的state
变化。
-
手写中间件的关键点:
- 拦截
dispatch(action)
- 可以修改/处理
action
- 调用
next(action)
传递给 reducer
- 拦截
总结一句话:Redux 中间件是一个 dispatch
的增强函数,它让 Redux 可以处理异步、日志、权限等功能,核心就是一个 store -> next -> action
结构的高阶函数! 🚀
56.React的render props是什么?
如果我是前端面试者,我会这样回答:
什么是 Render Props?
Render Props
是 React 组件的一种模式,它指的是:一个组件接收一个函数作为 props
,然后在组件内部调用这个函数并渲染其返回的内容。
通常用于 组件复用,特别是共享组件逻辑(例如:状态管理、动画、数据获取等)。
Render Props 的基本用法
const MouseTracker = (props) => {
const [position, setPosition] = React.useState({ x: 0, y: 0 });
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
};
return (
<div style={{ height: "200px", border: "1px solid black" }} onMouseMove={handleMouseMove}>
{props.render(position)} {/* 调用 render props 函数 */}
</div>
);
};
const App = () => (
<MouseTracker
render={(position) => <h1>鼠标位置:{position.x}, {position.y}</h1>}
/>
);
✅ 工作原理:
MouseTracker
组件封装了鼠标位置的状态,并通过render
传递给子组件。App
组件通过render
传入函数,决定如何渲染position
数据。- 这避免了继承,允许灵活的 UI 复用。
Render Props vs 其他模式
模式 | 适用场景 | 缺点 |
---|---|---|
Render Props | 组件复用、状态共享 | 嵌套较深时可能影响可读性 |
HOC(高阶组件) | 逻辑复用,如权限控制 | 可能导致 props 冲突,React DevTools 难以调试 |
Hooks | 现代 React 组件逻辑复用 | 仅适用于函数组件 |
Render Props 的应用场景
- 共享组件状态(如
MouseTracker
示例) - 数据获取(如封装
fetch
逻辑) - 动画、过渡(如
react-motion
)
Render Props 现状
React 16.8+ 之后,Hooks
(如 useState
, useEffect
, useContext
)的出现减少了 Render Props 的使用,因为 Hooks
让组件逻辑复用变得更加简洁。
但在某些场景下,Render Props 仍然适用,特别是当你想要封装一个逻辑复用但 UI 仍然可自定义的组件时。
总结:Render Props 是 React 组件复用的一种模式,通过传递函数 props
让组件逻辑和 UI 解耦,适用于状态共享、数据获取等场景。 🚀
57.为什么之前react组件要写import react from ‘react’,现在又不用了?
在 React 17 之前,组件需要显式导入 React
(如 import React from 'react'
),是因为 JSX 语法会被 Babel 等工具转换为 React.createElement
调用,必须确保 React
在作用域内可用。而 React 17 引入了 新的 JSX 转换方式,通过自动从 react/jsx-runtime
注入 jsx
或 jsxs
函数,不再依赖全局的 React
变量,因此无需手动导入。这一变化简化了代码,减少了冗余,同时为未来优化(如更高效的编译输出)奠定了基础。
详细解释
-
React 17 之前
-
JSX 转换依赖
React.createElement
:
当编写<Button />
时,Babel 会将其转换为React.createElement(Button, null)
,因此必须导入React
以访问createElement
方法。// 代码 import React from 'react'; function App() { return <Button />; } // 转换后 import React from 'react'; function App() { return React.createElement(Button, null); }
-
-
React 17+ 的新 JSX 转换
-
自动引入运行时函数:
Babel 会将 JSX 转换为_jsx
或_jsxs
函数,这些函数从react/jsx-runtime
自动导入,不再依赖全局React
。// 代码(无需导入 React) function App() { return <Button />; } // 转换后 import { jsx as _jsx } from 'react/jsx-runtime'; function App() { return _jsx(Button, {}); }
-
-
优势
- 代码更简洁:省略不必要的
import React
。 - 避免错误:消除因忘记导入
React
导致的React is not defined
错误。 - 性能优化:新的 JSX 运行时可能生成更高效的代码(如编译时优化)。
- 代码更简洁:省略不必要的
-
注意事项
-
类组件仍需导入
React
:若使用class App extends React.Component
,仍需导入React
。 -
直接使用 React API:如
useState
、useEffect
需从'react'
导入,但无需引入整个React
对象:import { useState } from 'react'; // ✅ 正确 // import React from 'react'; // ❌ 不再需要
-
总结
- 旧方式:JSX →
React.createElement
→ 强制导入React
。 - 新方式:JSX →
_jsx
(自动注入)→ 无需导入React
。 - 升级条件:使用 React 17+ 和 Babel 7.9.0+(或相应工具链)。
59.说说 stack reconciler和fiber reconciler
在 React 中,Stack Reconciler 和 Fiber Reconciler 是两种不同的调和(Reconciliation)算法,主要区别在于性能优化和更新方式。
1. Stack Reconciler(旧版调和器)
特点:
- React 15 及之前使用的是 Stack Reconciler。
- 采用 递归调用 组件树的方式进行协调(Reconciliation)。
- 由于 JavaScript 引擎的调用栈大小有限,组件树过深时可能会导致 递归调用栈溢出。
- 同步更新:一旦开始调和,就必须一次性完成整个更新任务,无法中断。
缺点:
- 递归调用导致大任务无法拆分,阻塞主线程,影响用户体验(如页面卡顿)。
- 无法进行任务优先级调度,所有更新一视同仁。
2. Fiber Reconciler(新版调和器)
特点:
- React 16 及之后引入 Fiber Reconciler,完全重写了协调算法。
- 采用 Fiber 数据结构,将组件树转换为一个可操作的链表结构,使调和过程变成 可中断、可恢复 的。
- 时间切片(Time Slicing) :更新可以被拆分成多个小任务,并在浏览器的空闲时间继续执行,从而提高页面响应速度。
- 任务优先级调度:React 可以根据更新的重要性(如用户输入 vs. 动画 vs. 数据加载)分配不同的优先级。
Fiber 工作原理:
-
Render 阶段(可中断)
- 以 深度优先遍历 方式遍历 Fiber 树,创建新的 Fiber 节点,并标记需要更新的部分。
- 这一步可以被 分片执行,允许浏览器在空闲时继续计算,避免卡顿。
-
Commit 阶段(不可中断)
-
经过调度后,将最终的变更提交到 DOM 上,分为:
- beforeMutation(调用
getSnapshotBeforeUpdate
) - Mutation(更新 DOM)
- Layout(触发
componentDidMount
和componentDidUpdate
)
- beforeMutation(调用
-
总结
对比项 | Stack Reconciler | Fiber Reconciler |
---|---|---|
数据结构 | 组件树(递归) | Fiber 链表(双缓冲) |
调和方式 | 递归同步 | 可中断 & 分片调度 |
性能 | 任务无法拆分,容易阻塞 | 任务可拆分,提升页面流畅度 |
任务优先级 | 一视同仁 | 可分配不同优先级 |
React 版本 | React 15 及之前 | React 16 及之后 |
Fiber Reconciler 解决了 Stack Reconciler 中的同步阻塞问题,使 React 可以更高效地渲染 UI,特别是在复杂应用和动画交互中带来了更好的用户体验。
你可以补充一些实际的 Fiber 调度 API(如 requestIdleCallback
、scheduler
任务调度)来展示你的深入理解!
60.react中有哪几种数据结构,分别是干什么的?
在 React 源码中,主要涉及以下几种重要的数据结构,每种都有特定的用途:
1. Fiber 树(FiberNode)—— 组件调和
-
作用:用于描述 React 组件树,并支持可中断的渲染更新。
-
数据结构:双向链表(单个 Fiber 节点有
child
、sibling
、return
指针)。 -
关键字段:
tag
:表示当前 Fiber 节点的类型(如函数组件、类组件、DOM 元素等)。stateNode
:存放与 Fiber 关联的 DOM 节点或组件实例。child
、sibling
、return
:指向子节点、兄弟节点、父节点,形成 Fiber 树。alternate
:指向前一次更新的 Fiber,形成 双缓存机制,用于 Diff 计算。
2. Update Queue(更新队列)—— 组件状态管理
-
作用:存储组件的
state
更新任务。 -
数据结构:链表,存放多个
update
对象(如setState
触发的更新)。 -
关键字段:
shared.pending
:指向等待处理的更新。baseState
:上一次计算的state
值。memoizedState
:本次计算的state
值。effects
:存放副作用(useEffect
)的更新列表。
3. Effect List(副作用链表)—— 处理副作用
-
作用:存储需要执行的副作用(
useEffect
、componentDidMount
、componentDidUpdate
等)。 -
数据结构:单向链表(所有需要执行副作用的 Fiber 形成链表)。
-
关键字段:
flags
:标记当前 Fiber 节点的副作用类型,如Placement
(插入)、Update
(更新)、Deletion
(删除)。nextEffect
:指向下一个需要执行副作用的 Fiber 节点。
4. Lanes & Scheduler(优先级调度)—— 任务调度
-
作用:控制 React 的并发更新,确保高优先级任务先执行。
-
数据结构:位运算(bitmask),类似二进制位的调度系统。
-
关键字段:
lanes
:存储当前任务的优先级,React 16+ 通过 bitmask 进行任务分配。currentPriorityLevel
:当前正在执行的任务优先级。pendingLanes
:所有等待执行的任务。
5. Hook 链表(useState、useEffect)—— 函数组件状态管理
-
作用:管理
useState
、useReducer
、useEffect
等 Hook 状态。 -
数据结构:单向链表,每个 Hook 形成链表节点。
-
关键字段:
memoizedState
:存储 Hook 的当前值。next
:指向下一个 Hook 节点。queue
:存储setState
触发的更新队列。
总结
数据结构 | 作用 | 关键字段 |
---|---|---|
Fiber 树 | 组件调和(可中断更新) | tag 、stateNode 、child 、sibling |
Update Queue | 管理 state 更新 | shared.pending 、memoizedState |
Effect List | 处理副作用(useEffect ) | flags 、nextEffect |
Lanes & Scheduler | 任务优先级调度 | lanes 、pendingLanes |
Hook 链表 | 函数组件状态管理 | memoizedState 、next |
61.说一下react的更新流程
React 的更新流程
React 的更新流程可以分为 触发更新、调和(Reconciliation)、提交(Commit) 三个阶段。React 采用 Fiber Reconciler 进行调和,使更新可以拆分并中断,提高渲染效率。
1. 触发更新(Trigger Update)
触发方式
更新可以由以下方式触发:
setState
(类组件)useState/useReducer
(函数组件)forceUpdate
Context
变化props
变更Suspense
触发回退 UI事件、定时器等外部触发
存入更新队列
- 每个
Fiber
节点都有一个 更新队列(Update Queue) ,当setState
等触发更新时,新的update
被加入队列。 React
通过Lanes
机制计算优先级,决定何时执行更新。
2. 调和(Reconciliation,Render 阶段)
- 工作原理:使用 Fiber 架构 遍历组件树,计算需要更新的部分。
- 特点:这个阶段是 可中断的(React 18 中的并发模式利用
requestIdleCallback
和Scheduler
进行时间切片)。
过程
-
生成新的 Fiber 树
- 通过
workInProgress
指向当前正在构建的新 Fiber 树。 - 通过 Diff 算法 对比新旧 Fiber 树,标记需要更新的节点。
- 生成
Effect List
记录需要执行的副作用(如useEffect
)。
- 通过
-
任务调度
- React 使用 Scheduler(调度器) 分配更新优先级。
- 高优先级(如用户输入)可以中断低优先级(如网络请求)。
-
计算更新
- 组件的
render
方法或函数组件
被执行,返回新的VNode
结构。 - 更新
memoizedState
,存储新的state
计算结果。
- 组件的
3. 提交(Commit 阶段,不可中断)
作用:将更新应用到 DOM,并执行副作用。
阶段
-
Before Mutation(更新前)
- 触发
getSnapshotBeforeUpdate
- 记录旧 DOM 信息
- 触发
-
Mutation(更新 DOM)
- 遍历
Effect List
,执行Placement
(插入)、Update
(更新)、Deletion
(删除) - React 直接操作 DOM 进行渲染
- 遍历
-
Layout(更新后)
- 触发
componentDidMount
、componentDidUpdate
- 执行
useEffect
、useLayoutEffect
- 组件状态更新完成
- 触发
4. 重点总结
阶段 | 作用 | 关键点 |
---|---|---|
触发更新 | 组件 setState 、props 变化 | Update Queue 、Lanes 机制 |
调和(Render) | 计算更新部分,构建新 Fiber 树 | Diff 算法 、Effect List |
提交(Commit) | 更新 DOM,执行副作用 | Mutation 、useEffect 、生命周期 |
优化点
-
减少不必要的渲染
React.memo
useMemo
/useCallback
shouldComponentUpdate
-
避免 Reconciliation
useRef
保存不变数据PureComponent
避免无效更新
-
提高并发性能
startTransition
(低优先级更新)useDeferredValue
(减少重新渲染)
理解 React 的更新流程 及 优化策略,可以更高效地调试和优化应用,提高面试通过率!
62.什么是闭包陷阱?
闭包陷阱(Closure Trap)
闭包(Closure) 是 JavaScript 重要的特性之一,它允许函数“记住”其定义时的作用域。然而,使用不当时,会导致 性能问题、变量引用错误、意外的内存泄漏 等问题,这些问题被称为 闭包陷阱(Closure Trap) 。
常见闭包陷阱
1. 循环中的闭包(var 作用域问题)
📌 问题:var
没有块级作用域,导致所有回调函数共享同一个变量,最终 i
变成了 3
。
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 3, 3, 3
}, 1000);
}
✅ 解决方案:
- 使用
let
使i
形成块级作用域 - 使用 立即执行函数表达式(IIFE) 传递
i
// 方案1:使用 let
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2
}, 1000);
}
// 方案2:使用 IIFE
for (var i = 0; i < 3; i++) {
(function (i) {
setTimeout(() => {
console.log(i); // 0, 1, 2
}, 1000);
})(i);
}
2. 闭包导致内存泄漏
📌 问题:闭包中的变量被长时间引用,导致无法被垃圾回收。
function createClosure() {
let largeData = new Array(1000000); // 占用大量内存
return function () {
console.log(largeData.length);
};
}
const closureFn = createClosure(); // `largeData` 无法被回收
✅ 解决方案:
- 显式置
null
,释放引用 - 仅在必要时使用闭包
function createClosure() {
let largeData = new Array(1000000);
return function () {
console.log(largeData.length);
largeData = null; // 释放内存
};
}
3. 事件监听中的闭包
📌 问题:事件监听中使用闭包,导致 DOM 变量无法被回收。
function attachEvent() {
const element = document.getElementById("btn");
element.addEventListener("click", function () {
console.log(element.id);
});
}
attachEvent(); // `element` 仍然在闭包中,无法回收
✅ 解决方案:
- 解绑事件监听
- 使用
this
代替闭包
function attachEvent() {
const element = document.getElementById("btn");
element.addEventListener("click", function () {
console.log(this.id); // 这里 `this` 指向 `element`
});
element = null; // 释放引用
}
总结
闭包陷阱 | 原因 | 解决方案 |
---|---|---|
循环中的闭包 | var 作用域导致变量共享 | 使用 let 或 IIFE |
内存泄漏 | 变量被闭包长时间引用 | 置 null 释放引用 |
事件监听泄漏 | DOM 节点被闭包引用 | 解绑事件监听,使用 this |
63.闭包陷阱的成因与解法?闭包陷阱的成因与解法?
闭包陷阱的成因与解法
闭包(Closure) 是 JavaScript 中的一个强大特性,它允许函数“记住”创建它时的作用域。然而,如果使用不当,闭包可能导致 变量引用错误、内存泄漏、性能问题,这些问题统称为 闭包陷阱(Closure Trap) 。
1. 闭包陷阱的成因
(1) 变量共享(作用域链问题)
📌 成因:
在 for
循环中使用 var
时,var
变量在 函数作用域 内是共享的,因此闭包函数访问的是最终的 var
变量值,而不是循环时的 i
值。
🚨 问题示例:
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 输出 3, 3, 3
}, 1000);
}
所有 setTimeout
共享同一个 i
,在 for
循环结束时 i = 3
,所以 console.log(i)
输出 3, 3, 3
。
✅ 解法:
- 使用
let
(块级作用域)
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2
}, 1000);
}
let
使 i
在每次循环时都有自己的作用域,不会共享。
- 使用 IIFE(立即执行函数表达式)
for (var i = 0; i < 3; i++) {
(function (i) {
setTimeout(() => {
console.log(i); // 0, 1, 2
}, 1000);
})(i);
}
IIFE 创建了一个独立作用域,每次循环 i
值都会被“冻结”在该作用域中。
(2) 闭包导致的内存泄漏
📌 成因:
闭包函数持有对 大对象或 DOM 元素 的引用,导致变量不会被垃圾回收(GC)。
🚨 问题示例:
function createClosure() {
let largeData = new Array(1000000); // 大量数据
return function () {
console.log(largeData.length);
};
}
const closureFn = createClosure(); // `largeData` 仍然存活
largeData
被 closureFn
持有,无法被垃圾回收,造成 内存泄漏。
✅ 解法:
- 在不需要时手动释放引用
function createClosure() {
let largeData = new Array(1000000);
return function () {
console.log(largeData.length);
largeData = null; // 释放引用
};
}
(3) 事件监听中的闭包陷阱
📌 成因:
闭包函数引用了 DOM 元素,但未及时移除事件监听,导致 DOM 对象无法被回收。
🚨 问题示例:
function attachEvent() {
const element = document.getElementById("btn");
element.addEventListener("click", function () {
console.log(element.id); // `element` 仍然被引用
});
}
attachEvent();
element
被闭包引用,即使 btn
被移除,内存仍然无法回收。
✅ 解法:
- 解绑事件监听
function attachEvent() {
const element = document.getElementById("btn");
const handler = function () {
console.log(element.id);
};
element.addEventListener("click", handler);
element.removeEventListener("click", handler); // 解绑监听
}
- 使用
this
代替闭包
function attachEvent() {
const element = document.getElementById("btn");
element.addEventListener("click", function () {
console.log(this.id); // `this` 指向 `element`
});
}
这样不会在闭包内存储 element
,减少引用链。
(4) 计时器 & 异步回调
📌 成因:
setTimeout
和 setInterval
可能导致闭包持有变量的引用,导致 变量长时间存活。
🚨 问题示例:
function startTimer() {
let data = "重要数据";
setTimeout(() => {
console.log(data); // `data` 长时间存活
}, 5000);
}
startTimer();
✅ 解法:
- 手动释放数据
function startTimer() {
let data = "重要数据";
let timer = setTimeout(() => {
console.log(data);
data = null; // 释放数据
}, 5000);
}
startTimer();
- 清除计时器
let timer = setTimeout(() => console.log("执行任务"), 5000);
clearTimeout(timer); // 及时清理
2. 闭包陷阱的总结
闭包陷阱 | 成因 | 解决方案 |
---|---|---|
变量共享 | var 作用域导致变量被共享 | 使用 let 或 IIFE |
内存泄漏 | 变量被闭包长时间引用 | 置 null 释放引用 |
事件监听泄漏 | DOM 元素被闭包引用 | 解绑事件监听,使用 this |
计时器/回调 | 定时器 & 回调函数持有变量 | 手动释放数据,清除计时器 |
64.在react中,如何实现渲染控制?
React 中的渲染控制(Render Control)
在 React 中,渲染控制指的是 有选择性地渲染组件,以提高性能或优化用户体验。可以使用 条件渲染、列表渲染、优化渲染性能 等多种方式来实现。
1. 条件渲染(Conditional Rendering)
1.1 三元运算符
function Greeting({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? <h1>欢迎回来!</h1> : <h1>请登录</h1>}
</div>
);
}
1.2 &&
短路运算
function ShowMessage({ hasMessage }) {
return <div>{hasMessage && <p>您有新消息</p>}</div>;
}
⚠️ 注意:当
hasMessage
为false
或0
时,不会渲染<p>
。
1.3 if-else
语句
function Greeting({ isLoggedIn }) {
if (isLoggedIn) {
return <h1>欢迎回来!</h1>;
} else {
return <h1>请登录</h1>;
}
}
2. 列表渲染
使用 map()
动态生成 JSX 元素
function UserList({ users }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
⚠️ 注意:必须给每个列表项加上唯一的 key
,否则 React 可能会导致渲染错误或性能下降。
3. 避免不必要的渲染
3.1 React.memo()
(浅比较 props)
适用于:纯函数组件,当 props
没有变化时跳过重新渲染。
const MemoizedComponent = React.memo(function MyComponent({ name }) {
console.log("渲染了!"); // 仅当 `name` 变化时触发
return <p>你好,{name}!</p>;
});
3.2 shouldComponentUpdate()
(类组件)
适用于:类组件,可手动控制组件是否重新渲染。
class MyComponent extends React.PureComponent {
shouldComponentUpdate(nextProps) {
return nextProps.value !== this.props.value; // 仅当 props 变化时更新
}
render() {
return <div>{this.props.value}</div>;
}
}
PureComponent
继承shouldComponentUpdate()
,默认对props
进行浅比较。
3.3 useMemo()
& useCallback()
(函数组件)
useMemo()
:缓存计算结果,避免重复计算useCallback()
:缓存函数引用,避免组件重新渲染时创建新函数
const memoizedValue = useMemo(() => computeExpensiveValue(data), [data]);
const memoizedCallback = useCallback(() => doSomething(), []);
4. 组件卸载(移除不必要的组件)
4.1 通过 state
控制组件的挂载
function App() {
const [show, setShow] = useState(true);
return (
<div>
<button onClick={() => setShow(!show)}>切换组件</button>
{show && <MyComponent />}
</div>
);
}
⚠️ 注意:当组件被卸载时,会触发 useEffect
的清理函数(return () => {...}
)。
5. 使用 Suspense
和 lazy
实现懒加载
React 允许对 组件进行懒加载,提升首屏渲染性能。
const LazyComponent = React.lazy(() => import("./LazyComponent"));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<LazyComponent />
</Suspense>
);
}
Suspense
提供fallback
组件,在组件加载时展示占位内容。
总结
渲染控制方法 | 用途 | 示例 |
---|---|---|
条件渲染 | 仅在满足条件时渲染 | if-else 、&& 、三元运算符 |
列表渲染 | 动态渲染数组数据 | map() |
避免重复渲染 | 仅当数据变更时重新渲染 | React.memo() 、useMemo() 、shouldComponentUpdate() |
控制组件挂载 | 组件按需显示/隐藏 | useState 控制组件是否渲染 |
懒加载 | 按需加载组件,提高性能 | React.lazy() + Suspense |
65.如何实现一个redux?
实现一个 Redux
Redux 是一个 状态管理库,核心思想是 单一数据源(Store)、纯函数(Reducer)和不可变状态。下面我们手写一个简化版 Redux,并解析其实现原理。
1. Redux 的核心概念
store
(仓库) :存储应用的全局状态state
(状态) :存储的数据action
(动作) :描述对state
进行的操作(必须是一个普通对象)reducer
(纯函数) :根据action
计算新的state
dispatch
(派发) :触发action
,通知reducer
更新state
subscribe
(订阅) :监听state
变化,触发回调
2. 手写 Redux
📌 核心代码
function createStore(reducer) {
let state; // 存储状态
let listeners = []; // 存储订阅者
// 获取当前状态
function getState() {
return state;
}
// 触发 action,更新 state
function dispatch(action) {
state = reducer(state, action); // 计算新 state
listeners.forEach(listener => listener()); // 通知所有订阅者
}
// 订阅 state 变化
function subscribe(listener) {
listeners.push(listener); // 添加订阅者
return () => {
listeners = listeners.filter(l => l !== listener); // 取消订阅
};
}
// 初始化 store,触发一次 reducer,获取初始值
dispatch({ type: "@@INIT" });
return { getState, dispatch, subscribe };
}
3. 使用 createStore
(1) 定义 reducer
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
default:
return state;
}
}
(2) 创建 store
const store = createStore(counterReducer);
(3) 订阅状态变化
store.subscribe(() => {
console.log("State changed:", store.getState());
});
(4) 触发 action
更新 state
store.dispatch({ type: "INCREMENT" }); // State changed: { count: 1 }
store.dispatch({ type: "INCREMENT" }); // State changed: { count: 2 }
store.dispatch({ type: "DECREMENT" }); // State changed: { count: 1 }
4. 实现 Redux 中间件机制(类似 applyMiddleware
)
Middleware(中间件)用于增强 dispatch
,例如:日志、异步操作等。
function applyMiddleware(store, middlewares) {
let dispatch = store.dispatch;
middlewares.forEach(middleware => {
dispatch = middleware(store)(dispatch);
});
return { ...store, dispatch };
}
// 示例:日志中间件
const loggerMiddleware = store => next => action => {
console.log("Action:", action);
let result = next(action);
console.log("New State:", store.getState());
return result;
};
// 创建 store 并应用中间件
const storeWithMiddleware = applyMiddleware(store, [loggerMiddleware]);
storeWithMiddleware.dispatch({ type: "INCREMENT" });
5. Redux 的异步处理(Thunk 实现)
Redux 本身是同步的,要处理异步操作(如 API 请求),可以使用 redux-thunk
。
实现 thunkMiddleware
const thunkMiddleware = store => next => action => {
if (typeof action === "function") {
return action(store.dispatch, store.getState);
}
return next(action);
};
// 使用 thunkMiddleware
const storeWithThunk = applyMiddleware(store, [thunkMiddleware]);
// 异步 action
const asyncIncrement = () => (dispatch) => {
setTimeout(() => {
dispatch({ type: "INCREMENT" });
}, 1000);
};
// 触发异步 action
storeWithThunk.dispatch(asyncIncrement());
6. 总结
功能 | 核心 API | 作用 |
---|---|---|
创建 Store | createStore(reducer) | 创建全局状态存储 |
获取状态 | getState() | 获取当前 state |
更新状态 | dispatch(action) | 触发 reducer ,计算新 state |
订阅变化 | subscribe(listener) | 监听状态变化 |
支持中间件 | applyMiddleware() | 增强 dispatch ,添加日志、异步支持等 |
🔹 通过 createStore
创建 Redux,结合 middleware
处理异步,最终实现一个完整的 Redux! 🚀
66.useRoutes的原理是什么?
白话回答:useRoutes是一个hook,原理是将声明式路由配置(json的路由表)转化为动态的路由匹配与组件渲染,通过 React Router 的上下文和内部匹配算法,实现 URL 到组件的映射。其中使用到matchRoutes和
useLocation将从浏览器url中获取到的路由和json中进行match匹配,并将匹配到的路由对应的组件进行映射渲染
匹配结果通过 renderMatches(matches)
进行递归渲染:
- 从最外层父路由开始,逐层渲染匹配的
element
。 - 递归传递子路由 通过
Outlet
组件完成嵌套渲染。
useRoutes
是 React Router v6 中引入的一个核心钩子,用于在函数式组件中声明式地定义路由配置。它的原理可以拆解为以下几个关键点:
1. 基于配置的路由匹配
useRoutes
的核心是将路由配置转化为 React Router 内部的路由匹配逻辑。它接受一个路由配置对象数组(类似 Route
组件的结构化数据),例如:
const routes = [
{ path: '/', element: <Home /> },
{ path: 'users', element: <Users />, children: [
{ path: ':id', element: <UserProfile /> }
]}
];
useRoutes(routes)
会解析当前 URL,按配置的路径(path
)和嵌套关系(children
)进行匹配,最终确定需要渲染的组件。
2. 依赖 React Router 的上下文
useRoutes
必须工作在 React Router 的上下文(<BrowserRouter>
或 <HashRouter>
)中,因为它依赖以下机制:
- 路由状态:通过
useLocation()
获取当前 URL 的location
对象。 - 匹配算法:使用
matchPath()
等内部方法,将location.pathname
与配置的path
进行模式匹配(支持动态参数、通配符等)。 - 嵌套路由:通过递归处理
children
配置,构建嵌套的路由层级结构。
3. 生成 React 元素
useRoutes
的返回值是一个 React 元素(或 null
),代表当前 URL 匹配到的组件树。例如:
- 当 URL 为
/users/123
时,useRoutes
会依次匹配users
和:id
,最终返回<Users><UserProfile /></Users>
的嵌套结构。 - 若未匹配到路由,返回
null
(通常需要配置path: '*'
作为兜底路由)。
4. 动态参数与状态传递
- 参数解析:动态路径(如
:id
)的参数会被提取,并通过useParams()
传递给组件。 - 状态传递:
useRoutes
自动处理路由状态(如location.state
),确保子组件能通过useLocation()
获取。
5. 性能优化
useRoutes
内部通过 Memoization 优化路由匹配过程,避免不必要的重新渲染。只有当 location
或路由配置变化时,才会重新计算匹配结果。
源码简析(简化版)
useRoutes
的核心逻辑类似于以下伪代码:
function useRoutes(routes) {
const location = useLocation();
// 递归匹配路由,生成匹配结果
const matches = matchRoutes(routes, location);
// 将匹配结果转换为 React 元素(嵌套结构)
return renderMatches(matches);
}
其中:
matchRoutes()
:遍历路由配置,找到与当前location
匹配的路由。renderMatches()
:将匹配的路由按嵌套关系渲染为 React 元素。
总结
可回答 useRoutes的原理应该是将路由配置对象转换为React元素,利用React Router的上下文和路由匹配机制,动态地根据当前URL决定渲染哪个组件。内部可能使用React的context API来传递路由状态,并通过匹配算法找到对应的路由配置,然后生成相应的组件结构。
useRoutes
的原理是将声明式路由配置转化为动态的路由匹配与组件渲染,通过 React Router 的上下文和内部匹配算法,实现 URL 到组件的映射。它简化了路由定义,同时保持了与 React 组件模型的深度集成。
67.react18中,为什么选择messagechannel来让出执行权?
React在更新时需要进行任务调度,确保高优先级的任务优先执行,同时不阻塞主线程,保持应用的响应性。 在React 18之前,可能使用的是requestIdleCallback或者setTimeout来进行任务调度。但requestIdleCallback的兼容性和触发时机可能不够可靠,而setTimeout有最小延迟时间4ms,可能不够及时。所以React需要一种更高效的方式来调度任务。 MessageChannel属于宏任务,它的回调会在当前事件循环的末尾执行,这样可以让出主线程,让浏览器有机会处理用户输入等紧急任务。相比setImmediate,MessageChannel的兼容性更好,而相比requestAnimationFrame,它更适合处理非渲染相关的任务。 另外,React可能希望更精细地控制任务的调度,使用MessageChannel可以更主动地安排任务的执行时机,避免被其他宏任务阻塞。同时,结合Scheduler包,React能够实现时间切片和任务的中断恢复,提升并发模式下的性能。
在 React 18 中,选择 MessageChannel 作为调度器(Scheduler)中让出主线程(Yield to Main Thread)的核心机制,是为了实现更精细的时间切片(Time Slicing)和并发渲染(Concurrent Rendering) ,其背后的设计原理可以拆解如下:
1. 为什么需要让出执行权?
React 的并发模式需要将长任务拆分为可中断的微任务块,避免阻塞主线程导致页面卡顿(如点击事件、动画无法及时响应)。传统同步渲染模式下,组件树的渲染是连续不可中断的,而并发模式下需要主动让出主线程,让浏览器有机会处理更高优先级的任务。
2. MessageChannel 的核心优势
a. 宏任务(Macro Task)的特性
MessageChannel 的回调属于宏任务,其执行时机在当前事件循环的末尾,与微任务(如 Promise
)相比:
- 更可控的让出时机:宏任务会在浏览器完成当前任务(如渲染、事件处理)后执行,确保主线程有喘息机会。
- 避免微任务的饥饿问题:微任务队列会一次性全部执行,若递归触发微任务可能导致主线程长期被占用。
b. 对比其他宏任务方案
setTimeout(fn, 0)
:存在最小延迟(~4ms) ,且可能被浏览器节流(如后台标签页)。requestAnimationFrame
:与渲染帧强绑定,不适合非渲染相关的任务调度。setImmediate
:仅 IE/Node.js 支持,兼容性差。MessageChannel
:无延迟、无兼容性问题(主流浏览器均支持)、触发时机精准。
3. React 调度器的实现逻辑
React 的调度器(Scheduler)使用 MessageChannel 实现任务队列的调度,核心流程如下:
- 任务入队:将 React 的渲染任务(如组件更新)加入调度队列。
- 触发宏任务:通过
MessageChannel.port.postMessage
触发一个宏任务。 - 执行任务块:在宏任务回调中,执行一段任务(如 5ms 的时间切片)。
- 让出主线程:若任务超时,将剩余任务重新入队,等待下一个宏任务继续执行。
// 简化的调度逻辑
const channel = new MessageChannel();
channel.port2.onmessage = () => {
// 执行任务块,若超时则让出主线程
if (needsYield()) {
channel.port1.postMessage(null); // 触发下一个宏任务
return;
}
processNextTask();
};
4. 与浏览器事件循环的协同
通过 MessageChannel,React 的调度器能够:
- 精准控制任务中断点:在每段任务执行后检查是否需要让出主线程。
- 避免阻塞用户交互:即使渲染未完成,点击、滚动等事件也能在下一个事件循环中及时响应。
- 兼容时间切片:实现类似“可中断渲染”的效果,为并发模式(Concurrent Mode)提供基础。
5. 性能与兼容性权衡
- 性能:MessageChannel 的触发速度比
setTimeout
更快,且不受浏览器节流影响。 - 兼容性:MessageChannel 在 IE11 及现代浏览器中均可使用,而
setImmediate
或requestIdleCallback
存在兼容性问题。
6. 源码中的关键代码
在 React 源码的 Scheduler
模块中,以下代码体现了 MessageChannel 的使用:
// React Scheduler 中的调度逻辑
const performWorkUntilDeadline = () => {
// 执行任务...
if (hasMoreWork) {
// 通过 MessageChannel 调度下一个任务块
schedulePerformWorkUntilDeadline();
}
};
const channel = new MessageChannel();
channel.port1.onmessage = performWorkUntilDeadline;
// 触发任务调度
schedulePerformWorkUntilDeadline = () => {
channel.port2.postMessage(null);
};
总结
React 18 选择 MessageChannel 作为让出执行权的机制,是因为它结合了宏任务的非阻塞特性、精准的触发时机和良好的兼容性,使得 React 能够在并发模式下实现高效的时间切片,平衡渲染性能与用户体验。这是 React 调度器设计中的关键决策,为复杂应用的流畅运行奠定了基础。
微任务和宏任务的区别,为什么选择宏任务而不是微任务。微任务会在当前任务结束后立即执行,可能导致长时间占用主线程,而宏任务可以让出主线程,让浏览器有机会处理其他事件。
React 18选择MessageChannel可能是因为它作为宏任务能够有效让出主线程,提供更可靠的调度时机,同时兼容性较好,适合实现时间切片和并发模式的需求。
5. MessageChannel
vs. setTimeout
vs. requestIdleCallback
API | 执行时间 | 适用场景 | React 适配 |
---|---|---|---|
setTimeout(fn, 0) | 4ms 延迟(最小值) | 低优先级任务 | ❌ 太慢 |
requestAnimationFrame | 下一帧执行 | 动画、布局计算 | ❌ 不能保证及时执行 |
requestIdleCallback | 浏览器空闲时执行 | 后台任务 | ❌ 不适用于高优先级任务 |
MessageChannel | 微任务队列,尽快执行 | 任务调度、React 并发渲染 | ✅ 最优选择 |
69.RN和react在跨端架构上有什么区别?
React 和 React Native(RN)在跨端架构上的主要区别可以从以下几个方面来分析:
1. 渲染机制
React(Web 端)
- React 主要运行在浏览器,通过 Virtual DOM (VDOM) 进行高效的 UI 更新。
- 使用
ReactDOM.render()
渲染组件,最终通过 HTML + CSS 在浏览器中显示。 - 依赖 CSS、HTML、JavaScript,主要针对 Web 平台。
示例
function App() {
return <div>Hello, Web!</div>;
}
最终会被转换成:
<div>Hello, Web!</div>
React Native(移动端)
- 不使用 DOM,而是使用 Native 组件(如 iOS 的
UIView
、Android 的View
)。 - 使用 React Native Bridge 将 JavaScript 调用转换为原生渲染指令。
- 组件如
<View>
、<Text>
会在 iOS 和 Android 上映射到原生组件。
示例
function App() {
return <View><Text>Hello, Mobile!</Text></View>;
}
在 iOS 上会被转换为:
UIView -> UILabel("Hello, Mobile!")
在 Android 上会被转换为:
View -> TextView("Hello, Mobile!")
✅ 核心区别:
- React 依赖 DOM,使用 HTML/CSS。
- RN 直接调用原生组件,不使用 HTML,而是通过 桥接(Bridge) 与原生交互。
2. 样式与布局
React (Web)
- 使用 CSS 进行样式布局。
- 采用 Flexbox(但实现和 RN 有些不同)。
- 依赖
px、rem、em、vh、vw
等单位。
.container {
display: flex;
justify-content: center;
align-items: center;
}
React Native
- 没有 CSS,只能用
StyleSheet
定义样式。 - 所有样式基于 Flexbox(不支持
grid
、float
)。 - 单位默认为 dp(独立像素) ,不使用
px
。
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
}
});
✅ 核心区别:
- React 用 CSS,支持多种布局方式。
- RN 只支持 Flexbox,并使用
StyleSheet
定义样式。
3. 事件处理
React (Web)
- 依赖 DOM 事件,比如
onClick
、onMouseMove
。 - 事件是合成事件(Synthetic Event) ,在
document
级别进行事件代理。
<button onClick={() => alert('Clicked!')}>Click Me</button>
React Native
- 没有 DOM,所以事件是直接绑定在原生组件上。
- 主要事件有
onPress
(代替onClick
)、onTouchStart
、onScroll
等。
<TouchableOpacity onPress={() => alert('Clicked!')}>
<Text>Click Me</Text>
</TouchableOpacity>
✅ 核心区别:
- React 使用 DOM 事件,React Native 直接绑定到原生事件。
- RN 没有
onClick
,而是onPress
。
4. 跨端运行方式
React(Web 端)
- 只在浏览器中运行,不需要
Bridge
。 - 使用 ReactDOM 渲染,最终依赖 HTML + CSS + JavaScript。
React Native(移动端)
-
需要一个 JavaScript 线程 + 原生线程。
-
通过 React Native Bridge 进行通信:
- JS 线程 运行 React 代码。
- Bridge 传输数据(JS <-> Native)。
- Native 线程 负责渲染 UI。
✅ 核心区别:
- React 直接在浏览器运行,React Native 需要 Bridge 连接 JS 和 Native 代码。
5. 跨端兼容
React(Web 跨端)
- 主要依赖 React Native Web,将 RN 组件转换为 HTML + CSS 组件。
- Web 端用
react-router-dom
进行路由管理。
import { BrowserRouter as Router, Route } from 'react-router-dom';
React Native(移动端跨端)
- 主要依赖 React Navigation,提供
StackNavigator
和TabNavigator
。
import { createStackNavigator } from '@react-navigation/stack';
const Stack = createStackNavigator();
✅ 核心区别:
- React 依赖
react-router-dom
进行 Web 路由。 - RN 依赖
react-navigation
进行移动端导航。
6. 代码复用性
React
- 主要针对 Web,跨端需要借助 React Native Web 或 Electron。
React Native
- 通过
Platform
进行 按平台渲染:
import { Platform } from 'react-native';
const styles = StyleSheet.create({
container: {
padding: Platform.OS === 'ios' ? 20 : 10
}
});
- 可以使用
react-native-web
让 RN 代码在 Web 端运行。
✅ 核心区别:
- React 不能直接用于移动端,React Native 代码可以通过
react-native-web
兼容 Web。
总结
维度 | React(Web) | React Native(移动端) |
---|---|---|
渲染方式 | DOM + Virtual DOM | 原生组件 + Bridge |
样式 | CSS | StyleSheet (无 CSS) |
事件 | onClick 、onMouseMove | onPress 、onTouchStart |
运行环境 | 浏览器 | iOS/Android |
跨端能力 | 需要 React Native Web | 直接支持 iOS 和 Android |
代码复用 | 主要针对 Web | 可通过 Platform 区分平台 |
✅ 核心结论
- React 面向 Web,依赖 HTML + CSS + JS,而 RN 面向移动端,依赖原生组件。
- React Native 使用
MessageQueue
进行 JS-Native 通信,而 React 直接运行在浏览器。 - React 代码可通过
react-native-web
兼容 RN,但 RN 代码比 Web 更通用。
🚀 面试官可能会追问:
-
RN 如何优化 Bridge 速度?
- 通过 Fabric(新架构)+ TurboModule 提高原生通信效率。
-
RN 的
Hermes
引擎是什么?Hermes
是一个优化的 JS 引擎,提高 RN 启动速度。
-
Web 和 RN 组件能共享吗?
- 可以,使用
react-native-web
实现跨端共享。
- 可以,使用
总结一句话:React 适用于 Web,React Native 适用于移动端,RN 通过 Bridge
实现 JS 和原生交互,二者在跨端架构上有本质区别! 🚀
70.常见的redux中间件有哪些
Redux 常见的中间件主要用于 异步处理、日志记录、调试工具、错误处理 等,
- redux-thunk(异步 Action 处理)
- redux-saga(更强大的异步流控制)
- redux-logger(日志中间件)
- redux-devtools-extension(开发者工具)
- redux-promise(Promise 处理)
作用:基于 ES6 Generator 实现的异步流管理,比redux-thunk
更适合 复杂的异步逻辑(如监听多个 action、并发请求、取消任务等)。