赞
踩
React 是靠数据驱动视图改变的一种框架,它的核心驱动方法就是用其提供的 setState 方法设置 state 中的数据从而驱动存放在内存中的虚拟 DOM 树的更新
更新方法就是通过 React 的 Diff 算法比较旧虚拟 DOM 树和新虚拟 DOM 树之间的 Change ,然后批处理这些改变。
遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效
使用虚拟 DOM 来有效地操作 DOM,遵循从高阶组件到低阶组件的单向数据流
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
React 中进行性能优化的手段可以从多个维度进行分类,以下是一些关键类别及其对应的优化策略:
1. 组件优化
React.PureComponent
或者对其包装一层React.memo
,它们都能通过浅比较props来避免不必要的重新渲染。shouldComponentUpdate
生命周期方法来手动控制是否更新组件。在函数组件中,使用useMemo
缓存计算结果,useCallback
缓存回调函数,防止因依赖项不变而引起的无效渲染。2. 状态管理与变更
useState
hook时,可以利用函数式的setState来一次性更新多个状态值。3. Virtual DOM与Diff算法优化
4. 事件处理优化
useCallback
来缓存函数引用。5. 懒加载与代码分割
6. 优化渲染过程
7、工具辅助
8、前端通用优化
错误边界(Error Boundaries):
除此之外还可以通过window.onerror或unhandledrejection事件监听器在全局范围内捕获未处理的错误。
- window.addEventListener('error', function(event) { ... })
- window.addEventListener('unhandledrejection', function(event) { ... })
Fiber 即是React新的调度算法
在数据更新时,react生成了一棵更大的虚拟dom树,给第二步的diff带来了很大压力——我们想找到真正变化的部分,这需要花费更长的时间。js占据主线程去做比较,渲染线程便无法做其他工作,用户的交互得不到响应,所以便出现了react fiber。
React 为了解决这个问题,根据浏览器的每一帧执行的特性,构思出了 Fiber 来将一次任务拆解成单元,以划分时间片的方式,按照Fiber的自己的调度方法,根据任务单元优先级,分批处理或吊起任务,将一次更新分散在多次时间片中,另外, 在浏览器空闲的时候, 也可以继续去执行未完成的任务, 充分利用浏览器每一帧的工作特性。
一次更新任务是分时间片执行的,直至完成某次更新。
这样 React 更新任务就只能在规定时间内占用浏览器线程了, 如果说在这个时候用户有和浏览器的页面交互,浏览器也是可以及时获取到交互内容。
Fiber架构出现之前 react 存在的问题
jsx 形成 dom 的以下几个步骤,react 在虚拟 dom 之前做了一层数据结构设计将ReactElement 转换为 fiberNode
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会复用当前值
- }

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,
- },
- //...
- }

