React面试葵花宝典之二
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带来革命性性能提升与扩展能力。
- 核心价值:将同步渲染转化为异步可调度任务,使复杂应用保持流畅交互。