React 相关知识(2024)一

1. 对 React 的理解、特性

React 是靠数据驱动视图改变的一种框架,它的核心驱动方法就是用其提供的 setState 方法设置 state 中的数据从而驱动存放在内存中的虚拟 DOM 树的更新

更新方法就是通过 React 的 Diff 算法比较旧虚拟 DOM 树和新虚拟 DOM 树之间的 Change ,然后批处理这些改变。

遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效

使用虚拟 DOM 来有效地操作 DOM,遵循从高阶组件到低阶组件的单向数据流

  1. 声明式编程:React采用声明式编程范式,允许开发者描述UI应该呈现的状态,而非具体的操作步骤。这种抽象简化了开发过程,并且有利于React在内部执行高效的DOM更新。
  2. 虚拟DOM:React引入了虚拟DOM的概念,即在内存中维护一个与实际DOM相对应的数据结构。每当组件的状态变化时,React首先更新虚拟DOM,然后通过高效的Diff算法找出最小化的DOM更新操作,再将这些变化反映到实际DOM上,从而极大提升了渲染性能。
  3. 组件化:React强调组件化开发,鼓励将UI拆分成可复用的独立单元。组件具有明确的输入(Props)和内部状态(State),使得代码易于组织和测试。
  4. 单向数据流与可预测性:React推崇单向数据流的设计理念,数据从父组件流向子组件,通过props传递,而子组件通过回调函数通知父组件状态变化,这种设计有助于理解和调试应用状态。
  5. 函数组件与Hooks:随着React 16.8版本引入的Hooks,函数组件获得了状态管理和生命周期功能,使得无需类就能写出完整功能的组件,进一步简化了代码结构,提高了可读性和可复用性。
  6. React Fiber:React 16 引入的调度算法改进,提供了更细粒度的任务划分与优先级调度,增强了应用在复杂场景下的流畅性。
  7. 兼容性与扩展性: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)

返回的函数在组件卸载时执行,用于资源清理

注意:useEffectuseMemouseCallback 的依赖数组可以帮助确定何时重新执行钩子逻辑。

由于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) { ... })
window.addEventListener('error', function(event) { ... })

window.addEventListener('unhandledrejection', function(event) { ... })

5. tsx转换成真实DOM过程

  1. TypeScript 编译阶段
    • .tsx 文件包含了 TypeScript 类型注解和 JSX 语法,首先通过 TypeScript 编译器(如tsc或者配合Babel的@babel/preset-typescript插件)进行编译。
    • TypeScript 编译器负责检查类型注解的正确性,并且把包含类型信息的 TypeScript+JSX 代码转换为普通的 JavaScript 代码,同时保持 JSX 结构不变。
  1. JSX编译阶段
    • 开发者使用JSX编写React组件。
    • JSX并非浏览器原生支持的语法,所以需要通过像Babel这样的编译器将其转换为标准的JavaScript代码。
    • Babel将JSX转换成React.createElement(component, props, …children)的调用形式,其中component是组件名称或原生DOM元素标签名,props是组件属性对象,children是子元素数组。
  1. 创建虚拟DOM(VDOM)
    • React.createElement()调用会产生一个虚拟DOM节点对象,它是一个轻量级的JavaScript对象,模拟了DOM节点的结构和属性。
    • 整个组件树会转换成由这些虚拟DOM节点构成的虚拟DOM树。
  1. 渲染虚拟DOM
    • 当调用ReactDOM.render()方法时,React接收虚拟DOM树作为参数。
    • React会将这个虚拟DOM树与实际DOM进行比较(首次渲染时不存在比较,直接创建)。
  1. DIFF算法与更新DOM
    • 在组件状态或props更改导致重新渲染时,React会生成一个新的虚拟DOM树,并与旧的虚拟DOM树进行高效差异比较(称为Reconciliation或Diffing过程)。
    • Diff算法找出最小化的DOM操作集合(增删改查节点)。
    • React随后将这些操作应用到实际的DOM上,仅更新需要改变的部分,而不是完全替换整个DOM树。
  1. 真实DOM更新
    • 最终,React通过ReactDOM模块与浏览器底层交互,执行必要的DOM操作,将虚拟DOM树的改动反映到浏览器的真实DOM中