React Fiber工作原理详解
双缓冲技术: React Fiber使用了类似于图形渲染中的双缓冲技术。这意味着在构建新的UI树时,React会同时在内存中维护两棵树:当前屏幕上显示的树(current tree)和正在构建的树(work-in-progress tree)。只有当新的树完全构建完成后,它才会被一次性地渲染到屏幕上,从而实现更加流畅的用户体验。
任务调度: React 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做了什么?
Commit阶段
在Commit阶段,React将根据在Reconciliation阶段生成的更新计划来执行实际的DOM更新。这个过程包括更新DOM节点、处理生命周期方法(如在类组件中的useEffect)以及执行其他与渲染相关的副作用。此阶段是同步执行的,意味着一旦开始,就会一口气完成,不会被其他任务打断
那 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后期支持并发渲染和异步数据流等功能打下了基础
核心特点与优势:
总之,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中的diff算法是其核心优化策略之一,用于比较新旧两个虚拟DOM树之间的差异,并找出最小化的DOM操作集以更新真实DOM。以下是React中diff算法的大致步骤概述:
key
属性。 key
的节点,则认为它们是同一个“实体”,只需更新属性或内容。key
的节点,则视为新增或删除节点。通过以上步骤,React的diff算法能够在O(n)的时间复杂度内完成虚拟DOM树的比较,从而实现在大量DOM更新时依然保持较高的性能。
在React中提高组件渲染效率并避免不必要的渲染主要有以下几个策略:
React.PureComponent
自动进行浅比较(shallow comparison
),只有当props或state发生改变时才会触发组件重新渲染。继承自React.PureComponent
的组件会默认检查props和state对象是否严格相等。React.memo
对其进行包裹,React.memo
同样会进行浅比较,只有当props发生变化时才会重新渲染组件。shouldComponentUpdate(nextProps, nextState)
方法,根据传入的新props和新state判断是否有必要调用render
方法,从而避免不必要的渲染。React.useState
和React.useReducer
时,可以根据业务逻辑精确控制state的变化,避免不必要的状态更新。React.useMemo
来缓存计算结果,仅当依赖的props或state改变时才重新计算。React.useCallback
来缓存函数引用,避免在props没变的情况下因为回调函数引用变了而导致不必要的子组件重渲染。shouldComponentUpdate
、useMemo
或手动进行深比较。useCallback
中,保证其在props不变时引用始终一致,避免无意义的组件重渲染。setState
。通过以上这些方法,可以最大程度地减少不必要的组件渲染,从而提升React应用的性能。在实践中,需要根据组件的具体情况进行权衡和选择最适合的优化方案。
React的render
方法是React组件的核心方法之一,它的基本原理和作用在于将组件的状态和属性转化为可以在浏览器中渲染的虚拟DOM表示,然后将这个虚拟DOM转化为实际的DOM操作,最终更新到浏览器的真实DOM中。
以下是render
方法的基本原理和过程:
render
方法。这个方法必须返回一个React元素(可以是原生DOM元素、组件元素或Fragment),React会根据这个返回值创建一个虚拟DOM树。render
方法返回的是一个虚拟DOM树,这是一个轻量级的JavaScript对象结构,与实际的DOM树结构相似但并非真实的DOM节点。props
或state
发生变化时,React会重新执行render
方法生成新的虚拟DOM树。React render
方法的作用就是将组件的状态和属性转化为虚拟DOM,通过虚拟DOM Diff算法来决定实际DOM的最小更新操作,从而实现高性能的用户界面更新。在类组件中,render
方法是必需的,而在函数组件中,函数体本身充当了render
方法的角色。
从Flux中衍生来的(单一数据源,单向数据流)
官方解释:Redux 是 JavaScript 状态容器,提供可预测化的状态管理。我的理解是,redux是为了解决react组件间通信和组件间状态共享而提出的一种解决方案,主要包括3个部分,(store + action + reducer)。
主要解决什么问题:
1、组件间通信
由于connect后,各connect组件是共享store的,所以各组件可以通过store来进行数据通信,当然这里必须遵守redux的一些规范,比如遵守 view -> aciton -> reducer的改变state的路径
2、通过对象驱动组件进入生命周期
对于一个react组件来说,只能对自己的state改变驱动自己的生命周期,或者通过外部传入的props进行驱动。通过redux,可以通过store中改变的state,来驱动组件进行update
3、方便进行数据管理和切片
redux通过对store的管理和控制,可以很方便的实现页面状态的管理和切片。通过切片的操作,可以轻松的实现redo之类的操作
Redux 中间件(Middleware)是 Redux 库中一个强大的扩展机制,它位于 action 被发出(dispatched)和到达 reducer 处理这两个阶段之间。中间件可以看作是对 Redux dispatch 流程的一种拦截器,它允许开发者在 action 传播的过程中执行额外的操作,例如日志记录、异步处理、事务控制、取消操作、异常处理等。
中间件通过链式调用的方式来组织,形成一个中间件栈。当一个 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。
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的引入极大地增强了函数式组件的能力,使得它们既能保持简洁又能拥有状态管理和生命周期功能,有效地解决了上述缺点。
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);

