React面试题总结
React面试题总结
React因为版本迭代,很难找到比较新的,比较全的一些知识点和总结,这篇文章是我搜集了很多文章总结下来的比较全面的react相关题目和知识点,并在不断更新中~~
1. 对 React 的理解、特性
React 是靠数据驱动视图改变的一种框架,它的核心驱动方法就是用其提供的 setState 方法设置 state 中的数据从而驱动存放在内存中的虚拟 DOM 树的更新
更新方法就是通过 React 的 Diff 算法比较旧虚拟 DOM 树和新虚拟 DOM 树之间的 Change ,然后批处理这些改变。
遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效
使用虚拟 DOM 来有效地操作 DOM,遵循从高阶组件到低阶组件的单向数据流
- 声明式编程:React 采用声明式编程范式,允许开发者描述 UI 应该呈现的状态,而非具体的操作步骤。这种抽象简化了开发过程,并且有利于 React 在内部执行高效的 DOM 更新。
- 虚拟 DOM:React 引入了虚拟 DOM 的概念,即在内存中维护一个与实际 DOM 相对应的数据结构。每当组件的状态变化时,React 首先更新虚拟 DOM,然后通过高效的 Diff 算法找出最小化的 DOM 更新操作,再将这些变化反映到实际 DOM 上,从而极大提升了渲染性能。
- 组件化:React 强调组件化开发,鼓励将 UI 拆分成可复用的独立单元。组件具有明确的输入(Props)和内部状态(State),使得代码易于组织和测试。
- 单向数据流与可预测性:React 推崇单向数据流的设计理念,数据从父组件流向子组件,通过 props 传递,而子组件通过回调函数通知父组件状态变化,这种设计有助于理解和调试应用状态。
- 函数组件与 Hooks:随着 React 16.8 版本引入的 Hooks,函数组件获得了状态管理和生命周期功能,使得无需类就能写出完整功能的组件,进一步简化了代码结构,提高了可读性和可复用性。
- React Fiber:React 16 引入的调度算法改进,提供了更细粒度的任务划分与优先级调度,增强了应用在复杂场景下的流畅性。
- 兼容性与扩展性:React 生态丰富,支持服务端渲染(SSR)、静态站点生成(SSG)、移动应用开发(React Native)等多种应用场景,具备良好的兼容性和扩展能力。
2. React 生命周期
React 组件的生命周期在过去的不同版本中有所调整,以下是 React 类组件的经典生命周期钩子(React v16 及之前版本)和现代函数组件使用的 Hook 形式生命周期方法的对比表:
类组件生命周期方法(经典生命周期):
阶段 | 生命周期钩子 | 描述 |
初始化 / 挂载 | constructor(props) | 构造函数,在组件实例化时调用,用于设置初始状态或绑定实例方法 |
挂载前 / 实例化后 | static getDerivedStateFromProps(props, state) | (可选)在每次渲染前调用,返回新的 state 以响应 props 更改,但不推荐过度依赖此方法 |
挂载前 | render() | 必须定义的方法,用于返回 jsx 元素,React 根据此方法渲染 DOM |
挂载后 | componentDidMount() | 组件挂载到 DOM 后调用,常用于网络请求、订阅或手动操作 DOM |
更新前 | shouldComponentUpdate(nextProps, nextState) | (可选)在 props 或 state 即将更改时调用,返回布尔值决定是否重新渲染 |
更新前 | static getSnapshotBeforeUpdate(prevProps, prevState) | 在最新的渲染被提交到 DOM 之前获取一些信息,返回值将在 componentDidUpdate 中作为第三个参数 |
更新 | render() | (同上)在 props 或 state 更改时再次调用 |
更新后 | componentDidUpdate(prevProps, prevState, snapshot) | 组件完成更新并重新渲染到 DOM 后调用 |
卸载前 | componentWillUnmount() | 组件从 DOM 移除之前调用,用于清理工作如取消定时器、解绑事件监听器等 |
函数组件生命周期钩子(使用 React Hooks):
阶段 | Hook 方法 | 描述 |
初始化 / 挂载 | useState() | 初始化状态并在每次渲染时返回一对值(当前状态和更新状态的函数) |
初始化 / 挂载 | useEffect(fn, deps) | 类似于 componentDidMount 和 componentDidUpdate 的合并,以及 componentWillUnmount 功能;fn 函数在组件渲染后运行,deps 是依赖数组,控制何时重新运行该效果 |
初始化 / 挂载 | useLayoutEffect(fn, deps) | 类似 useEffect,但在所有 DOM 变更之后同步调用 |
初始化 / 挂载 | useMemo(() => result, deps) | 记忆化计算结果,仅当依赖项 deps 改变时重新计算 |
初始化 / 挂载 | useCallback(fn, deps) | 记忆化函数引用,避免不必要的函数重创建 |
卸载 | useCleanup(returnFn) | 返回的函数在组件卸载时执行,用于资源清理 |
注意:useEffect
、useMemo
和 useCallback
的依赖数组可以帮助确定何时重新执行钩子逻辑。
由于 React Hooks 的引入,函数组件现在可以直接处理大部分原本需要生命周期方法才能完成的任务,使得组件更加简洁和易于维护。
旧的生命周期流程图如下:
通过两个图的对比,可以发现新版的生命周期减少了以下三种方法:
componentWillMount
componentWillReceiveProps
componentWillUpdate
其实这三个方法仍然存在,只是在前者加上了 UNSAFE_前缀,如 UNSAFE_componentWillMount,并不像字面意思那样表示不安全,而是表示这些生命周期的代码可能在未来的 react 版本可能废除
同时也新增了两个生命周期函数:
getDerivedStateFromProps
getSnapshotBeforeUpdate
3. React 性能优化的手段
React 中进行性能优化的手段可以从多个维度进行分类,以下是一些关键类别及其对应的优化策略:
1. 组件优化
- 使用 PureComponent 或 React.memo:对于仅根据 props 和 state 改变才重新渲染的组件,使用
React.PureComponent
或者对其包装一层React.memo
,它们都能通过浅比较 props 来避免不必要的重新渲染。 - shouldComponentUpdate/React Hooks 中的 useMemo/useCallback:在类组件中实现
shouldComponentUpdate
生命周期方法来手动控制是否更新组件。在函数组件中,使用useMemo
缓存计算结果,useCallback
缓存回调函数,防止因依赖项不变而引起的无效渲染。
2. 状态管理与变更
- 减少不必要的 setState 调用:合并多次对同一状态的修改,例如使用
useState
hook 时,可以利用函数式的 setState 来一次性更新多个状态值。 - 选择性地更新 state:只在 props 或 state 真正发生变化时才进行更新,避免频繁或大面积的 state 变更引发大量子组件重新渲染。
3. Virtual DOM 与 Diff 算法优化
- 合理构建组件层级:保持组件树扁平化,减少不必要的嵌套层次,使 React 的 diff 算法更高效。
- 利用 key 属性:为列表元素提供稳定的唯一 key,帮助 React 识别并最小化 DOM 变动。
- 少用 dom 层级 多使用箭头标签替代
4. 事件处理优化
- 使用合成事件:React 的合成事件系统可以减少全局事件监听器的数量,提高事件处理效率。
- 避免内联函数绑定:在事件处理函数中,避免每次渲染时创建新的函数引用,而是使用箭头函数或者
useCallback
来缓存函数引用。
5. 懒加载与代码分割
- 动态导入:使用 React.lazy 和 Suspense 来按需加载组件,减轻初始加载负担,提高首屏加载速度。
- 使用优先级加载 CSS、JavaScript 和图片资源。
6. 优化渲染过程
- 使用 ReactDOM.createPortal:将某些组件渲染到根 DOM 之外,比如渲染到 document.body,可以避免不必要的 re-render。
- CSS 动画与交互优化:配合 requestAnimationFrame 等 API 来处理复杂的动画,减少不必要的布局重排和重绘。
7、工具辅助
- Profiler 工具:利用 React DevTools 的 Profiler 面板分析组件渲染性能瓶颈。
- 性能监控与警告:设置性能指标监控点,及时发现和修复潜在性能问题。
8、前端通用优化
- 静态资源压缩与 HTTP 缓存:优化 CSS、JavaScript 文件大小,合理设置 HTTP 缓存策略。
- 服务端渲染 (SSR):针对 SEO 友好和首屏加载速度,结合 Next.js 等框架进行服务器端渲染。
4. React 如何捕获错误
错误边界(Error Boundaries):
- React 16 及更高版本引入了错误边界这一概念,它是一种特殊的 React 组件,能够在其子组件树中捕获任何渲染错误或其他 JavaScript 错误。当错误边界内的任何子组件抛出错误时,错误边界能够捕获这个错误,记录日志,并且可以选择性地显示恢复界面,而不是让整个应用程序崩溃。
除此之外还可以通过 window.onerror 或 unhandledrejection 事件监听器在全局范围内捕获未处理的错误。
window.addEventListener('error', function(event) { ... })
window.addEventListener('unhandledrejection', function(event) { ... })
5. tsx 转换成真实 DOM 过程
- TypeScript 编译阶段:
- .tsx 文件包含了 TypeScript 类型注解和 JSX 语法,首先通过 TypeScript 编译器(如 tsc 或者配合 Babel 的 @babel/preset-typescript 插件)进行编译。
- TypeScript 编译器负责检查类型注解的正确性,并且把包含类型信息的 TypeScript+JSX 代码转换为普通的 JavaScript 代码,同时保持 JSX 结构不变。
- JSX 编译阶段:
- 开发者使用 JSX 编写 React 组件。
- JSX 并非浏览器原生支持的语法,所以需要通过像 Babel 这样的编译器将其转换为标准的 JavaScript 代码。
- Babel 将 JSX 转换成 React.createElement(component, props, ...children) 的调用形式,其中 component 是组件名称或原生 DOM 元素标签名,props 是组件属性对象,children 是子元素数组。
- 创建虚拟 DOM(VDOM):
- React.createElement() 调用会产生一个虚拟 DOM 节点对象,它是一个轻量级的 JavaScript 对象,模拟了 DOM 节点的结构和属性。
- 整个组件树会转换成由这些虚拟 DOM 节点构成的虚拟 DOM 树。
- 渲染虚拟 DOM:
- 当调用 ReactDOM.render() 方法时,React 接收虚拟 DOM 树作为参数。
- React 会将这个虚拟 DOM 树与实际 DOM 进行比较(首次渲染时不存在比较,直接创建)。
- DIFF 算法与更新 DOM:
- 在组件状态或 props 更改导致重新渲染时,React 会生成一个新的虚拟 DOM 树,并与旧的虚拟 DOM 树进行高效差异比较(称为 Reconciliation 或 Diffing 过程)。
- Diff 算法找出最小化的 DOM 操作集合(增删改查节点)。
- React 随后将这些操作应用到实际的 DOM 上,仅更新需要改变的部分,而不是完全替换整个 DOM 树。
- 真实 DOM 更新:
- 最终,React 通过 ReactDOM 模块与浏览器底层交互,执行必要的 DOM 操作,将虚拟 DOM 树的改动反映到浏览器的真实 DOM 中
6. 对 Fiber 架构的理解
Fiber 即是 React 新的调度算法
在数据更新时,react 生成了一棵更大的虚拟 dom 树,给第二步的 diff 带来了很大压力——我们想找到真正变化的部分,这需要花费更长的时间。js 占据主线程去做比较,渲染线程便无法做其他工作,用户的交互得不到响应,所以便出现了 react fiber。
React 为了解决这个问题,根据浏览器的每一帧执行的特性,构思出了 Fiber 来将一次任务拆解成单元,以划分时间片的方式,按照 Fiber 的自己的调度方法,根据任务单元优先级,分批处理或吊起任务,将一次更新分散在多次时间片中,另外, 在浏览器空闲的时候, 也可以继续去执行未完成的任务, 充分利用浏览器每一帧的工作特性。
一次更新任务是分时间片执行的,直至完成某次更新。
这样 React 更新任务就只能在规定时间内占用浏览器线程了, 如果说在这个时候用户有和浏览器的页面交互,浏览器也是可以及时获取到交互内容。
Fiber 架构出现之前 react 存在的问题
- 主线程阻塞:在 React 15 及更早版本中,当组件树发生更新时,React 会通过递归算法一次性完成整个组件树的渲染过程,这个过程如果涉及大量组件,会导致主线程长时间阻塞,无法处理其他的 UI 交互,从而造成卡顿和延迟,降低用户体验。
- 无法中断与恢复渲染:原有的渲染过程不具备中断和恢复的能力,一旦开始渲染,就必须等到整个过程结束,即使在中间有更高优先级的任务也需要等待。
- 无法实现增量渲染:以往的 React 无法有效区分渲染任务的重要性和紧急程度,所有更新任务都被视为同等重要的,无法做到逐步、增量地渲染 UI。
- 资源优化不足:旧版 React 无法根据应用的具体需求动态分配资源,无法高效利用有限的 CPU 周期来优化渲染性能。
jsx 形成 dom 的以下几个步骤,react 在虚拟 dom 之前做了一层数据结构设计将 ReactElement 转换为 fiberNode
- 写 JSX 来描述 React 组件的结构和内容。
- JSX 被 Babel 转译成 React.createElement(__jsx 或__jsxs) 的调用,生成 ReactElement。
- 在 React 的协调过程中,ReactElement 被转换成 FiberNode。
- FiberNode 是 React 用来进行高效渲染和更新的数据结构,它支持并发渲染和优先级调度。
- 最终,FiberNode 的信息被用来更新浏览器中的真实 DOM,从而呈现用户界面。
Fiber 数据结构是一个链表,这样就为 Fiber 架构可中断渲染提供可能
js
复制代码
function FiberNode(){
this.tag = tag; //元素类型
this.key = key;//元素的唯一标识。
this.elementType = null; //元素类型
this.type = null;//元素类型
this.stateNode = null;//元素实例的状态节点
// Fiber
this.return = null;//该组件实例的父级。
this.child = null;//该组件实例的第一个子级。
this.sibling = null;//该组件实例的下一个兄弟级
this.index = 0;//该组件实例在父级的子级列表中的位置。
this.ref = null;//该组件实例的ref属性
this.refCleanup = null;//ref的清理函数
this.pendingProps = pendingProps;//待处理的props(最新的)
this.memoizedProps = null;//处理后的props(上一次)
this.updateQueue = null;//TODO
this.memoizedState = null;//类组件保存state信息,函数组件保存hooks信息
this.dependencies = null;//该组件实例的依赖列表
this.mode = mode;//该组件实例的模式 (DOM模式和Canvas模式)
// Effectsx
this.flags = NoFlags$1;//副作用标签 ,之前的版本是effectTag
this.subtreeFlags= NoFlags$1;//子节点副作用标签。
this.deletions = null;//待删除的子树列表。
this.lanes = NoLanes;//任务更新的优先级区分
this.childLanes = NoLanes;//子树任务更新的优先级区分
this.alternate = null;//组件实例的备份实例,用于记录前一次更新的状态。更新时候 workInProgress会复用当前值
}
- tag**:**
- tag 用来标识 Fiber 节点的类型。
- 不同的 tag 值代表了不同类型的 React 元素,比如函数组件、类组件、DOM 元素等。
- React 会根据 tag 的值来决定如何处理该 Fiber 节点。
- key**:**
- key 是一个可选的字符串,用于在兄弟元素之间建立唯一的身份。
- **当列表重新排序或元素添加 / 删除时,**key 帮助 React 识别哪些元素发生了变化,从而高效地更新 UI。
- **在 Fiber 节点中,**key 用于在协调过程中识别节点的身份。
- child**:**
- child 指向 Fiber 节点的第一个子节点。
- 通过 child 属性,React 可以遍历 Fiber 树,执行渲染和更新操作。
- sibling**:**
- sibling 指向 Fiber 节点的下一个兄弟节点。
- 当 React 遍历完一个 Fiber 节点的所有子节点后,它会通过 sibling 属性移动到下一个兄弟节点,继续遍历。
- return**:**
- return 指向 Fiber 节点的父节点。
- 通过 return 属性,React 可以在 Fiber 树中向上回溯,这对于错误处理和优先级调度等功能非常重要。
Fiber 树生成
- 首先调用 createRoot 方法创建 FiberRoot(应用根节点)、RootFiber(Fiber 树的根节点),目前对应节点上的数据都是空的,生成的数据结构如下:
FiberRoot={
"tag": 1,//ConcurrentRoot
"containerInfo": "div#root",//挂载的dom节点
"current": { // RooFiber
"tag": 3,//标记Fiber的类型(如类组件、函数组件、DOM组件等)
"key": null,//用于在列表或其他需要区分子元素的场景中识别Fiber的键。
"elementType": null,//通常与type相同,但在某些情况下(如懒加载组件)可能不同。它指的是要渲染的元素类型。
"type": null,//组件的类型(函数、类等)或DOM节点的类型(如'div')
"stateNode": null,//对于DOM组件,这是实际的DOM节点;对于类组件,这是组件的实例
"return": null,//指向父Fiber的指针
"child": null,//指向子Fiber的指针
"sibling": null,// 指向兄弟Fiber的指针
"index": 0,//在父Fiber的子节点列表中的索引
"ref": null,//用于获取DOM节点或类组件实例的引用
"refCleanup": null,
"pendingProps": null,//新的或待处理的props
"memoizedProps": null,//上一次渲染使用的props
"updateQueue": null,//存储状态更新和回调的队列
"memoizedState": null,//上一次渲染时的状态
"dependencies": null,
"mode": 3,//表示Fiber的渲染模式(如并发模式、阻塞模式等)
"flags": 0,//用于跟踪Fiber位字段的状态和效果的位字段
"subtreeFlags": 0,//用于跟踪Fiber子树的状态和效果的位字段
"deletions": null,//指向要删除的Fiber子树的指针
"lanes": 0,//与优先级和并发渲染相关的内部字段
"childLanes": 0,//与优先级和并发渲染相关的内部字段
"alternate": null,//在双缓冲系统中,指向对应Fiber的指针(用于新旧树之间的比较)
"actualDuration": 0,
"actualStartTime": -1,
"selfBaseDuration": 0,
"treeBaseDuration": 0,
},
//...
}
- 调用 render 方法创建对应 Fiber 节点的信息,因为上一波生成都是空,我们需要把组件 App(),dev 节点都构建成 Fiber node。后面你就需要知道 React Fiber 是如何工作的?
React Fiber 工作原理详解
双缓冲技术: React Fiber 使用了类似于图形渲染中的双缓冲技术。这意味着在构建新的 UI 树时,React 会同时在内存中维护两棵树:当前屏幕上显示的树(current tree)和正在构建的树(work-in-progress tree)。只有当新的树完全构建完成后,它才会被一次性地渲染到屏幕上,从而实现更加流畅的用户体验。
任务调度: React Fiber 引入了任务调度的概念,允许将渲染工作拆分成多个较小的任务单元。这些任务单元可以被中断和恢复,从而实现并发渲染。React 根据任务的优先级来决定它们的执行顺序,确保高优先级的任务(如用户交互)能够优先执行。
运行方式:
- Reconciliation 阶段: 当 React 决定要更新 UI 时,它会启动 reconciliation(协调)过程。在这个阶段,React 会比较新旧两棵树之间的差异,并为需要更新的组件生成相应的 Fiber 节点。这个过程是异步的,可以被中断和恢复。
- Commit 阶段: 当所有的 Fiber 节点都被处理完毕后,React 会进入 commit 阶段。在这个阶段,React 会将之前在 render 阶段计算出的所有变化一次性应用到 DOM 上,并触发相关的生命周期方法(如 useEffect,useLayoutEffect 方法)。这个过程是不可中断的,因为它涉及到实际的 DOM 操作。
- 优先级调度: React Fiber 通过优先级调度来管理任务的执行顺序。每个 Fiber 节点都有一个与之关联的优先级,React 会根据节点的优先级来决定哪些节点需要先更新。高优先级的任务(如用户交互)会打断低优先级的任务(如定时器回调)并优先执行,从而实现更流畅的用户体验。
Reconciliation 阶段
在 Reconciliation 阶段,React 会遍历 Fiber 树,并执行每个 Fiber 节点的更新逻辑。这个过程可以被分为两个阶段:beginWork 和 completeWork。在 beginWork 阶段,React 会执行组件的渲染逻辑,并计算副作用(side effects)。在 completeWork 阶段,是向上归并的过程,如果有兄弟节点,会返回 sibling 兄弟,没有返回 return 父级,一直返回到 fiebrRoot ,期间可以形成 effectList,对于初始化流程会创建 DOM ,对于 DOM 元素进行事件收集,处理 style,className 等,这个阶段并不直接更新 DOM 或触发任何用户可见的更改,而是为后续的 Commit 阶段做准备。
Reconciliation 阶段
在 Reconciliation 阶段,React 会遍历 Fiber 树,并执行每个 Fiber 节点的更新逻辑。这个过程可以被分为两个阶段:beginWork 和 completeWork。在 beginWork 阶段,React 会执行组件的渲染逻辑,并计算副作用(side effects)。在 completeWork 阶段,是向上归并的过程,如果有兄弟节点,会返回 sibling 兄弟,没有返回 return 父级,一直返回到 fiebrRoot ,期间可以形成 effectList,对于初始化流程会创建 DOM ,对于 DOM 元素进行事件收集,处理 style,className 等,这个阶段并不直接更新 DOM 或触发任何用户可见的更改,而是为后续的 Commit 阶段做准备。
beginWork 做了什么?
- 对于组件,执行部分生命周期,执行 render ,得到最新的 children 。
- 向下遍历调和 children ,复用 oldFiber (diff 算法),diff 流程。
- 打不同的副作用标签 effectTag ,比如类组件的生命周期,或者元素的增加,删除,更新
Commit 阶段
在 Commit 阶段,React 将根据在 Reconciliation 阶段生成的更新计划来执行实际的 DOM 更新。这个过程包括更新 DOM 节点、处理生命周期方法(如在类组件中的 useEffect)以及执行其他与渲染相关的副作用。此阶段是同步执行的,意味着一旦开始,就会一口气完成,不会被其他任务打断
- 对于组件,执行部分生命周期,执行 render ,得到最新的 children 。
- 向下遍历调和 children ,复用 oldFiber (diff 算法),diff 流程。
- 打不同的副作用标签 effectTag ,比如类组件的生命周期,或者元素的增加,删除,更新
那 React Fiber 是怎么实现的
主要是通过两个原生的 API 来实现的 requestAnimationFrame 和 requestIdleCallback
显示器每秒 60 帧我们看着才不会感觉到卡嘛,比如动画的时候,一帧的时间内布局和绘制结束,还有剩余时间,JS 就会拿到主线程使用权,如果 JS 某个任务执行过长,动画下一帧开始时 JS 还没有执行完,就会导致掉帧,出现卡顿。
所以就通过把 JS 任务分成更小的任务块,分到每一帧上的方式,一帧时间到先暂停 JS 执行,然后下一帧绘制任完成再把主线程交给 JS,在每一帧绘制之前调用 requestAnimationFrame;在每一帧空间阶段,就是一帧动画任务完成,下一帧还没到开始时间,这中间还有时间的话就调用 requetIdleCallback,执行它里面的任务
Fiber 具体都做了什么?
React Fiber 架构是 React 在 16 版本推出的一种全新的、革命性的调度算法和组件更新机制。它对 React 核心算法进行了重构,以满足更流畅的 UI 渲染、更灵活的任务调度以及更好的交互体验。
react fiber 使得 diff 阶段有了被保存工作进度的能力
Fiber 架构通过引入可中断和恢复的渲染机制,以及基于任务优先级的调度系统,使得 React 能够更灵活地管理渲染任务,实现增量渲染和精细化调度,从而显著提升了性能和用户体验。此外,Fiber 架构也为 React 后期支持并发渲染和异步数据流等功能打下了基础
核心特点与优势:
- 精细化任务调度:
- 在 Fiber 架构之前,React 的更新是同步且整体的,一旦开始更新,直到结束才会释放浏览器主线程,这可能导致长时间阻塞,无法响应用户的其他交互。
- Fiber 架构则采用了增量(Incremental)和可暂停(Interruptible)的渲染方式,将渲染任务分解为一个个小的工作单元(Fiber 节点),并可以根据优先级和浏览器环境的空闲时间灵活调度,确保 UI 的渲染不会阻塞用户交互。
- 可恢复更新:
- Fiber 架构允许 React 在渲染过程中暂停和恢复,这意味着 React 可以在执行更新的过程中临时切换去处理高优先级的任务(如响应用户的鼠标点击或滚动事件),然后再回到之前的渲染任务,这样就保证了用户界面始终能够快速响应用户的操作。
- 优先级调度:
- Fiber 架构支持任务优先级排序,不同的更新任务可以根据其影响范围、交互重要性等因素赋予不同优先级,优先处理对用户体验影响更大的更新。
- 协调(Reconciliation)算法优化:
- Fiber 架构改进了 React 的协调算法,使其在对比新老虚拟 DOM 树时更为智能,能够更快地找出需要更新的 DOM 节点,从而提高渲染性能。
- 并发模式与 Suspense:
- 基于 Fiber 架构,React 得以实现并发模式,通过时间切片(Time Slicing)技术分配渲染任务,未来还可以更好地处理异步数据加载,如 Suspense 组件。
总之,React Fiber 架构在很大程度上提升了 React 的性能表现和用户体验,使得 React 能够在更复杂的场景下仍然保持高效、流畅和可预测的行为。同时,也为 React 未来的演进提供了坚实的基础和更多可能性。
底层原理
我们要找到前后状态变化的部分,必须把所有节点遍历。
在老的架构中,节点以树的形式被组织起来:每个节点上有多个指针指向子节点。要找到两棵树的变化部分,最容易想到的办法就是深度优先遍历,规则如下:
- 从根节点开始,依次遍历该节点的所有子节点;
- 当一个节点的所有子节点遍历完成,才认为该节点遍历完成;
如果你系统学习过数据结构,应该很快就能反应过来,这不过是深度优先遍历的后续遍历。根据这个规则,在图中标出了节点完成遍历的顺序。
这种遍历有一个特点,必须一次性完成。假设遍历发生了中断,虽然可以保留当下进行中节点的索引,下次继续时,我们的确可以继续遍历该节点下面的所有子节点,但是没有办法找到其父节点——因为每个节点只有其子节点的指向。断点没有办法恢复,只能从头再来一遍。
以该树为例:
在遍历到节点 2 时发生了中断,我们保存对节点 2 的索引,下次恢复时可以把它下面的 3、4 节点遍历到,但是却无法找回 5、6、7、8 节点。
在新的架构中,每个节点有三个指针:分别指向第一个子节点、下一个兄弟节点、父节点。这种数据结构就是 fiber,它的遍历规则如下:
- 从根节点开始,依次遍历该节点的子节点、兄弟节点,如果两者都遍历了,则回到它的父节点;
- 当一个节点的所有子节点遍历完成,才认为该节点遍历完成;
根据这个规则,同样在图中标出了节点遍历完成的顺序。跟树结构对比会发现,虽然数据结构不同,但是节点的遍历开始和完成顺序一模一样。不同的是,当遍历发生中断时,只要保留下当前节点的索引,断点是可以恢复的——因为每个节点都保持着对其父节点的索引。
同样在遍历到节点 2 时中断,fiber 结构使得剩下的所有节点依旧能全部被走到。
这就是 react fiber 的渲染可以被中断的原因。树和 fiber 虽然看起来很像,但本质上来说,一个是树,一个是链表。
fiber 是纤程
这种数据结构之所以被叫做 fiber,因为 fiber 的翻译是纤程,它被认为是协程的一种实现形式。协程是比线程更小的调度单位:它的开启、暂停可以被程序员所控制。具体来说,react fiber 是通过 requestIdleCallback 这个 api 去控制的组件渲染的 “进度条”。
requesetIdleCallback 是一个属于宏任务的回调,就像 setTimeout 一样。不同的是,setTimeout 的执行时机由我们传入的回调时间去控制,requesetIdleCallback 是受屏幕的刷新率去控制。本文不对这部分做深入探讨,只需要知道它每隔 16ms 会被调用一次,它的回调函数可以获取本次可以执行的时间,每一个 16ms 除了 requesetIdleCallback 的回调之外,还有其他工作,所以能使用的时间是不确定的,但只要时间到了,就会停下节点的遍历。
使用方法如下:
const workLoop = (deadLine) => {
let shouldYield = false;// 是否该让出线程
while(!shouldYield){
console.log('working')
// 遍历节点等工作
shouldYield = deadLine.timeRemaining()<1;
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop);
requestIdleCallback 的回调函数可以通过传入的参数 deadLine.timeRemaining() 检查当下还有多少时间供自己使用。上面的 demo 也是 react fiber 工作的伪代码。
但由于兼容性不好,加上该回调函数被调用的频率太低,react 实际使用的是一个 polyfill(自己实现的 api),而不是 requestIdleCallback。
现在,可以总结一下了:React Fiber 是 React 16 提出的一种更新机制,使用链表取代了树,将虚拟 dom 连接,使得组件更新的流程可以被中断恢复;它把组件渲染的工作分片,到时会主动让出渲染主线程。
react fiber 带来的变化
- 使用新架构后,动画变得流畅,宽度的变化不会卡顿;
- 使用新架构后,用户响应变快,鼠标悬停时颜色变化更快;
动画变流畅的根本原因,一定是一秒内可以获得更多动画帧。但是当我们使用 react fiber 时,并没有减少更新所需要的总时间。
上面是使用旧的 react 时,获得每一帧的时间点,下面是使用 fiber 架构时,获得每一帧的时间点,因为组件渲染被分片,完成一帧更新的时间点反而被推后了,我们把一些时间片去处理用户响应了。
这里要注意,不会出现 “一次组件渲染没有完成,页面部分渲染更新” 的情况,react 会保证每次更新都是完整的。
但页面的动画确实变得流畅了,这是为什么呢?
我们现在已经知道了 react fiber 是在弥补更新时 “无脑” 刷新,不够精确带来的缺陷。这是不是能说明 react 性能更差呢?
并不是。孰优孰劣是一个很有争议的话题,在此不做评价。因为 vue 实现精准更新也是有代价的,一方面是需要给每一个组件配置一个 “监视器”,管理着视图的依赖收集和数据更新时的发布通知,这对性能同样是有消耗的;另一方面 vue 能实现依赖收集得益于它的模版语法,实现静态编译,这是使用更灵活的 JSX 语法的 react 做不到的。
在 react fiber 出现之前,react 也提供了 PureComponent、shouldComponentUpdate、useMemo,useCallback 等方法给我们,来声明哪些是不需要连带更新子组件。
- react 因为先天的不足——无法精确更新,所以需要 react fiber 把组件渲染工作切片;而 vue 基于数据劫持,更新粒度很小,没有这个压力;
- react fiber 这种数据结构使得节点可以回溯到其父节点,只要保留下中断的节点索引,就可以恢复之前的工作进度;
7. diff 的原理
React 中的 diff 算法是其核心优化策略之一,用于比较新旧两个虚拟 DOM 树之间的差异,并找出最小化的 DOM 操作集以更新真实 DOM。以下是 React 中 diff 算法的大致步骤概述:
- 树结构比较:
- React 并不会简单地递归遍历整颗新旧虚拟 DOM 树进行全量比较,而是采用分层比较的思想,仅比较同层级的节点。
- 它首先会比较树的根节点,如果根节点不同,则直接替换整个根节点对应的真实 DOM 元素。
- 同层级节点比较:
- 对于同层级的子节点,React 会尝试找到旧树中与新树中每一个节点相匹配的节点,主要依据是它们的
**key**
属性。
- 对于同层级的子节点,React 会尝试找到旧树中与新树中每一个节点相匹配的节点,主要依据是它们的
- 如果找到了具有相同
key
的节点,则认为它们是同一个 “实体”,只需更新属性或内容。 - 如果没有找到相同
key
的节点,则视为新增或删除节点。
- 如果找到了具有相同
- 属性比较与更新:
- 对于找到匹配节点的情况,React 会进一步比较它们的属性是否有变化,如果有变化则更新相应的真实 DOM 元素的属性。
- 文本节点与组件节点处理:
- 文本节点的比较相对简单,直接比较文本内容即可。
- 组件节点则根据组件类型(类或函数组件)以及 props 的变化决定是否需要重新渲染。
- 子节点递归比较:
- 当节点匹配成功后,继续对其子节点进行同样的分层级比较,直至所有子节点都已比较完毕。
- 跳过跨层级操作:
- React 假设在一次更新中,一个节点及其子节点不会随意地在 DOM 树中跨层级移动。因此,它在寻找匹配节点时只会对兄弟节点进行比较,而不是在整个树中搜索,大大降低了算法复杂度。
- 删除与插入操作:
- 在比较过程中,React 收集到的所有需要删除的旧节点和需要插入的新节点,会在比较结束后统一进行 DOM 操作,避免频繁地增删 DOM 元素。
- 优化策略:
- React 还有一系列优化策略,例如先处理移动或更新的节点,尽量减少 DOM 元素的移动次数;对组件类型不同的节点直接进行替换,不尝试深入比较等。
通过以上步骤,React 的 diff 算法能够在 O(n) 的时间复杂度内完成虚拟 DOM 树的比较,从而实现在大量 DOM 更新时依然保持较高的性能。
8. 提高组件的渲染效率的?避免不必要的 render?
在 React 中提高组件渲染效率并避免不必要的渲染主要有以下几个策略:
- 使用 PureComponent 或 React.memo:
React.PureComponent
自动进行浅比较 (shallow comparison
),只有当 props 或 state 发生改变时才会触发组件重新渲染。继承自React.PureComponent
的组件会默认检查 props 和 state 对象是否严格相等。- 对于函数组件,可以使用
React.memo
对其进行包裹,React.memo
同样会进行浅比较,只有当 props 发生变化时才会重新渲染组件。
- 自定义 shouldComponentUpdate 生命周期方法:
- 在 class 组件中,可以覆盖
shouldComponentUpdate(nextProps, nextState)
方法,根据传入的新 props 和新 state 判断是否有必要调用render
方法,从而避免不必要的渲染。
- 在 class 组件中,可以覆盖
- 使用 React Hooks 进行优化:
- 使用
React.useState
和React.useReducer
时,可以根据业务逻辑精确控制 state 的变化,避免不必要的状态更新。 - 使用
React.useMemo
来缓存计算结果,仅当依赖的 props 或 state 改变时才重新计算。 - 使用
React.useCallback
来缓存函数引用,避免在 props 没变的情况下因为回调函数引用变了而导致不必要的子组件重渲染。
- 使用
- 优化数据结构:
- 避免在 props 或 state 中传递深度嵌套的对象或数组,因为 React 的默认浅比较无法检测到深层数据的变化。若必须使用复杂数据结构,应当在适当的地方使用
shouldComponentUpdate
、useMemo
或手动进行深比较。
- 避免在 props 或 state 中传递深度嵌套的对象或数组,因为 React 的默认浅比较无法检测到深层数据的变化。若必须使用复杂数据结构,应当在适当的地方使用
- 优化事件处理器:
- 将事件处理器封装在
useCallback
中,保证其在 props 不变时引用始终一致,避免无意义的组件重渲染。
- 将事件处理器封装在
- 减少不必要的 state 和 props 更新:
- 只有当数据实际变化时才更新 state,避免频繁调用
setState
。 - 使用 Context API 或者 Redux 等状态管理库时,确保只在数据变化时触发全局状态更新。
- 只有当数据实际变化时才更新 state,避免频繁调用
通过以上这些方法,可以最大程度地减少不必要的组件渲染,从而提升 React 应用的性能。在实践中,需要根据组件的具体情况进行权衡和选择最适合的优化方案。
9. React render 方法的原理
React 的render
方法是 React 组件的核心方法之一,它的基本原理和作用在于将组件的状态和属性转化为可以在浏览器中渲染的虚拟 DOM 表示,然后将这个虚拟 DOM 转化为实际的 DOM 操作,最终更新到浏览器的真实 DOM 中。
以下是render
方法的基本原理和过程:
- 首次渲染:
当组件实例化并初次插入到 DOM 中时,React 会调用该组件的render
方法。这个方法必须返回一个 React 元素(可以是原生 DOM 元素、组件元素或 Fragment),React 会根据这个返回值创建一个虚拟 DOM 树。 - 虚拟 DOM 的创建与比对:
render
方法返回的是一个虚拟 DOM 树,这是一个轻量级的 JavaScript 对象结构,与实际的 DOM 树结构相似但并非真实的 DOM 节点。- 当组件的
props
或state
发生变化时,React 会重新执行render
方法生成新的虚拟 DOM 树。 - React 使用其内部的高效算法——虚拟 DOM Diff 算法,比较新旧两棵虚拟 DOM 树的差异。
- 最小化 DOM 操作:
- 根据虚拟 DOM 的比较结果,React 确定最少必要的 DOM 操作,如添加、更新或删除 DOM 节点,而不是每次都完全重建 DOM 树。
- 这种增量更新机制极大提高了 React 应用的性能,因为它避免了频繁地直接操作 DOM 带来的性能损耗。
- DOM 更新:
- 最终,React 将这些最小化的 DOM 操作应用到实际 DOM 树上,确保用户界面得到准确、高效的更新。
React render
方法的作用就是将组件的状态和属性转化为虚拟 DOM,通过虚拟 DOM Diff 算法来决定实际 DOM 的最小更新操作,从而实现高性能的用户界面更新。在类组件中,**render**
方法是必需的,而在函数组件中,函数体本身充当了**render**
方法的角色。
10. Redux 原理
从 Flux 中衍生来的(单一数据源,单向数据流)
官方解释:Redux 是 JavaScript 状态容器, 提供可预测化的状态管理。我的理解是,redux 是为了解决 react 组件间通信和组件间状态共享而提出的一种解决方案,主要包括 3 个部分,(store + action + reducer)。
- **store:**用来存储当前 react 状态机(state)的对象。connect 后,store 的改变就会驱动 react 的生命周期循环,从而驱动页面状态的改变
- State: store 对象包含的所有数据
- action: 用于接受 state 的改变命令,是改变 state 的唯一途径和入口。一般使用时在当前组件里面调用相关的 action 方法,通常把和后端的通信 (ajax) 函数放在这里
- reducer: action 的处理器,用于修改 store 中 state 的值,返回一个新的 state 值
- Dispatch: view 发出 action 的唯一方法
主要解决什么问题:
1、组件间通信
由于 connect 后,各 connect 组件是共享 store 的,所以各组件可以通过 store 来进行数据通信,当然这里必须遵守 redux 的一些规范,比如遵守 view -> aciton -> reducer 的改变 state 的路径
2、通过对象驱动组件进入生命周期
对于一个 react 组件来说,只能对自己的 state 改变驱动自己的生命周期,或者通过外部传入的 props 进行驱动。通过 redux,可以通过 store 中改变的 state,来驱动组件进行 update
3、方便进行数据管理和切片
redux 通过对 store 的管理和控制,可以很方便的实现页面状态的管理和切片。通过切片的操作,可以轻松的实现 redo 之类的操作
11. redux 中间件?
Redux 中间件(Middleware)是 Redux 库中一个强大的扩展机制,它位于 action 被发出(dispatched)和到达 reducer 处理这两个阶段之间。中间件可以看作是对 Redux dispatch 流程的一种拦截器,它允许开发者在 action 传播的过程中执行额外的操作,例如日志记录、异步处理、事务控制、取消操作、异常处理等。
- redux 中间件接受一个对象作为参数,对象的参数上有两个字段 dispatch 和 getState,分别代表着 Redux Store 上的两个同名函数。
- 柯里化函数两端一个是 middewares,一个是 store.dispatch
中间件通过链式调用的方式来组织,形成一个中间件栈。当一个 action 被 dispatch 时,它会依次经过中间件栈中的每一个中间件。每个中间件都有机会查看 action,对其进行操作(如修改、延迟 dispatch 或发起异步请求),然后决定是否将 action 传递给下一个中间件或者直接发送给 reducers 进行状态更新。
中间件的结构通常遵循一个标准的函数签名,即接受 store
的 dispatch
方法和 getState
方法作为参数,然后返回一个新的增强过的 dispatch
函数。这个新的 dispatch
函数会在执行原先的 dispatch 行为前后插入自定义的逻辑。
Redux 中常见的中间件如 redux-thunk
和 redux-saga
分别用于简化异步操作的处理。redux-thunk
允许 dispatch 一个函数而不是单纯的 action 对象,这个函数可以在运行时生成和 dispatch 多个 action。而 redux-saga
通过生成器函数实现复杂的异步流程控制,它可以监听 actions 并触发一系列的异步操作,然后再 dispatch 回结果 action。
12. 对 React Hooks 的理解
hooks 的出现,使函数组件的功能得到了扩充,拥有了类组件相似的功能,在我们日常使用中,使用 hooks 能够解决大多数问题,并且还拥有代码复用机制,因此优先考虑 hooks
解决老的函数式组件在 React Hook 出现之前,函数式组件(也称为无状态组件)的主要特点与优缺点如下:
早期函数式组件优点:
**简洁性:**函数式组件代码结构简单,易于阅读和理解,因为它仅负责接收 props 并基于 props 返回 JSX 元素,不涉及复杂的生命周期方法和状态管理。
**效率:**由于没有内部状态和生命周期方法,函数式组件在每次 props 改变时都会重新渲染,而这种简单的渲染方式往往更快,减少了不必要的计算和 DOM 操作。
**易测试:**由于它们是纯函数,不依赖外部状态或上下文,因此单元测试更加容易和可靠。
**记忆化:**React 在某些情况下能够利用 PureComponent 或 shouldComponentUpdate 优化,减少不必要的渲染,即使对于无状态组件。
函数式组件缺点:
无状态:最大的限制在于它们不能拥有自身的 state,所有数据必须由父组件通过 props 传递,难以实现局部状态管理。
无生命周期方法:这意味着无法在组件挂载、更新、卸载等阶段执行自定义操作,比如数据获取、订阅、清理等。
逻辑复用困难:若需复用包含副作用或状态相关的逻辑,往往需要借助高阶组件(HOC)或 Render Props 等模式,这会导致组件层级过深,代码组织不够直观。
Hooks 是 useState、useEffect、useMemo 等 hook 方法的总称,提供了一种在函数组件中实现状态逻辑、生命周期方法、副作用处理以及其他各种功能的方法,使得函数组件也能拥有原本只有类组件才能拥有的能力。
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性
至于为什么引入 hook,官方给出的动机是解决长时间使用和维护 react 过程中常遇到的问题,例如:
【状态复用困难】难以重用和共享组件中的与状态相关的逻辑
【可维护性差】逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 local state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面
【this 心智负担高】类组件中的 this 增加学习成本,类组件在基于现有工具的优化上存在些许问题
【函数组件太弱】由于业务变动,函数组件不得不改为类组件等等
为函数式组件赋能
在以前,函数组件也被称为无状态的组件,只负责渲染的一些工作
因此,现在的函数组件也可以是有状态的组件,内部也可以维护自身的状态以及做一些逻辑方面的处理
在 React Hooks 推出之前,函数式组件非常适合于那些只需要根据传入 props 进行渲染的简单场景,但对于复杂的交互逻辑和状态管理,开发者不得不转向类组件或采用间接的方式来弥补这些不足。React Hooks 的引入极大地增强了函数式组件的能力,使得它们既能保持简洁又能拥有状态管理和生命周期功能,有效地解决了上述缺点。
13. 高阶组件
React 中的高阶组件(Higher-Order Component,简称 HOC)是一种高级的 React 组件抽象概念,它本质上是一个函数,此函数接受一个 React 组件作为参数,并返回一个新的封装过的 React 组件。高阶组件主要用于代码复用、逻辑抽象和交叉关注点的处理,比如权限控制、数据预取、主题样式切换等场景。
什么是高阶组件:
在函数式编程的概念中,高阶函数是指接受函数作为输入或者输出函数的函数。在 React 中,高阶组件遵循同样的原则,它是接收组件并返回新组件的函数,这样可以使得我们能够在不修改原始组件代码的情况下为其增加额外的功能。
怎么编写高阶组件:
下面是一个基础的高阶组件示例,它接收一个组件WrappedComponent
并返回一个新的组件,为传入的组件增加了某些通用逻辑或特性:
function withEnhancement(WrappedComponent) {
return class EnhancedComponent extends React.Component {
componentDidMount() {
// 在这里添加额外的生命周期逻辑
}
render() {
// 可能会修改或添加props,或者包裹WrappedComponent
const newProps = { ...this.props, extraProp: 'someValue' };
return <WrappedComponent {...newProps} />;
}
};
}
// 使用高阶组件
const EnhancedMyComponent = withEnhancement(MyComponent);
应用场景:
- 跨组件状态管理:当多个组件需要共享相同的数据源或状态时,可以通过高阶组件统一管理和分发状态。
- Props 代理:HOC 可以拦截和修改传入子组件的 props,例如注入依赖、处理副作用逻辑或进行数据转换。
- 生命周期方法的封装:可以集中处理诸如数据加载、订阅和取消订阅等生命周期事件。
- 组件装饰:为组件添加全局功能,如路由、错误边界、日志记录、性能检测等。
- 权限控制:根据用户的权限信息,决定是否渲染某个组件或者限制组件的部分功能。
- 响应式数据获取:在组件挂载前或更新时自动获取数据,然后将其作为 props 传递给被包裹的组件。
- 主题切换:允许在不同主题间切换,通过高阶组件动态修改组件的样式或其他主题相关属性。
总结起来,高阶组件提供了一种强大的抽象手段,帮助开发者更好地组织和复用代码,保持组件层级扁平化,并且能够集中处理与特定业务逻辑或框架无关的通用需求。随着 React Hooks 的引入,虽然很多原本通过 HOC 实现的需求可以转由自定义 Hooks 完成,但在某些情况下,特别是在处理复杂的组件组合和扩展时,高阶组件依然是一种有效的设计模式。
通过上面的了解,高阶组件能够提高代码的复用性和灵活性,在实际应用中,常常用于与核心业务无关但又在多个模块使用的功能,如权限控制、日志记录、数据校验、异常处理、统计上报等
数据流管理:例如在 Redux 中,connect 函数就是一个典型的高阶组件,用来连接 React 组件与 Redux Store。
生命周期管理:为多个组件提供统一的生命周期逻辑,如初始化、清理资源、错误处理等。
条件渲染:根据某些条件动态决定是否渲染某个组件,或者渲染不同的组件版本。
API 调用封装:在组件渲染前后执行 API 调用,并将数据注入到组件 props 中。
权限控制
日志记录
数据校验
异常处理
统计上报
14. 受控组件和非受控组件
React 受控组件(Controlled Components):
受控组件是指组件内部的状态完全由 React 的 state 管理,用户输入的值立即反映到组件的 state 中。在受控组件中,表单元素(如<input>
、<textarea>
或<select>
)的值不是直接由 DOM 本身管理,而是通过 React 组件的 state 进行控制。例如,<input>
元素的值由value
属性关联到组件的 state 变量,每次用户输入时,都会触发 onChange 事件,此时组件会根据事件处理函数更新 state,进而更新value
属性,始终保持 DOM 元素的值与 state 同步。
特点:
- 值由 React 组件控制,用户输入不会直接修改 DOM 元素的值。
- 必须提供 onChange 事件处理函数,以更新组件状态。
- 表单提交时,可以通过组件自身的 state 获取用户输入的最新数据。
优点:
- 提供了对用户输入行为的完全控制,方便在表单验证、实时反馈等方面做出快速反应。
- 状态管理集中,有利于组件间的协作和数据一致性。
缺点:
- 需要额外的代码来处理 onChange 事件和更新 state,增加了开发复杂度。
- 不适用于某些不需要实时响应用户输入,或者希望由 DOM 自身处理的场景。
常用场景:
- 实现复杂的表单验证逻辑,需要实时获取并校验用户输入的表单。
- 用户输入内容需要与其他组件或数据源紧密联动,例如搜索框实时查询。
React 非受控组件(Uncontrolled Components):
非受控组件则是指 DOM 元素的值由浏览器本身管理,不受 React 组件的 state 直接影响。在非受控组件中,表单元素的值是由 DOM 自身的 defaultValue 属性初始化,用户输入的改变并不会立刻同步到 React 组件的 state 中。获取非受控组件的值通常通过 ref 来访问 DOM 节点的 value 属性。
特点:
- 用户输入直接改变 DOM 元素的值,不依赖 React 组件的 state。
- 不需要提供 onChange 事件处理函数来更新 state。
- 获取当前值时,通常需要使用 ref。
优点:
- 开发简单,更接近原生 HTML 表单的使用方式。
- 适用于只需要在表单提交时获取用户输入值的简单场景。
缺点:
- 无法实时跟踪表单值的变化,不利于实现即时验证或联动其他组件的功能。
- 多个组件共享数据时需要额外考虑数据同步问题。
常用场景:
- 简单的表单,仅在提交时才关心用户输入的值,而不关心输入过程。
- 需要保留浏览器默认行为,比如在文本框中粘贴内容时保持原有格式。
总结来说,受控组件适合于需要对用户输入实时响应和严格控制的场景,而非受控组件更适合于简单的表单处理,或者是需要利用原生 DOM 特性的场合。开发者应根据具体需求灵活选择合适的方式来处理表单输入。
15. React refs 的理解
React 中的 ref
是一种在组件之间直接访问 DOM 节点或在函数组件中访问类组件实例的能力。React 的 ref
底层原理涉及以下几个关键点:
- 创建和分配 Refs:
- React 提供了几种创建 ref 的方式,如
React.createRef()
(适用于类组件)、useRef()
Hook(适用于函数组件)以及forwardRef()
(用于在函数组件之间传递 ref)。 - 创建 ref 后,可以通过
ref={myRef}
的方式将 ref 分配给具体的 DOM 元素或类组件实例。
- React 提供了几种创建 ref 的方式,如
- React 内部对 Ref 的处理:
- 当 React 渲染组件时,它会注意到带有
ref
属性的元素,并将 ref 的.current
属性指向相应的 DOM 节点或组件实例。 - 对于 DOM 节点,React 在组件挂载时将 ref 的
.current
设置为对应的 DOM 元素。 - 对于类组件,
.current
指向的是组件的实例对象。 - 对于函数组件,通过
forwardRef
结合useImperativeHandle
Hook,可以让函数组件暴露特定方法或属性给父组件。
- 当 React 渲染组件时,它会注意到带有
- 生命周期中的 Ref 更新:
- 当组件的生命周期发生变化(如挂载、更新或卸载)时,React 会更新 ref 的
.current
属性。 - 在组件卸载时,
.current
会被设置为null
,以避免对已不存在的 DOM 节点或组件实例进行引用。
- 当组件的生命周期发生变化(如挂载、更新或卸载)时,React 会更新 ref 的
- Ref 的传播与转发:
- 使用
React.forwardRef
API,我们可以创建一个能够将接收到的ref
传递给其内部子组件的组件,这对于函数组件尤为有用,因为它允许函数组件也能获得和操作内部 DOM 节点或子组件实例。
- 使用
- 底层实现:
- 在 React 的 Fiber 架构中,refs 是在调度和 reconciliation 过程中进行处理的。
- React 通过
commitWork
阶段将更新的 ref 信息应用到实际的 DOM 树中。
总而言之,React 的 ref
系统通过对组件生命周期的精细控制和对 DOM 操作的抽象,为开发者提供了便捷地访问和操作底层 DOM 节点的能力,同时也支持组件间的直接交互。React 内部通过巧妙的数据结构和算法确保了 ref 更新的高效和准确。
- Ref 的底层绑定
当 React 在渲染过程中遇到带有 ref 属性的元素时,会通过内部机制将 ref 对象与对应的 DOM 节点或组件实例建立联系。对于 DOM 元素,React 会在组件挂载后立即将 ref 对象的 current 属性指向对应的 DOM 节点。
对于类组件,ref 的 current 属性将会指向组件的实例。这样就可以访问类组件的实例方法和内部状态。
对于函数组件,通过 forwardRef 和 useImperativeHandle 可以定制函数组件对外暴露的实例行为,使其也可以拥有类似类组件实例的行为。
- 生命周期中的 Ref 管理
在组件生命周期的不同阶段,React 会适当地更新 ref 的 current 属性。例如,在组件卸载时,React 会将 ref 的 current 设为 null,以防止对已卸载的 DOM 节点或组件实例进行引用。 - Ref 的更新和同步
React 的 Fiber 架构在调度和 Reconciliation 过程中处理 ref。在确定了 DOM 更新计划后,React 会在 commit 阶段把 ref 的更新同步到实际的 DOM 树中。 - React Fiber 与 Ref 的关系
React Fiber 通过 commitRootImpl 等内部方法,在 DOM 更新阶段遍历 Fiber 节点,当遇到有 ref 的节点时,会将 ref.current 指向正确的 DOM 节点或组件实例。
总的来说,React 通过一套内部机制,在不影响 React 组件抽象的同时,为开发者提供了一种间接但灵活地访问和操作 DOM 节点的方式。通过 ref,开发者可以在 React 的声明式编程模型中实现对 DOM 的命令式操作,这对处理聚焦、测量尺寸、动画等场景至关重要。
16. 组件之间如何通信
父组件向子组件通信:父组件通过 props 向子组件传递需要的信息。
子组件向父组件通信:: props + 回调的方式。
跨级组件的通信方式?
- Context API:
React 的 Context API 提供了一种在组件树中共享数据的方法,而无需手动通过每个级别的 props 传递。你可以创建一个 Context 对象,并使用 <MyContext.Provider> 在组件树中的某个位置提供数据。然后,任何子组件(无论多深)都可以使用 < MyContext.Consumer > 或 useContext Hook 来访问这些数据。 - Redux:
Redux 是一个流行的状态管理库,它允许你在应用程序的任何地方管理和访问状态。通过使用 Redux,你可以创建一个全局可访问的 store 来存储应用程序的状态,并使用 connect 函数或 useSelector 和 useDispatch Hooks 将组件与 store 连接起来。 - props React Hooks(如 useState 和 useReducer)与自定义 Hooks:
对于较简单的跨级通信,你可以使用 useState 和 useReducer Hooks 在父组件中管理状态,并通过自定义 Hooks 或 props 将状态和方法传递给子组件。自定义 Hooks 允许你封装和重用状态逻辑。 - MobX:
MobX 是另一个状态管理库,它提供了一种更响应式的方法来管理和更新应用程序的状态。MobX 鼓励使用可观察的对象和反应机制来自动更新 UI。 - 事件总线(Event Bus)或发布 - 订阅模式:
你可以实现一个简单的事件总线或使用现有的库(如 mitt、tiny-emitter 等),允许组件订阅事件并在事件发生时接收通知。这种方法在需要解耦组件时特别有用。 - 全局状态容器(如 window 对象):
虽然不推荐作为常规做法,但有时你可以将状态附加到全局对象(如 window)上以实现跨级通信。这种方法应该谨慎使用,因为它可能导致难以追踪的状态更新和潜在的命名冲突。 - 父组件回调:
通过父组件向子组件传递回调函数,子组件可以在需要时调用这些函数来通知父组件状态的变化。这种方法适用于较简单的场景和较浅的组件层次结构。 - 使用第三方库:
除了 Redux 和 MobX 之外,还有其他一些第三方库可以帮助实现跨级组件通信,如 reactn、unstated、zustand 等。这些库提供了不同的抽象和机制来处理状态管理。
父组件获取子组件的状态和方法
- 回调函数
- 使用 refs
1、通常建议遵循 React 数据流向单向数据绑定的原则,尽量避免直接访问子组件的状态。
2、使用回调函数是一种更符合 React 设计理念的方式,它促进了组件之间的解耦和可复用性。
3、Refs 主要用于获取 DOM 节点或在必要时获取子组件实例进行一些特殊操作,而不鼓励常规情况下频繁获取子组件的状态。
17. hook 的跨级组件的通信方式
Context API with useContext Hook
- 顶层组件创建 createContext 包裹组件,子组件使用
- useContext
useReducer with useContext
- 首先在顶层组件(或接近顶层的位置)创建一个 Context,并通过 useReducer 初始化和管理状态。然后将 useReducer 返回的状态和 dispatch 方法作为值放入 Context 中。这样一来,任何需要访问或修改这个状态的子组件,不管它们在层级结构中的位置有多深,只要通过 useContext 消费这个 Context,就能直接获取到状态和 dispatch 方法,实现跨层级的状态共享和更新
Redux or MobX with their respective Hooks 如果你的应用规模较大,可以使用 Redux 或 MobX 配合它们的 React Hooks(如 useSelector、useDispatch(Redux Toolkit)或 useObservable、useComputed(MobX))来进行跨级甚至全局范围内的状态管理。
自定义 Hook + Context 你可以创建一个自定义 Hook,封装对 Context 的消费,然后在任何层级的组件中使用这个自定义 Hook。
事件总线(Event Bus) 使用第三方库(如 pubsub-js)或者自己实现一个简单的事件发布订阅系统,通过发布和订阅事件来实现跨级通信。但在 React 中,这种方法相比 Context API 等更为低效且不易于维护,故不太推荐。
祖先组件作为中介 如果组件层次结构相对清晰,可以向上寻找共同的祖先组件作为通信的媒介,祖先组件可以使用 useReducer、useState 等方式维护共享状态,再通过 props 向下传递给需要通信的子组件。
18. hook 模拟生命周期
类组件生命周期方法 | 对应的 Hooks 功能 |
| N/A(直接在函数组件中初始化状态即可,如使用 初始化状态) |
|
(传入一个空数组作为依赖项,表示在组件挂载后执行一次) |
|
(传入依赖项数组,当这些依赖项变化时执行) |
|
返回的清理函数(在组件卸载前执行清理操作) |
|
(对于函数组件,用于优化不必要的渲染) 的依赖数组中精确定义需要监听的变化 |
| 避免在函数组件中使用派生状态。推荐使用 或 ,并在 中根据 props 更新 state。 |
补充说明:
- 对于
componentDidUpdate
,React Hooks 并没有直接对应的 Hook 来模拟,但可以通过在useEffect
的依赖数组中声明需要观察的 props 或 state 变量来实现类似的效果。 - 若要模拟类组件生命周期中的状态更新逻辑,可以结合
useState
、useReducer
以及useEffect
等 Hooks 来实现。
19. react 的批处理
在最新版本的 React 18 中,批处理机制得到了进一步增强和改进。批处理主要是指 React 能够有效地合并多个连续的状态更新,从而减少不必要的渲染次数。
在 React 17 及以前版本中,React 已经实现了半自动的批处理,即在 React 事件处理程序内部发生的多个setState
调用会被合并在一起,并且在事件循环结束前一次性执行这些更新,而非分别触发多次渲染。
React 18 引入了更全面的自动批处理机制,其中的核心改变在于:
- 自动批处理(Automatic Batching):
- 在 React 18 中,批处理不仅限于事件处理程序,而是扩展到了整个 React 的工作流程中。这意味着,任何异步上下文中(例如 Promise 回调、setTimeout 回调等)发起的多个状态更新请求都会被自动合并,即使它们不在同一个事件处理器中。
- Concurrent Mode 下的批处理:
- React 18 引入了 Concurrent Mode,其中的批处理变得更加智能,它可以更好地利用浏览器的异步能力,将多个更新放入微任务队列进行合并,然后在一个批次中执行,从而大大减少了视觉层面上不必要的界面刷新。
createRoot
API:
- 要启用 React 18 的全部批处理功能,推荐使用
ReactDOM.createRoot()
方法替换原有的ReactDOM.render()
方法来挂载根组件。这样做是为了兼容新的批处理逻辑以及并发渲染特性。
- 要启用 React 18 的全部批处理功能,推荐使用
- 新增的过渡 API:
- React 18 还提供了新的 API 如
startTransition
,允许开发者标记某个状态更新为可延迟的,这将进一步帮助 React 进行有效的批处理和优化渲染过程,特别是对于那些不影响 UI 关键路径的状态变更。
- React 18 还提供了新的 API 如
总之,React 18 通过更加精细和广泛的批处理机制,增强了其性能优化能力,使得应用程序能够在状态频繁变动的情况下仍然保持高效和流畅的用户体验。
20. setState 到页面重新渲染经历了什么
setState
方法在 React 中调用后,直到页面重新渲染之间经历了一系列的步骤。以下是这个过程的详细解释:
- 状态合并:
- 当调用
setState
时,React 不会立即改变组件的状态,而是将传入的新状态对象与当前状态进行合并。合并通常是浅层合并,这意味着如果新状态包含深层次的对象属性更改,那么只有第一层属性会合并,深层对象的更改可能不会生效,除非显式替换整个深层对象。
- 当调用
- 异步处理:
- React 将
setState
操作视为异步的,特别是当在事件处理器或生命周期方法中调用时。这意味着调用setState
并不会立即导致重新渲染。实际上,React 可能会把多个连续的setState
调用合并成一个,以减少不必要的渲染次数。
- React 将
- 批处理:
- React 有一个批处理过程,它可以收集在一个事件循环 tick 内的多个
setState
调用,然后一次性去更新状态。这样有助于在高并发更新时优化性能。
- React 有一个批处理过程,它可以收集在一个事件循环 tick 内的多个
- 状态更新调度:
- React 会在微任务队列中安排状态更新的任务,这通常发生在事件处理结束、生命周期钩子调用之后,或者在某些异步操作(如网络请求完成)之后。
- 组件生命周期方法:
- 在状态更新前,React 可能会触发
componentWillReceiveProps
(在旧版 React 中)或getDerivedStateFromProps
(在 React 16.3+ 版本中),接着是shouldComponentUpdate
(如果有实现的话)来决定是否需要继续渲染。
- 在状态更新前,React 可能会触发
- 重新渲染决策:
- 如果根据新的 props 或 state,React 确定组件需要重新渲染,它会进入
render
方法来生成新的虚拟 DOM 树。
- 如果根据新的 props 或 state,React 确定组件需要重新渲染,它会进入
- 虚拟 DOM 比较:
- 新的虚拟 DOM 与旧的虚拟 DOM 进行对比,找出最小化的差异(diffing 算法),确定哪些实际 DOM 节点需要更新、添加或删除。
- DOM 更新:
- 根据 diff 结果,React 更新实际的 DOM 树,仅做必要的改动。
- 生命周期方法调用:
- 在 DOM 更新完成后,React 会触发
getSnapshotBeforeUpdate
(如果有的话)来抓取更新前后的状态差异,随后触发componentDidUpdate
生命周期方法,这时组件已经反映出了新的状态和 UI。
- 在 DOM 更新完成后,React 会触发
21. React 事件代理原理
- 事件绑定在顶级容器:React 在组件的最外层容器(比如在 ReactDOM.render() 方法挂载的 DOM 节点上)绑定一个单一的事件监听器,而不是为每个子组件分别绑定。
- 合成事件系统:React 创建了自己的合成事件(SyntheticEvent),它是对浏览器原生事件的一层抽象,无论底层浏览器如何,合成事件的行为都是一致的。当用户交互发生时,相应的原生事件会冒泡到顶层。
- 事件分发:当事件冒泡到顶层的事件监听器时,React 根据事件的目标(event.target)来判断哪个组件应当处理这个事件,并调用相应组件上绑定的事件处理器。
- 映射关系:React 维护了一种映射关系,记录了每个组件内部定义的事件处理器与其对应的实际 DOM 节点之间的联系。当合成事件触发时,React 能够准确地找到应当处理这个事件的组件实例,并调用相应的处理函数
- React 16 之前,事件代理确实是在 document 级别进行的,这样做的确有可能引起全局事件冲突和性能问题。
- React 17 引入了一个重要的改进,它改变了事件绑定的位置。React 17 开始,事件监听器会直接附加到 React 组件挂载的 DOM 节点上,而不是像以前那样固定在 document 上。这样可以避免与外部库或其他独立于 React 的事件监听器产生冲突,同时减少不必要的全局事件监听。
22. React.PureComponent 和 React.memo
React.PureComponent
应用场景:React.PureComponent 是一个内置的类组件,它继承自 React.Component,并且重写了 shouldComponentUpdate 生命周期方法,用于决定何时需要更新组件。
原理:shouldComponentUpdate(nextProps, nextState) 方法默认执行浅比较(shallow comparison):比较当前组件的 props 和 state 与即将接收到的新 props 和 state 是否相等。如果两者完全相等(浅比较下引用不变或值类型相等),则返回 false,阻止组件进行重新渲染;否则返回 true,组件将会进行正常渲染。
注意:浅比较意味着对于复杂的数据结构(如嵌套对象或数组),如果内部的值发生了变化但是引用地址没变,PureComponent 无法检测到这些变化,会导致组件不会更新。
React.memo
应用场景:React.memo 是一个高阶组件(HOC),专门用于优化函数组件的性能,相当于函数组件版本的 PureComponent。
原理:React.memo 会包裹提供的函数组件,并为其添加一层优化机制。当组件的 props 发生变化时,React.memo 也会进行类似的浅比较。
默认情况下,React.memo 会比较前后两次传递给组件的 props 对象是否相等,如果不相等,则重新渲染组件;如果相等,则跳过渲染。
自定义比较函数:React.memo 允许传入第二个参数作为自定义的比较函数,这个函数接收新旧两个 props 对象作为参数,由开发者自行决定是否应该触发组件重新渲染。
React.PureComponent 适用于类组件,而 React.memo 适用于函数组件,它们通过浅比较来决定组件是否需要重新渲染,从而达到性能优化的目的。但请注意,这两种方式都不适用于含有深层嵌套数据结构或依赖内部状态变更的组件优化。在这种情况下,应手动进行深比较或者使用更高级别的优化手段。
23. 触发 React 重新渲染
- 状态变化(State Update):
- 当组件内部调用 setState() 方法更新状态时,React 会触发该组件及其后代组件的重新渲染流程,除非 shouldComponentUpdate()、getDerivedStateFromProps() 或者 useEffect() Hook 中指定了特殊的优化条件。
- Props 变化(Props Change):
- 当父组件向子组件传递的 props 发生变化时,子组件会接收到新的 props,进而触发重新渲染。
- Context 变化(Context Update):
- 如果组件订阅了某个 Context,当 Context 的值发生变化时,相关组件也会重新渲染。
- Force Update:
- 使用 forceUpdate() 方法会强制组件重新渲染,不论其状态或 props 是否发生变化。这是一种特殊情况,通常不推荐使用,因为它绕过了 React 的优化机制。
- React.memo & PureComponent:
- 虽然 React.memo 和 React.PureComponent 旨在防止不必要的渲染,但如果它们的 props 比较得出的结果是不相等的(对于 React.memo 可以自定义比较函数),那么包裹的组件也会重新渲染。
- useState() Hook 更新:
- 在函数组件中使用 useState() Hook 更新状态时,同样会引起组件重新渲染。
- UseReducer Dispatch:
- 使用 useReducer() Hook 时,dispatch 动作后 reducer 返回新的 state 会触发组件重新渲染。
- 生命周期钩子:
- 在某些生命周期方法中进行状态更改,如在 componentDidMount、componentDidUpdate 或 useEffect 中的异步操作完成后。
- 自定义 Hook 引起的副作用:
- 在自定义 Hook 内部使用 useState、useReducer 或 useContext 等 Hook 更新状态,同样会触发使用了这些自定义 Hook 的组件重新渲染。
- Web API 事件:
- 例如,当组件监听到浏览器的某些事件(如窗口大小变化、存储事件等),并在事件处理函数中做了状态更新,这也可能引发组件重新渲染。
24. 能 render 时访问 refs 吗
不能。这是因为 refs 是在组件挂载到 DOM 之后填充的,并且在 render() 执行期间,React 还没有完成这个过程。
refs 会在组件渲染完成后才被赋值,即在组件生命周期的某个阶段(比如 componentDidMount 或 getDerivedStateFromProps(已弃用)后的一个回调钩子中)才能保证已经被正确设置并指向真实的 DOM 节点或组件实例。
因此,如果你尝试在 render() 方法内部访问 ref,它可能会是 null 或未定义的,因为在那个时刻,React 还没有机会将其关联到实际的 DOM 元素或组件实例上。
正确的做法是在生命周期方法中或者其他合适的时机(例如事件处理函数中)访问 refs。例如,在 componentDidMount 或 useEffect Hook(对于函数组件)中,可以安全地通过 this.refName.current(对于类组件)或 useRef Hook 返回的 .current 属性(对于函数组件)来访问 refs 指向的内容。
25. 性能优化在哪个生命周期?原理?
React 中进行性能优化的关键生命周期方法是 shouldComponentUpdate(nextProps, nextState),这是在类组件中可用的一个生命周期方法。另外,在函数组件中,可以通过 React.memo 或者 useMemo、useCallback Hooks 来实现相似的效果。
shouldComponentUpdate(nextProps, nextState)****:
- 这个方法在组件接收到新的 props 或 state 更新后,但在实际渲染前被调用。
- 默认情况下,React 组件在接收到新的 props 或 state 后都会重新渲染。然而,如果组件的渲染成本较高或者状态变化并不影响视图,我们可以通过重写 shouldComponentUpdate 方法,根据 nextProps 和 nextState 与当前的 props 和 state 进行比较,如果两者相同,则返回 false,阻止不必要的渲染,从而优化性能。
优化原理:
- shouldComponentUpdate 的优化原理是基于组件的局部更新理念。通过检查组件是否真的需要因 props 或 state 的变化而更新视图,避免了对整个组件树的不必要的 diff 过程和 DOM 更新。
- 当返回 false 时,React 不会进一步递归更新该组件及其子组件,节省了 CPU 计算和浏览器 DOM 操作的资源。
函数组件优化:
- React.memo:用于函数组件的性能优化,它是一个高阶组件,通过记忆最近一次的 props 值,然后在下次渲染时比较新旧 props 是否有变化,如果没有变化就不执行组件的渲染逻辑。
- useMemo:用于缓存昂贵计算结果的 Hook,当依赖项数组中任何一个值发生变化时才会重新计算 memoized 值。
- useCallback:类似于 useMemo,但它用于缓存函数,确保当依赖项没有改变时,返回同一个函数引用,从而避免不必要的子组件重新渲染。
26. React-Router 的实现原理是什么
基于 hash 的路由:通过监听 hashchange 事件,感知 hash 的变化
- 改变 hash 可以直接通过 location.hash=xxx
基于 H5 history 路由:
- 改变 url 可以通过 history.pushState 和 resplaceState 等,会将 URL 压入堆栈,同时能够应用 history.go() 等 API
- 监听 url 的变化可以通过自定义事件触发实现
基于 Hash 的路由(Hash-Based Routing)
优点:
- 兼容性良好:几乎在所有浏览器中都可以使用,即使是老旧的浏览器,包括不支持 HTML5 History API 的浏览器。
- 无需服务器配置:由于 Hash 改变并不会触发页面刷新,所以浏览器不会向服务器发送新的请求,这就意味着无需对服务器进行任何特定配置以支持前端路由。
缺点:
- URL 不够美观:URL 中包含哈希符号(#),例如 http://example.com/#/about,这在一定程度上影响了用户体验和 SEO(搜索引擎优化)。
- 浏览器历史记录管理受限:每个 Hash 变化都被浏览器认为是同一页面的不同状态,所以在浏览器的历史记录栈中会产生大量条目,不利于用户导航回退。
- 无法进行真正的网页跳转:由于 hash 变化不会触发页面加载,所以不能通过改变 hash 实现真正的页面跳转和前进后退功能,需要借助 onhashchange 事件自行管理。
基于 HTML5 History 的路由(History-Based Routing)
优点:
- 美观的 URL:能够提供更直观、更友好的 URL,例如 http://example.com/about,这种 URL 看起来更像是传统 web 应用的链接。
- 更好的用户体验:可以利用浏览器的前进、后退按钮进行导航,且不会在历史记录中产生过多条目。
- 有利于 SEO:对于需要搜索引擎优化的应用,History API 可以让每个路由映射到唯一的真实 URL,便于搜索引擎索引和理解页面结构。
缺点:
- 兼容性限制:需要现代浏览器支持 HTML5 History API,老版本浏览器(如 IE9 及以下版本)可能不支持。
- 需要服务器支持:因为切换路由时会改变 URL 路径,当用户直接访问这些路径或刷新页面时,服务器需要配置重定向或服务端渲染以避免 404 错误。
- 实现相对复杂:相较于基于 Hash 的路由,History API 的实现稍微复杂一些,需要更多的代码来处理浏览器的路由变化和页面跳转。
27. react 中怎么实现重定向
重定向(Redirect)在 Web 开发中指的是将用户的请求从一个 URL 转向另一个 URL 的行为,通常是当用户访问某个页面时,服务器或前端框架决定将用户引导至另一个页面。
- 使用 Redirect 组件(适用于 React Router v4 及更高版本): 在一个路由配置或组件内部使用 Redirect 组件,当组件渲染时,它会立刻执行重定向。
- 编程式重定向: 通过 history 对象或者 useHistory Hook,可以在组件内部进行动态重定向。
- 在路由配置中实现重定向: 在设置路由规则时,可以直接配置重定向规则
28. react-router 里的 Link 标签和 a 标签的区别
HTML a 标签:
- 原生 HTML 元素,点击后会触发浏览器的默认行为,即发送一个新的 HTTP 请求到指定的 URL,页面会发生整页刷新。
- 通过 href 属性指定目标 URL。
React Router 标签:
是 React Router 提供的一个组件,专为 SPA(Single Page Applications)设计。
点击后不会导致整页刷新,而是通过 React Router 的内部机制来改变当前应用的状态,只重新渲染需要更新的部分,实现页面局部刷新。
通过 to 属性指定目标路由,而不是 URL,可以是绝对路径或相对路径。
- 标签同样支持激活样式(activeClassName、activeStyle)等功能,可根据当前路由是否匹配来添加特定样式。
a 标签用于传统的、页面间全量加载的导航。
Link 标签则是为 React Router 中的路由导航设计,用于在 SPA 中实现无刷新的页面跳转,能够更好地整合到 React 应用的客户端路由系统中。
29. React-Router 4 的 Switch 有什么用?
React Router 4 中的 Switch 组件用于渲染一组 Route 组件,它会遍历其子 Route 组件并依次检查它们是否与当前的 location 匹配。重点在于,Switch 会确保只渲染第一个匹配路径的 Route,一旦找到匹配的 Route,它会停止遍历剩余的 Route 子组件。
这样做的好处是可以避免在多个路径同时匹配时,多个组件同时被渲染的情况,确保任何时候路由匹配时只有一个组件是活跃并渲染出来的。这对于构建更有序和明确的路由逻辑非常有用,尤其是在有嵌套路由或多分支路由的情况下,能够确保页面展现的正确性和一致性
30. useEffect 与 useLayoutEffect 的区别
useEffect
和 useLayoutEffect
都是 React 中的 Hook,用于处理副作用,例如订阅事件、执行异步操作、更新 DOM 属性等。它们的主要区别在于执行时机和浏览器渲染流水线的影响:
- 执行时机:
**useEffect**
:在所有的 DOM 变更已经完成并同步到浏览器界面之后,浏览器的事件循环下一次迭代中异步执行。这意味着在**useEffect**
里的代码运行时,用户已经可以看到渲染的结果。useLayoutEffect
:在所有的 DOM 变更完成后立即同步执行,紧接在 DOM 更新之前。这意味着useLayoutEffect
内部的代码更改会影响到当前帧的渲染结果,因此在它执行期间,浏览器会暂停布局和绘制工作,等待同步代码执行完毕。
- 案例:树状组件渲染完后修补对齐线
- div 宽高改变时
- 使用 useLayoutEffect 要特别谨慎,因为如果执行的副作用中有耗时操作,可能会导致页面的 “卡顿”。
- 对渲染性能的影响:
useEffect
:由于其异步执行的特性,不会阻塞浏览器的渲染流程,所以不会引起明显的卡顿或布局闪烁。useLayoutEffect
:由于其同步执行,若在其回调函数中修改了样式或其他影响布局的属性,将会强制浏览器重新计算布局,这可能造成性能上的影响,特别是在循环或大量元素上进行同步更新时。
- 何时使用:
useEffect
:适用于大部分副作用场景,尤其是涉及网络请求、设置定时器、订阅事件等不直接影响布局的任务。useLayoutEffect
:在那些需要在渲染后立即操作 DOM 并确保 DOM 更新与之同步的场景中使用,比如某些动画效果、依赖 DOM 尺寸变动的操作,或者为了防止 UI 闪烁而需要在绘制前调整样式的情况。
总结来说,useEffect
更适合非阻塞渲染管线的异步操作,而 useLayoutEffect
则用于那些需要严格保证在渲染完成前执行,并可能影响布局的操作。在大部分情况下,优先考虑使用 useEffect
;只有在特定场景下,当确实需要同步更新并避免布局抖动时,才应该选择 useLayoutEffect
。
31. Hooks 需要注意的问题
(1)不要在循环,条件或嵌套函数中调用 Hook,必须始终在 React 函数的顶层使用 Hook
(2)使用 useState 时候,使用 push,pop,splice 等直接更改数组对象的坑
(3)useState 设置状态的时候,只有第一次生效,后期需要更新状态,必须通过 useEffect
(4)善用 useCallback
32. useState 实现原理
以下是useState
在 React 中的底层实现原理的大致步骤概述:
- 注册状态:
当 React 在渲染函数组件的过程中遇到useState(initialState)
时,它会在组件实例的内部创建一个新的状态槽(slot),并将initialState
作为初始值存入该槽位。由于函数组件没有实例的概念,React 通过 Fiber 节点来模拟实例行为,所以实际上是给对应的 Fiber 节点添加了状态。 - 状态钩子的创建:
React 会返回一个状态对——当前状态值和更新函数。状态值就是从状态槽中读取的初始值;更新函数则是由 React 自动生成的一个闭包函数,它能够找到正确的状态槽并更新其值。 - 状态更新:
当调用setState(newState)
时,React 不会立即修改状态,而是创建一个更新任务,将其加入到一个优先级队列中。这是因为 React 采用了异步更新策略,以批量处理多个状态更新,提高性能。 - 调度过程:
调度器(Reconciler/Renderer)会按照一定的优先级顺序处理队列中的更新任务。当轮到处理某个组件的状态更新时,调度器会查看新旧状态是否不同,如果不同,则标记该组件及其子孙组件为需要重新渲染。 - 渲染阶段:
在渲染阶段,React 会重新执行函数组件体以获取最新的 UI 描述(虚拟 DOM)。此时,useState
会返回上一次渲染时的状态值,但如果在本次渲染周期中有待处理的更新,则会返回已更新的状态值。 - 差异比较与 DOM 更新:
React 使用虚拟 DOM diff 算法找出前后两次渲染之间的差异,然后只对实际 DOM 进行必要的更新操作。 - 闭包的作用:
setState
函数通过闭包绑定到了正确的组件实例(即 Fiber 节点),这样在任何地方调用这个函数都能够准确地修改相应组件的状态,而不影响其他组件。
综上所述,useState
在 React 的底层是通过一套复杂但高效的系统实现的,包括状态槽的管理、更新任务的调度、异步更新机制、以及闭包的应用等技术手段,从而使得函数组件也能具备维护自身状态的能力。
fiber 角度下的 useState
在 Fiber 架构下,React 为每个函数组件实例(即 Fiber 节点)内部维护了一个状态对象。当调用 useState(initialState) 时:
- 状态创建: React 为当前渲染的函数组件 Fiber 节点创建一个状态槽,并将传入的 initialState 保存在这个槽位中。
- 状态访问与更新: useState 返回一对值:当前状态和一个更新状态的函数。这个更新函数通过闭包捕获了与当前 Fiber 节点关联的状态槽,因此可以准确地定位和更新状态。
- 调度与更新: 当调用 setState(newState) 时,React 并不会立即更新状态和重新渲染组件,而是将更新操作添加到一个优先级队列中。React Fiber 调度器会依据任务优先级决定何时执行这些更新。在重新渲染过程中,Fiber 节点的状态会根据 useState 返回的新状态值进行更新。
- 记忆化与回收: React 在处理 Fiber 树时,会利用记忆化机制,确保状态和副作用函数与正确的 Fiber 节点关联,并在组件卸载时正确地清理资源。
因此,useState 能够在函数组件中管理状态,得益于 React Fiber 架构对其内部状态管理机制的改进和增强。Fiber 不仅负责调度和更新组件,还提供了在函数组件中存储和更新状态的能力。
33. useEffect 实现
useEffect
是 React Hooks 中的一个重要 Hook,用于处理副作用、订阅数据源和执行 DOM 操作等。以下是useEffect
在 React 中的底层实现原理的大致步骤:
- 注册效应函数:
当 React 在渲染过程中遇到useEffect(callback, deps)
时,它会捕获当前作用域下的callback
函数(副作用函数)以及依赖数组deps
。每次组件渲染完毕后,React 都会检查是否存在已经注册过的效应函数,并且比较新的依赖项列表与旧的是否发生变化。 - 首次渲染时执行:
在组件的首次渲染完成后,React 会立刻执行useEffect
中传入的回调函数,不论依赖项数组是否为空。这相当于 “挂载” 阶段执行的生命周期方法如componentDidMount
。 - 依赖变化时执行:
在后续的渲染周期中,React 会对比新旧依赖项数组。如果数组内容发生了变化(例如引用变更,或者数组元素内容有变),React 就会认为依赖关系有所更新,并触发新的副作用函数执行。这对应于类组件中的componentDidUpdate
生命周期方法。 - 清理工作:
如果在副作用函数中返回了一个清理函数(比如取消定时器、解绑事件监听器等),那么在下一次副作用函数执行前,React 会确保先调用上一次的清理函数,保证资源的有效释放。这相当于类组件中的componentWillUnmount
生命周期方法。 - 优化选项:
可以通过传递空数组[]
作为useEffect
的第二个参数,告诉 React 这个副作用函数只在组件挂载和卸载时运行,不依赖任何 props 或 state 的变化。这有助于避免不必要的重复执行。
useEffect
Hook 的工作机制基于渲染周期后的回调执行和依赖项变化的追踪,利用这种机制,React 能够在函数组件中有效地管理和调度各种副作用操作,保持组件逻辑清晰的同时,也实现了与类组件生命周期方法类似的灵活性和控制力。
- 任务调度: 在 React Fiber 架构下,所有的更新任务(包括 useEffect 回调函数的执行)都被视为可中断和可恢复的工作单元。当组件渲染完成后,React Fiber 会根据优先级调度执行 useEffect 回调函数。这意味着即使在组件树的深度遍历过程中,也可以暂停当前的工作并转而去处理更高优先级的任务,如处理浏览器事件或进行渲染。
- 依赖收集与更新: 当 useEffect 被调用时,React 会记录下其依赖项数组。在后续的渲染周期中,React 通过 Fiber 结构跟踪和比较依赖项的变化,决定是否需要重新执行 useEffect 回调函数。Fiber 节点存储了组件的状态信息和副作用信息,这是决定何时执行 useEffect 的关键。
- 清理工作与调度: useEffect 回调函数可以返回一个清理函数,用于在组件卸载或 useEffect 更新前执行清理操作。React Fiber 在调度任务时会管理这些清理函数的执行,确保资源的释放和状态的一致性。
- 并发模式支持: Fiber 架构支持 React 的并发模式(Concurrent Mode),在这种模式下,useEffect 会更加智能地配合调度系统,适应可暂停、可恢复的渲染过程,使得多个 useEffect 可以并发执行,从而提高应用的整体性能和响应性。
34. 为什么不能不要滥用 useContext
useContext
是 React 提供的一个 Hook,它允许组件无需通过 props 层层传递就能直接访问 Context 中的数据。然而,在使用过程中需要注意适度,避免滥用的原因主要有以下几点:
- 过度耦合:过多地使用
useContext
可能导致各个组件与全局状态高度耦合。即使这些组件原本不需要关心全局状态,也可能因为直接消费 Context 而导致维护性和可读性降低,同时也增加了组件重构和复用的难度。 - 难以追踪依赖:当多个组件都通过
useContext
访问同一个 Context 时,一旦这个全局状态发生改变,所有依赖它的组件都有可能被迫重新渲染,即便它们并不关心这次状态变化的具体内容。这可能导致不必要的性能损失。 - 架构复杂度:随着应用规模扩大,如果随意创建和使用 Context,会导致全局状态管理变得混乱且难以维护。合理的做法通常是集中管理和划分不同作用域的 Context,而不是处处使用。
- 不利于逻辑解耦:过度依赖全局上下文可能导致业务逻辑难以拆分和独立管理。理想的做法是将相关性强的状态和逻辑封装在自包含的组件或 Redux 等状态管理库中,保持每个模块的内聚性。
因此,在实际开发中,应当审慎地使用 useContext
,只在必要的全局共享状态场景下使用,并结合其他最佳实践(如 Redux、 Recoil、MobX 等)以及 React 的其他特性(如 useReducer
、useState
等),合理规划和组织应用的状态管理结构。
35. react 最新版本不同
React 16.x 的三大新特性 Time Slicing、Suspense、 hooks
- Time Slicing 萨拉森(解决 CPU 速度问题)使得在执行任务的期间可以随时暂停,跑去干别的事情,这个特性使得 react 能在性能极其差的机器跑时,仍然保持有良好的性能
- **Suspense 瑟斯半死(解决网络 IO 问题)**和 lazy 配合,实现异步加载组件。 能暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从 react 出生到现在都存在的「异步副作用」的问题,而且解决得非的优雅,使用的是 T 异步但是同步的写法,这是最好的解决异步问题的方式
- 提供了一个内置函数 componentDidCatch,当有错误发生时,可以友好地展示 fallback 组件; 可以捕捉到它的子元素(包括嵌套子元素)抛出的异常; 可以复用错误组件。
(1)React16.8
加入 hooks,让 React 函数式组件更加灵活,hooks 之前,React 存在很多问题:
- 在组件间复用状态逻辑很难
- 复杂组件变得难以理解,高阶组件和函数组件的嵌套过深。
- class 组件的 this 指向问题
- 难以记忆的生命周期
hooks 很好的解决了上述问题,hooks 提供了很多方法
- useState 返回有状态值,以及更新这个状态值的函数
- useEffect 接受包含命令式,可能有副作用代码的函数。
- useContext 接受上下文对象(从 React.createContext 返回的值)并返回当前上下文值,
- useReducer useState 的替代方案。接受类型为 (state,action)=> newState 的 reducer,并返回与 dispatch 方法配对的当前状态。
- useCalLback 返回一个回忆的 memoized 版本,该版本仅在其中一个输入发生更改时才会更改。纯函数的输入输出确定性 o useMemo 纯的一个记忆函数 o useRef 返回一个可变的 ref 对象,其 Current 属性被初始化为传递的参数,返回的 ref 对象在组件的整个生命周期内保持不变。
- useImperativeMethods 自定义使用 ref 时公开给父组件的实例值
- useMutationEffect 更新兄弟组件之前,它在 React 执行其 DOM 改变的同一阶段同步触发
- useLayoutEffect DOM 改变后同步触发。使用它来从 DOM 读取布局并同步重新渲染
(2)React16.9
- 重命名 Unsafe 的生命周期方法。新的 UNSAFE_前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出
- 废弃 javascrip: 形式的 URL。以 javascript: 开头的 URL 非常容易遭受攻击,造成安全漏洞。
- 废弃 "Factory" 组件。 工厂组件会导致 React 变大且变慢。
- act()也支持异步函数,并且你可以在调用它时使用 await。
- 使用 <React.ProfiLer> 进行性能评估。在较大的应用中追踪性能回归可能会很方便
(3)React16.13.0
- 支持在渲染期间调用 setState,但仅适用于同一组件
- 可检测冲突的样式规则并记录警告
- 废弃 unstable_createPortal,使用 CreatePortal
- 将组件堆栈添加到其开发警告中,使开发人员能够隔离 bug 并调试其程序,这可以清楚地说明问题所在,并更快地定位和修复错误。
36. react 中状态提升是什么?使用场景有哪些?
在 React 中,状态提升(State Lifting)是一种管理状态(state)的设计模式,它是指当多个组件需要共享相同状态时,而不是各自在各自的组件内维护独立的状态副本,应该将该状态集中管理,将其移到这些组件的最近共同父组件(或创建一个新的容器组件专门用来托管此状态)中去维护。这样,父组件可以通过 props 向下传递状态以及更新状态的方法给子组件,子组件通过调用这些方法通知父组件来间接更改和同步状态,从而保证所有依赖此状态的子组件能够得到及时和一致的更新。
使用场景举例:
- 表单联动:例如有两个相关的输入框,当在一个输入框中输入内容时,另一个输入框的内容需要同步更新,这时就可以将这两个输入框的状态提升到它们的父组件中统一管理。
- 全局应用状态:比如导航栏的显示 / 隐藏状态、主题颜色、登录状态等需要在整个应用范围内共享的状态,这些状态通常会被提升到顶层组件(如 App 组件)中统一管理。
- 列表项的选择状态:在一个列表组件中有多个可勾选的子项,这些子项的选择状态需要被集中管理以便进行全选、反选等操作,这时可以把选择状态提升到列表组件中。
- 跨组件通信:当两个没有直接关联的子组件需要基于同一份数据进行操作时,可以通过将这份数据放入它们的公共父组件中来实现在更高层级上的状态控制。
通过状态提升,可以确保状态的一致性和避免组件之间的不必要的直接耦合,符合 React 中单向数据流的原则,使应用程序的数据流更易于理解和调试。
37. react 中页面重新加载时怎样保留数据?
- **Redux:**将页面的数据存储在 redux 中,在重新加载页面时,获取 Redux 中的数据;
- **data.js:**使用 webpack 构建的项目,可以建一个文件,data.js,将数据保存 data.js 中,跳转页面后获取;
- **sessionStorge:**在进入选择地址页面之前,componentWillUnMount 的时候,将数据存储到 sessionStorage 中,每次进入页面判断 sessionStorage 中有没有存储的那个值,有,则读取渲染数据;没有,则说明数据是初始化的状态。返回或进入除了选择地址以外的页面,清掉存储的 sessionStorage,保证下次进入是初始化的数据
- **history API:**History API 的
pushState
函数可以给历史记录关联一个任意的可序列化state
,所以可以在路由push
的时候将当前页面的一些信息存到state
中,下次返回到这个页面的时候就能从state
里面取出离开前的数据重新渲染。react-router 直接可以支持。这个方法适合一些需要临时存储的场景。
38. 没有看到使用 react 却需要引入 react?
本质上来说 JSX 是React.createElement(component, props, ...children)
方法的语法糖。在 React 17 之前,如果使用了 JSX,其实就是在使用 React, babel
会把组件转换为 CreateElement
形式。在 React 17 之后,就不再需要引入,因为 babel
已经可以帮我们自动引入 react。
39. 高阶组件运用了什么设计模式?
使用了装饰模式,高阶组件的运用:
装饰模式的特点是不需要改变 被装饰对象 本身,而只是在外面套一个外壳接口。JavaScript 目前已经有了原生装饰器的提案,其用法如下:
40. useMome 和 react.memo 区别
React.memo():
- 用途: 用于优化函数组件,通过记忆(缓存)组件的渲染结果,当组件的 props 没有发生变化时,避免不必要的重新渲染。
- 实现:React.memo() 是一个高阶组件,用于包装函数组件,返回一个新的记忆化的组件。在组件的渲染时,它会检查当前的 props 是否和上一次渲染时的 props 相同,如果相同,则直接返回上一次的渲染结果。
- 使用场景: 适用于纯函数组件,尤其是当组件渲染开销较大且 props 变化不频繁时。
useMemo():
- 用途: 用于记忆化计算结果,以避免在每次渲染时都重新计算某个值。它可以缓存计算结果,只有在依赖项发生变化时才重新计算。
- 实现:useMemo() 是一个 Hook,接收一个回调函数和一个依赖数组,返回一个记忆化的值。只有当依赖数组中的值发生变化时,才会重新计算值;否则,它会直接返回上一次计算的结果。
- 使用场景: 适用于那些在渲染期间需要进行昂贵计算的值,尤其是在处理复杂的逻辑或计算的情况下。
区别:
- 应用对象不同:
- React.memo() 用于优化整个组件的重新渲染。
- useMemo() 用于优化特定值的计算,而不涉及整个组件的渲染。
- 处理方式不同:
- React.memo() 处理整个组件的 props 是否发生变化。
- useMemo() 处理值的计算和依赖项数组。
- 返回结果不同:
- React.memo() 返回一个记忆化的组件。
- useMemo() 返回一个记忆化的值。
- 应用场景不同:
- React.memo() 适用于整个组件的渲染优化,特别是对于纯函数组件。
- useMemo() 适用于计算昂贵值,以减少不必要的计算开销,特别是在处理复杂逻辑的情况下。
在实际使用中,这两个工具可能会同时使用,以确保组件和值的性能都得到了优化。
41. 事件合成
React 引入了 “合成事件”(SyntheticEvent)的概念,这是一种跨浏览器的事件抽象层,它模拟了 W3C 标准事件行为,并提供了统一的接口给开发者。
- 事件委派:
- React 并不直接将事件处理器绑定到各个 DOM 元素上,而是采用事件委派的策略,将所有的事件监听器绑定到最顶层的容器元素上(在 React 17 之前是 document,17 及以后版本默认绑定在根 DOM 节点 #root 上)。
- 当任何子元素触发事件时,事件会按照浏览器的事件传播机制冒泡到顶层容器,此时 React 的事件监听器会捕获这些事件。
- 事件对象标准化:
- React 创建了自己的事件对象——合成事件,当原生事件发生时,React 会创建一个与原生事件相对应的 SyntheticEvent 对象,并将其传递给相应的事件处理函数。
- SyntheticEvent 对象是对原生事件对象的包装,它具有与原生事件相同的接口和属性,但确保了在各种浏览器下的行为一致性。
- 事件池复用:
- 为了提高性能,React 采用了事件对象池的技术。每次事件触发后,SyntheticEvent 对象会被重置并放回池中以备下次使用,而不是每次触发事件都创建新的对象,从而减少内存分配和垃圾回收的压力。
42. 说下批处理
在 React 中,批处理(Batching)是一种优化策略,主要用于提升 React 组件状态更新时的性能。批处理的核心概念是将一系列连续的状态更新操作合并到一起,然后一次性去触发视图的重新渲染,而非每更新一次状态就立即引起一次渲染。
具体来说:
- 在 React 的早期版本中,每当通过
**setState**
或使用 Hooks 中的**useState**
、**useReducer**
等 API 更改状态时,通常会触发组件及其子组件的重新渲染。但如果短时间内有多个状态更新操作,每个更新都会导致渲染流水线上的额外开销。 - 为了避免这种频繁渲染,React 内部实现了一种机制,能够识别出在单个事件处理器(如 onClick 回调)、生命周期方法(如 componentDidUpdate)或者 React Suspense 上下文中的一系列状态变更,并将它们合并到同一个批处理中。这样,即使在短时间内多次调用
setState
,React 也会等待这些调用全部结束,然后才进行一次实际的组件树比较(reconciliation)和渲染过程。 - 在 React 18 中,批处理得到了进一步增强,尤其是对于异步渲染的支持,推出了并发模式。在这种模式下,React 可以更好地控制何时开始和结束渲染周期,以及如何处理中间状态。例如,React 18 会自动批处理在事件处理器内的状态更新,并且支持 Suspense 边界内的异步状态更新批处理。
- 针对那些需要立即强制同步更新场景,React 18 还引入了
**flushSync**
API,允许开发者手动同步执行状态更新,绕过批处理机制,确保状态变化立即反映到 DOM 中。
总的来说,批处理是 React 用来优化性能的关键手段之一,旨在减少不必要的渲染次数,提高应用的整体响应速度。
43. 介绍一下 React 的 patch 流程 React 的 patch 流程,即 React 的虚拟 DOM(Virtual DOM)更新机制,主要包括以下几个步骤:
- 生成新的虚拟 DOM 树:
当组件的 state 或 props 发生变化时,React 会重新执行render
方法,生成新的虚拟 DOM 树。虚拟 DOM 是一种轻量级的 JavaScript 对象表示,它模仿了实际 DOM 结构。 - 比较(Diffing):
React 使用了一种优化过的算法(通常称为 “reconciliation algorithm” 或“ diff 算法”)来比较新的虚拟 DOM 树与上一次渲染后的虚拟 DOM 树。这个算法并不是简单地对整个 DOM 树进行深比较,而是采取了一些优化策略,如:
- 仅对同级节点进行比较,不同层级的节点变化不会相互影响。
- 对于相同类型的组件,仅比较 props 和 state 的变化。
- 对于列表,通过 key 来高效地识别新增、移除和移动的元素。
- 生成差异对象(Delta):
Diff 算法得出哪些部分需要更新后,会生成一个差异对象,其中包含了需要在实际 DOM 中进行更新的具体操作。 - 执行更新操作(Patching):
React 根据差异对象,对实际 DOM 进行最小化操作。这些操作可能包括:
- 更新元素的属性。
- 插入、删除或移动 DOM 元素。
- 更新文本内容。
- 批处理更新:
React 还会将多个组件的更新操作进行批处理(batching),在事件循环的下一个 tick 统一执行,以提高性能。
在整个 patch 流程中,React 通过高效的 Diff 算法和最小化 DOM 操作策略,最大程度地减少了对实际 DOM 的直接操作,从而提高应用的性能和响应速度。在 React Fiber 架构下,这个流程变得更加灵活,能够支持异步渲染和优先级调度,使得 UI 更新更加平滑和高效。