react面试核心知识总结
一、React Hook
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
1.要解决什么问题?
- 可以在函数组件中使用状态、模拟组件的生命周期
- 可以复用组件状态及相关的变更逻辑。
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState
和 useEffect
调用之间保持 hook 状态的正确。
这样做的原因是因为: React 靠的是 Hook 调用的顺序来确定state应该和哪个useState对应,因此Hook 的调用顺序在多次渲染之间应保持一致。
3.底层原理
React Hook 的底层实现主要基于以下几个关键原理:
- 单向链表存储 Hook 状态
- 按顺序调用 Hook
- 闭包的使用
- 在 Fiber 架构下管理 Hook 状态
1. 单向链表存储 Hook 状态
React 使用了一个单向链表来存储每个组件中所有的 Hook。每个函数组件的实例有一个 memoizedState
属性,指向该组件的第一个 Hook。当组件渲染时,React 会从第一个 Hook 开始,一直到最后一个 Hook。
每个组件实例对应一个Fiber存储节点,Fiber 节点记录了组件的状态信息,包括 Hooks 链表。每当组件渲染时,React 会将这个 Fiber 节点作为上下文,用来关联该组件的 Hook 数据。
链表的每个节点存储了一个 Hook 对象,通常包含以下信息:
memoizedState
:存储最新的状态值next
:指向下一个 Hook 的引用queue
:需要更新的队列,通常用于useState
等可更新状态的 Hook
2.Hook 调用索引
React 依赖每个组件中 Hook 的调用顺序来记录状态。在初始化或更新时,每调用一个 Hook,React 都会增加一个索引值,确保 Hook 调用的顺序与链表存储的顺序一致。
React 每次渲染组件时,都会按照顺序遍历 Hook 链表,从而保证每个 Hook 调用位置不变。React 的 currentHook
指针指向当前执行的 Hook,并依次遍历更新每个 Hook 的状态或副作用。这个顺序和链表管理使 React 能够在每次渲染时正确地复用或更新 Hook。
3. 闭包的使用
Hook 使用闭包来持久化每次渲染中的状态。当你在一个渲染周期中使用 setState
更新状态时,React 会创建一个更新对象并将其推入当前 Hook 的 queue
队列中。组件下一次渲染时,React 会遍历更新队列,将所有更新依次应用到 memoizedState
中,以生成最新的状态值。
这种闭包的机制有助于保持每个 Hook 调用的上下文,使得每次渲染可以获取到它需要的状态值或副作用依赖。
4.React Fiber 和调度
React Hook 的实现依赖于 React Fiber 架构,Fiber 架构为每个组件分配了一个独立的 Fiber 节点,支持中断更新和优先级调度。
- Fiber 的作用:
- 每个 Fiber 节点对应一个组件实例,并存储了该组件的 Hook 状态链表。
- Fiber 架构允许 React 中断、恢复和调度渲染,优化了 Hook 调用的性能和一致性。
- 通过 Fiber 节点,React 可以管理每个组件的更新流程,并确保
useEffect
、useLayoutEffect
等在适当的时机执行。
1. 父组件传递给子组件的属性值是对象、函数时,不要使用内联模式,不然父组件每次render,即使属性值没发生变化,都会导致子组件渲染。
2. 函数组件的优化
- 组件内部如果涉及到复杂耗时的数据计算,可以使用useMemo来缓存计算结果
- 若内部有子组件,并且子组件需要接收函数做为属性值的话,此时父组件中的回调函数需要使用useCallback函数,不然会导致子组件的渲染优化判断机制失败。
- 使用React.memo(fnComponent)这个高阶组件来做记忆函数,当props不变时,直接复用上一次的渲染结果。对props的比较是浅比较,可以自己实现比较逻辑,文档说明
3. class组件非必要情况下,不要使用state;可以在shouldComponentUpdate里判断props或state是否发生了改变,或者继承自PureComponent
const Foo = React.lazy(() => import('../componets/Foo));
React.lazy不能单独使用,需要配合React.suspense,suspence是用来包裹异步组件,添加loading效果等。
<React.Suspense fallback={<div>loading...</div>}> <Foo/> </React.Suspense>
key
应该在兄弟节点中是唯一的。React 通过 key
来匹配新旧虚拟 DOM 树中的元素节点。具有相同 key
的节点被认为是相同的元素,因此可以复用或更新;而不同的 key
表示不同的元素,React 会卸载旧元素,添加新元素。key
的顺序和内容,以最小化 DOM 操作。若采用常规的树比较方法,时间复杂度是O(n^3),React基于一些假设,将时间复杂度变为O(n),相应的策略有:- 对比key
key
可以准确地发现新旧集合中的节点是否只存在位置不同的情况,若是,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置即可,无需进行节点删除和插入。diff
过程如下:
使用变量(下面假设为lastIndex)来记录在新集合访问过的节点中,并跟其在老dom树中的位置做比较,如果当前位置>旧的位置,则需要移动的节点,就应该移动到该位置,初始值为0。
循环新集合
- 节点B: lastIndex=0, oldIndex=1; lastIndex < oldindex,因此B节点不动,lastIndex= Math.max(oldIndex, lastIndex) = 1
- 节点A:lastIndex=1, oldIndex=0;lastIndex > oldIndex,因此A节点进行移动操作,lastIndex= Math.max(oldIndex, lastIndex) = 1
- 节点D:lastIndex=1, oldIndex=3;lastIndex < oldindex,因此D节点不动,lastIndex= Math.max(oldIndex, lastIndex) = 3
- 节点C:lastIndex=3, oldIndex=2;lastIndex > oldindex ,因此C节点进行移动操作。循环结束
- 对比元素类型
component D
换成了component G
后,即使两者的结构非常类似,也会将D
删除再重新创建G
节点间只存在添加、删除操作。如下图所示,当想把A移动到D下面时,先删除A,然后再到D节点中创建A
五、渲染原理
六、fiber架构
出现背景
React16之前组件更新是一个递归的过程,深度优先遍历整个组件树,所有工作必须在单一的渲染周期中完成,这可能导致卡顿、丢帧等性能问题,React 16 引入的一种全新的协调(reconciliation)引擎,它使得 React 可以更好地中断和恢复渲染任务,帮助 React 在 UI 更新时更流畅地响应用户操作。Fiber 提供了一种更细粒度的工作分割方式,使得 React 可以在必要时暂停渲染任务,去执行更高优先级的任务,并在空闲时继续未完成的工作。
Fiber 的关键思想
Fiber 架构的核心思想是将渲染工作分割成多个可中断的任务单元,并且通过以下两个关键概念实现优先级控制和调度:
-
可中断的任务:在传统的同步渲染中,React 会一次性完成整个渲染过程,导致长时间的主线程占用。而 Fiber 的设计允许渲染任务被分割成一个个小单元,这样当有更高优先级的任务(如用户输入)需要处理时,React 可以暂停当前任务,先去执行高优先级任务,然后在合适的时机恢复之前未完成的渲染任务。
-
优先级和任务调度:Fiber 引入了优先级的概念,为每个更新任务设定优先级。例如,用户输入等高优先级任务会优先被调度执行,而不影响低优先级的任务(如数据加载)。Fiber 能够根据任务的优先级合理安排渲染顺序,以确保高优先级任务更快得到响应。
Fiber 的核心机制
为了实现这种细粒度的任务控制,Fiber 架构引入了以下几个关键机制:
(1)Fiber 节点
- 每个 React 组件实例对应一个 Fiber 节点。Fiber 节点是一个 JavaScript 对象,用于描述组件的类型、状态、子节点等信息。
- 在组件更新过程中,每个组件对应的 Fiber 节点可以存储其状态和更新内容,以便在重新渲染时可以直接访问并更新。
(2)双缓存树结构(Current 和 WorkInProgress)
- React Fiber 使用双缓存树来管理渲染工作。它分为
current tree
和workInProgress tree
,即当前树和工作树。 current tree
表示当前屏幕上展示的 UI 状态,而workInProgress tree
是新的更新任务的工作空间。- 在渲染的过程中,React 会在
workInProgress tree
上构建新的 UI 状态,更新完成后会将其替换成current tree
,从而完成一次更新。
(3)任务的中断与恢复
- Fiber 使用 JavaScript 的
requestIdleCallback
或scheduler
来实现任务中断,使得更新任务可以分批执行。 - 这种机制确保在渲染任务被打断后,React 可以从中断的地方恢复,继续执行未完成的任务。
(4)任务优先级调度
- Fiber 支持多种优先级策略,如同步优先级(例如用户点击)、异步优先级(例如数据加载)和离线优先级(例如后台数据更新)。
- 每个任务都会被分配一个优先级,React 会根据优先级来调度任务,确保高优先级任务在低优先级任务之前完成。
七、react如何声明渲染优先级
八、React的钩子函数依赖项注意事项
使用浅比较判断各个依赖项是否发生了变化,对于基本类型(如 string
、number
、boolean
),只比较值本身;对于引用类型(如对象、数组、函数),只比较引用是否相同(即内存地址),而不比较内容是否发生了变化。
注意的点:
- 所有依赖项都必须声明,如果在依赖项数组中漏掉了某些值,可能会导致闭包问题、结果还是上次的,从而引发意料之外的行为
- 使用常量依赖没有意义,因为常量不会改变
- 如果依赖项数组中有复杂的对象或函数,可能导致不必要的重计算或重新创建(应该使用useRef来存储复杂变量)
const obj = { key: 'value' }; const memoizedValue = useMemo(() => doSomething(obj), [obj]); // 组件函数每次被执行时,obj每次都是新引用,导致重计算 // 改进 const stableObj = useRef({ key: 'value' }); const memoizedValue = useMemo(() => doSomething(stableObj.current), []);
eslint-plugin-react-hooks
插件。它可以帮助你检查依赖项是否完整和正确
九、React17、18有哪些新特性
react17没功能上的更新,主要为后续版本升级做了一些铺垫.
- jsx中不再需要import React from 'react'; 为了实现该特性,需要支持新转换的工具链(如 Babel 7.9+ 或 TypeScript 4.1+)
- React 17 优化了开发环境中的错误信息,使堆栈追踪更清晰、更有上下文
- React 事件系统被重写,避免事件绑定到
document
,而是直接绑定到 React 渲染树的根节点。 - 允许在同一应用中同时使用多个 React 版本
react18的主要在并发渲染方面做了一些更新:
- Concurrent Rendering(并发渲染): React 18 默认启用了并发渲染,它不会阻塞主线程,从而使应用更具响应性。
- 自动批处理(Automatic Batching): React 18 会将多个状态更新自动批处理,从而减少渲染次数
function handleClick() { setCount((c) => c + 1); setFlag((f) => !f); // React 18 中只会触发一次渲染,而不是两次。 }
-
开发者声明低优先级任务(
useTransition
)、延迟更新值(useDeferredValue
) - React 18 引入了新的根 API(
createRoot
和hydrateRoot
):createRoot
是为了支持并发渲染,hydrateRoot
则用于支持 SSR 的改进 - ssr的改进:1. 增加了流式渲染,支持以流的方式将 HTML 发送到客户端,提升首屏加载性能 2.
<Suspense>
支持服务端渲染(SSR)