应用场景:
总结起来,高阶组件提供了一种强大的抽象手段,帮助开发者更好地组织和复用代码,保持组件层级扁平化,并且能够集中处理与特定业务逻辑或框架无关的通用需求。随着React Hooks的引入,虽然很多原本通过HOC实现的需求可以转由自定义Hooks完成,但在某些情况下,特别是在处理复杂的组件组合和扩展时,高阶组件依然是一种有效的设计模式。
通过上面的了解,高阶组件能够提高代码的复用性和灵活性,在实际应用中,常常用于与核心业务无关但又在多个模块使用的功能,如权限控制、日志记录、数据校验、异常处理、统计上报等
数据流管理:例如在Redux中,connect函数就是一个典型的高阶组件,用来连接React组件与Redux Store。
生命周期管理:为多个组件提供统一的生命周期逻辑,如初始化、清理资源、错误处理等。
条件渲染:根据某些条件动态决定是否渲染某个组件,或者渲染不同的组件版本。
API调用封装:在组件渲染前后执行API调用,并将数据注入到组件props中。
权限控制
日志记录
数据校验
异常处理
统计上报
React 受控组件(Controlled Components):
受控组件是指组件内部的状态完全由React的state管理,用户输入的值立即反映到组件的state中。在受控组件中,表单元素(如<input>
、<textarea>
或<select>
)的值不是直接由DOM本身管理,而是通过React组件的state进行控制。例如,<input>
元素的值由value
属性关联到组件的state变量,每次用户输入时,都会触发onChange事件,此时组件会根据事件处理函数更新state,进而更新value
属性,始终保持DOM元素的值与state同步。
特点:
优点:
缺点:
常用场景:
React 非受控组件(Uncontrolled Components):
非受控组件则是指DOM元素的值由浏览器本身管理,不受React组件的state直接影响。在非受控组件中,表单元素的值是由DOM自身的defaultValue属性初始化,用户输入的改变并不会立刻同步到React组件的state中。获取非受控组件的值通常通过ref来访问DOM节点的value属性。
特点:
优点:
缺点:
常用场景:
总结来说,受控组件适合于需要对用户输入实时响应和严格控制的场景,而非受控组件更适合于简单的表单处理,或者是需要利用原生DOM特性的场合。开发者应根据具体需求灵活选择合适的方式来处理表单输入。
React 中的 ref
是一种在组件之间直接访问 DOM 节点或在函数组件中访问类组件实例的能力。React 的 ref
底层原理涉及以下几个关键点:
React.createRef()
(适用于类组件)、useRef()
Hook(适用于函数组件)以及 forwardRef()
(用于在函数组件之间传递 ref)。ref={myRef}
的方式将 ref 分配给具体的 DOM 元素或类组件实例。ref
属性的元素,并将 ref 的 .current
属性指向相应的 DOM 节点或组件实例。.current
设置为对应的 DOM 元素。.current
指向的是组件的实例对象。forwardRef
结合 useImperativeHandle
Hook,可以让函数组件暴露特定方法或属性给父组件。.current
属性。.current
会被设置为 null
,以避免对已不存在的 DOM 节点或组件实例进行引用。React.forwardRef
API,我们可以创建一个能够将接收到的 ref
传递给其内部子组件的组件,这对于函数组件尤为有用,因为它允许函数组件也能获得和操作内部 DOM 节点或子组件实例。commitWork
阶段将更新的 ref 信息应用到实际的 DOM 树中。总而言之,React 的 ref
系统通过对组件生命周期的精细控制和对 DOM 操作的抽象,为开发者提供了便捷地访问和操作底层 DOM 节点的能力,同时也支持组件间的直接交互。React 内部通过巧妙的数据结构和算法确保了 ref 更新的高效和准确。
当React在渲染过程中遇到带有ref属性的元素时,会通过内部机制将ref对象与对应的DOM节点或组件实例建立联系。对于DOM元素,React会在组件挂载后立即将ref对象的current属性指向对应的DOM节点。
对于类组件,ref的current属性将会指向组件的实例。这样就可以访问类组件的实例方法和内部状态。
对于函数组件,通过forwardRef和useImperativeHandle可以定制函数组件对外暴露的实例行为,使其也可以拥有类似类组件实例的行为。
总的来说,React通过一套内部机制,在不影响React组件抽象的同时,为开发者提供了一种间接但灵活地访问和操作DOM节点的方式。通过ref,开发者可以在React的声明式编程模型中实现对DOM的命令式操作,这对处理聚焦、测量尺寸、动画等场景至关重要。
父组件向子组件通信:父组件通过 props 向子组件传递需要的信息。
子组件向父组件通信:: props+回调的方式。
跨级组件的通信方式?
父组件获取子组件的状态和方法
1、通常建议遵循React数据流向单向数据绑定的原则,尽量避免直接访问子组件的状态。
2、使用回调函数是一种更符合React设计理念的方式,它促进了组件之间的解耦和可复用性。
3、Refs 主要用于获取DOM节点或在必要时获取子组件实例进行一些特殊操作,而不鼓励常规情况下频繁获取子组件的状态。
类组件生命周期方法 | 对应的 Hooks 功能 |
| N/A(直接在函数组件中初始化状态即可,如使用 初始化状态) |
|
(传入一个空数组作为依赖项,表示在组件挂载后执行一次) |
|
(传入依赖项数组,当这些依赖项变化时执行) |
|
返回的清理函数(在组件卸载前执行清理操作) |
|
(对于函数组件,用于优化不必要的渲染) 的依赖数组中精确定义需要监听的变化 |
| 避免在函数组件中使用派生状态。推荐使用 或 ,并在 中根据props更新state。 |
补充说明:
componentDidUpdate
,React Hooks并没有直接对应的Hook来模拟,但可以通过在 useEffect
的依赖数组中声明需要观察的props或state变量来实现类似的效果。useState
、useReducer
以及 useEffect
等Hooks来实现。在最新版本的React 18中,批处理机制得到了进一步增强和改进。批处理主要是指React能够有效地合并多个连续的状态更新,从而减少不必要的渲染次数。
在React 17及以前版本中,React已经实现了半自动的批处理,即在React事件处理程序内部发生的多个setState
调用会被合并在一起,并且在事件循环结束前一次性执行这些更新,而非分别触发多次渲染。
React 18引入了更全面的自动批处理机制,其中的核心改变在于:
createRoot
API:ReactDOM.createRoot()
方法替换原有的ReactDOM.render()
方法来挂载根组件。这样做是为了兼容新的批处理逻辑以及并发渲染特性。startTransition
,允许开发者标记某个状态更新为可延迟的,这将进一步帮助React进行有效的批处理和优化渲染过程,特别是对于那些不影响UI关键路径的状态变更。总之,React 18通过更加精细和广泛的批处理机制,增强了其性能优化能力,使得应用程序能够在状态频繁变动的情况下仍然保持高效和流畅的用户体验。
setState
方法在 React 中调用后,直到页面重新渲染之间经历了一系列的步骤。以下是这个过程的详细解释:
setState
时,React 不会立即改变组件的状态,而是将传入的新状态对象与当前状态进行合并。合并通常是浅层合并,这意味着如果新状态包含深层次的对象属性更改,那么只有第一层属性会合并,深层对象的更改可能不会生效,除非显式替换整个深层对象。setState
操作视为异步的,特别是当在事件处理器或生命周期方法中调用时。这意味着调用 setState
并不会立即导致重新渲染。实际上,React 可能会把多个连续的 setState
调用合并成一个,以减少不必要的渲染次数。setState
调用,然后一次性去更新状态。这样有助于在高并发更新时优化性能。componentWillReceiveProps
(在旧版 React 中)或 getDerivedStateFromProps
(在 React 16.3+ 版本中),接着是 shouldComponentUpdate
(如果有实现的话)来决定是否需要继续渲染。render
方法来生成新的虚拟 DOM 树。getSnapshotBeforeUpdate
(如果有的话)来抓取更新前后的状态差异,随后触发 componentDidUpdate
生命周期方法,这时组件已经反映出了新的状态和 UI。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 适用于函数组件,它们通过浅比较来决定组件是否需要重新渲染,从而达到性能优化的目的。但请注意,这两种方式都不适用于含有深层嵌套数据结构或依赖内部状态变更的组件优化。在这种情况下,应手动进行深比较或者使用更高级别的优化手段。
不能。这是因为refs是在组件挂载到DOM之后填充的,并且在render()执行期间,React还没有完成这个过程。
refs会在组件渲染完成后才被赋值,即在组件生命周期的某个阶段(比如componentDidMount或getDerivedStateFromProps(已弃用)后的一个回调钩子中)才能保证已经被正确设置并指向真实的DOM节点或组件实例。
因此,如果你尝试在render()方法内部访问ref,它可能会是null或未定义的,因为在那个时刻,React还没有机会将其关联到实际的DOM元素或组件实例上。
正确的做法是在生命周期方法中或者其他合适的时机(例如事件处理函数中)访问refs。例如,在componentDidMount或useEffect Hook(对于函数组件)中,可以安全地通过this.refName.current(对于类组件)或 useRef Hook 返回的 .current 属性(对于函数组件)来访问refs指向的内容。
React 中进行性能优化的关键生命周期方法是 shouldComponentUpdate(nextProps, nextState),这是在类组件中可用的一个生命周期方法。另外,在函数组件中,可以通过 React.memo 或者 useMemo、useCallback Hooks 来实现相似的效果。
shouldComponentUpdate(nextProps, nextState):
优化原理:
函数组件优化:
基于 Hash 的路由(Hash-Based Routing)
优点:
缺点:
基于 HTML5 History 的路由(History-Based Routing)
优点:
缺点:
重定向(Redirect)在Web开发中指的是将用户的请求从一个URL转向另一个URL的行为,通常是当用户访问某个页面时,服务器或前端框架决定将用户引导至另一个页面。
HTML <a> 标签:
React Router <Link> 标签:
React Router 4 中的 Switch 组件用于渲染一组 Route 组件,它会遍历其子 Route 组件并依次检查它们是否与当前的location匹配。重点在于,Switch 会确保只渲染第一个匹配路径的 Route,一旦找到匹配的 Route,它会停止遍历剩余的 Route 子组件。
这样做的好处是可以避免在多个路径同时匹配时,多个组件同时被渲染的情况,确保任何时候路由匹配时只有一个组件是活跃并渲染出来的。这对于构建更有序和明确的路由逻辑非常有用,尤其是在有嵌套路由或多分支路由的情况下,能够确保页面展现的正确性和一致性
useEffect
和 useLayoutEffect
都是 React 中的 Hook,用于处理副作用,例如订阅事件、执行异步操作、更新DOM属性等。它们的主要区别在于执行时机和浏览器渲染流水线的影响:
useEffect
:在所有的 DOM 变更已经完成并同步到浏览器界面之后,浏览器的事件循环下一次迭代中异步执行。这意味着在 useEffect
里的代码运行时,用户已经可以看到渲染的结果。useLayoutEffect
:在所有的 DOM 变更完成后立即同步执行,紧接在 DOM 更新之前。这意味着 useLayoutEffect
内部的代码更改会影响到当前帧的渲染结果,因此在它执行期间,浏览器会暂停布局和绘制工作,等待同步代码执行完毕。useEffect
:由于其异步执行的特性,不会阻塞浏览器的渲染流程,所以不会引起明显的卡顿或布局闪烁。useLayoutEffect
:由于其同步执行,若在其回调函数中修改了样式或其他影响布局的属性,将会强制浏览器重新计算布局,这可能造成性能上的影响,特别是在循环或大量元素上进行同步更新时。useEffect
:适用于大部分副作用场景,尤其是涉及网络请求、设置定时器、订阅事件等不直接影响布局的任务。useLayoutEffect
:在那些需要在渲染后立即操作DOM并确保DOM更新与之同步的场景中使用,比如某些动画效果、依赖DOM尺寸变动的操作,或者为了防止UI闪烁而需要在绘制前调整样式的情况。总结来说,useEffect
更适合非阻塞渲染管线的异步操作,而 useLayoutEffect
则用于那些需要严格保证在渲染完成前执行,并可能影响布局的操作。在大部分情况下,优先考虑使用 useEffect
;只有在特定场景下,当确实需要同步更新并避免布局抖动时,才应该选择 useLayoutEffect
。
(1)不要在循环,条件或嵌套函数中调用Hook,必须始终在 React函数的顶层使用Hook
(2)使用useState时候,使用push,pop,splice等直接更改数组对象的坑
(3)useState设置状态的时候,只有第一次生效,后期需要更新状态,必须通过useEffect
(4)善用useCallback
以下是useState
在React中的底层实现原理的大致步骤概述:
useState(initialState)
时,它会在组件实例的内部创建一个新的状态槽(slot),并将initialState
作为初始值存入该槽位。由于函数组件没有实例的概念,React通过Fiber节点来模拟实例行为,所以实际上是给对应的Fiber节点添加了状态。setState(newState)
时,React不会立即修改状态,而是创建一个更新任务,将其加入到一个优先级队列中。这是因为React采用了异步更新策略,以批量处理多个状态更新,提高性能。useState
会返回上一次渲染时的状态值,但如果在本次渲染周期中有待处理的更新,则会返回已更新的状态值。setState
函数通过闭包绑定到了正确的组件实例(即Fiber节点),这样在任何地方调用这个函数都能够准确地修改相应组件的状态,而不影响其他组件。综上所述,useState
在React的底层是通过一套复杂但高效的系统实现的,包括状态槽的管理、更新任务的调度、异步更新机制、以及闭包的应用等技术手段,从而使得函数组件也能具备维护自身状态的能力。
fiber 角度下的 useState
在Fiber架构下,React为每个函数组件实例(即Fiber节点)内部维护了一个状态对象。当调用 useState(initialState) 时:
因此,useState 能够在函数组件中管理状态,得益于React Fiber架构对其内部状态管理机制的改进和增强。Fiber不仅负责调度和更新组件,还提供了在函数组件中存储和更新状态的能力。
useEffect
是React Hooks中的一个重要Hook,用于处理副作用、订阅数据源和执行DOM操作等。以下是useEffect
在React中的底层实现原理的大致步骤:
useEffect(callback, deps)
时,它会捕获当前作用域下的callback
函数(副作用函数)以及依赖数组deps
。每次组件渲染完毕后,React都会检查是否存在已经注册过的效应函数,并且比较新的依赖项列表与旧的是否发生变化。useEffect
中传入的回调函数,不论依赖项数组是否为空。这相当于“挂载”阶段执行的生命周期方法如componentDidMount
。componentDidUpdate
生命周期方法。componentWillUnmount
生命周期方法。[]
作为useEffect
的第二个参数,告诉React这个副作用函数只在组件挂载和卸载时运行,不依赖任何props或state的变化。这有助于避免不必要的重复执行。useEffect
Hook的工作机制基于渲染周期后的回调执行和依赖项变化的追踪,利用这种机制,React能够在函数组件中有效地管理和调度各种副作用操作,保持组件逻辑清晰的同时,也实现了与类组件生命周期方法类似的灵活性和控制力。
useContext
是React提供的一个Hook,它允许组件无需通过props层层传递就能直接访问Context中的数据。然而,在使用过程中需要注意适度,避免滥用的原因主要有以下几点:
useContext
可能导致各个组件与全局状态高度耦合。即使这些组件原本不需要关心全局状态,也可能因为直接消费Context而导致维护性和可读性降低,同时也增加了组件重构和复用的难度。useContext
访问同一个Context时,一旦这个全局状态发生改变,所有依赖它的组件都有可能被迫重新渲染,即便它们并不关心这次状态变化的具体内容。这可能导致不必要的性能损失。因此,在实际开发中,应当审慎地使用 useContext
,只在必要的全局共享状态场景下使用,并结合其他最佳实践(如Redux、 Recoil、MobX等)以及React的其他特性(如 useReducer
、useState
等),合理规划和组织应用的状态管理结构。
React 16.x的三大新特性 Time Slicing、Suspense、 hooks
(1)React16.8
加入hooks,让React函数式组件更加灵活,hooks之前,React存在很多问题:
hooks很好的解决了上述问题,hooks提供了很多方法
(2)React16.9
(3)React16.13.0
在React中,状态提升(State Lifting)是一种管理状态(state)的设计模式,它是指当多个组件需要共享相同状态时,而不是各自在各自的组件内维护独立的状态副本,应该将该状态集中管理,将其移到这些组件的最近共同父组件(或创建一个新的容器组件专门用来托管此状态)中去维护。这样,父组件可以通过props向下传递状态以及更新状态的方法给子组件,子组件通过调用这些方法通知父组件来间接更改和同步状态,从而保证所有依赖此状态的子组件能够得到及时和一致的更新。
使用场景举例:
通过状态提升,可以确保状态的一致性和避免组件之间的不必要的直接耦合,符合React中单向数据流的原则,使应用程序的数据流更易于理解和调试。
pushState
函数可以给历史记录关联一个任意的可序列化 state
,所以可以在路由 push
的时候将当前页面的一些信息存到 state
中,下次返回到这个页面的时候就能从 state
里面取出离开前的数据重新渲染。react-router 直接可以支持。这个方法适合一些需要临时存储的场景。本质上来说JSX是React.createElement(component, props, ...children)
方法的语法糖。在React 17之前,如果使用了JSX,其实就是在使用React, babel
会把组件转换为 CreateElement
形式。在React 17之后,就不再需要引入,因为 babel
已经可以帮我们自动引入react。
使用了装饰模式,高阶组件的运用:
装饰模式的特点是不需要改变 被装饰对象 本身,而只是在外面套一个外壳接口。JavaScript 目前已经有了原生装饰器的提案,其用法如下:
React.memo():
useMemo():
区别:
在实际使用中,这两个工具可能会同时使用,以确保组件和值的性能都得到了优化。
React引入了“合成事件”(SyntheticEvent)的概念,这是一种跨浏览器的事件抽象层,它模拟了W3C标准事件行为,并提供了统一的接口给开发者。
在React中,批处理(Batching)是一种优化策略,主要用于提升React组件状态更新时的性能。批处理的核心概念是将一系列连续的状态更新操作合并到一起,然后一次性去触发视图的重新渲染,而非每更新一次状态就立即引起一次渲染。
具体来说:
setState
或使用Hooks中的useState
、useReducer
等API更改状态时,通常会触发组件及其子组件的重新渲染。但如果短时间内有多个状态更新操作,每个更新都会导致渲染流水线上的额外开销。 setState
,React也会等待这些调用全部结束,然后才进行一次实际的组件树比较(reconciliation)和渲染过程。flushSync
API,允许开发者手动同步执行状态更新,绕过批处理机制,确保状态变化立即反映到DOM中。总的来说,批处理是React用来优化性能的关键手段之一,旨在减少不必要的渲染次数,提高应用的整体响应速度。
render
方法,生成新的虚拟DOM树。虚拟DOM是一种轻量级的JavaScript对象表示,它模仿了实际DOM结构。在整个patch流程中,React通过高效的Diff算法和最小化DOM操作策略,最大程度地减少了对实际DOM的直接操作,从而提高应用的性能和响应速度。在React Fiber架构下,这个流程变得更加灵活,能够支持异步渲染和优先级调度,使得UI更新更加平滑和高效。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。