
React 副作用:是在组件渲染期间发生的任何操作,这些操作不仅仅是更新 DOM。副作用可能包括网络请求、访问本地存储、添加或删除事件监听器等。副作用是与 React 的声明式编程模型相对的
什么是React
React是一个网页UI框架,通过组件化的方式解决视图层开发复用的问题,本质是一个组件化框架。- 它的核心设计思路有三点,分别是声明式、组件化与通用性。
- 声明式的优势在于直观与组合。
- 组件化的优势在于视图的拆分与模块复用,可以更容易做到高内聚低耦合。
- 通用性在于一次学习,随处编写。比如
React Native,React 360等,这里主要靠虚拟DOM来保证实现。 - 这使得
React的适用范围变得足够广,无论是Web、Native、VR,甚至Shell应用都可以进行开发。这也是React的优势。 - 但作为一个视图层的框架,
React的劣势也十分明显。它并没有提供完整的一揽子解决方案,在开发大型前端应用时,需要向社区寻找并整合解决方案。虽然一定程度上促进了社区的繁荣,但也为开发者在技术选型和学习适用上造成了一定的成本。
什么是JSX
React本身并不强制使用JSX:
1 | class Hello extends React.Component{ |
React 需要将组件转化为虚拟 DOM 树;
XML在树结构的描述上天生具有可读性强的优势。
1 | class Hello extends React.Component{ |
模板
以AngularJS 为例
1 |
|
模板字符串
1 | var box = jsx` |
总结
JSX是一个JavaScript的语法扩展,结构类似于XML。JSX主要用来声明React元素,但React并取强制要求是用JSX,即使使用了JSX,也会在构建的过程中通过babel插件转化为React.CreateElement,所以JSX更像是React.CreateElement的语法糖,可以看出React团队并不想引入JavaScript本身以外的开发体系,而是通过合理的关注点分离保持组件开发的纯粹性。对比
- 模板:引入模板语法和模板指令等概念是一种不佳的实现方案;
- 模板字符串:造成多次嵌套,使整个结构变的复杂,并且优化代码提示也会变的困难重重
JXON:同样因为语法提示问题被React放弃
最后选用了JSX,因为JSX与其设计思想贴合,不需要引入过多新的概念,对代码编辑器的提示也极为友好。
如何避免生命周期的坑
- 在不恰当的时机调用了不合适的代码
- 在需要调用时,却忘记了调用
建立时机与操作的对应关系
社区中去除 constructor 的原因
constructor中并不推荐去处理初始化以外的逻辑;constructor不属于React的生命周期,只是Class的初始化函数;- 通过移除
constructor,代码也会变得更简洁;
挂载阶段
getDerivedStateFromPorps
本函数的作用是使组件在 props 变化时更新 state。
触发时机:(只要父级组件重新渲染时就会被调用)
- 当
props被传入时; state发生改变时;forceUpdate被调用时;
==你可能不需要使用派生state==。两种反模式使用方式:
- 直接复制
props到state - 在
props变化后修改state
这两种写法,==除了增加代码的维护成本外,没有任何好处==。
UNSAFE_componentWillMount
用于组件将加载前做某些操作,但目前被标记为弃用。因在 React 异步渲染机制下,该方法==可能被多次调用==。
常见的错误是:和服务器端同构渲染的时候,如果在该函数里面发起网络请求,会在服务端和客户端分别执行一次。
render
render 函数返回的 JSX 结构,用于描述具体的渲染内容。
不应该在render 函数里面产生任何副作用,比如使用setState或者绑定事件。
render函数在每次渲染时都会被调用,而setState会触发渲染,会造成死循环。绑定事件会被频繁调用注册。
更新阶段
指外部 props 传入,或 state 发生变化时的阶段。
UNSAFE_componentWillReceiveProps:在getDerivedStateFromPorps存在时,不会被调用。
UNSAFE_componentWillUpdate:因为在后续的React异步渲染设置中,可能会==出现暂停更新渲染==的情况;
getSnapshotBeforeUpdate: 返回值会作为 componentDidUpdate 的第三个参数使用。
卸载阶段
componentWillUnmount
主要用于执行清理工作。一定要在该阶段解除事件绑定,取消定时器。
不然会导致定时器在组件销毁后一直在不停地执行;
职责:
- 什么情况下会触发重新渲染?
- 渲染中发生报错后会怎样? 该如何处理?
函数组件:
任何情况下都会重新渲染,没有生命周期,官方提供React.memo优化手段。
React.memo并不是阻断渲染,而是==跳过渲染组件的操作,并直接复用最后一次渲染的结果==
React.Component:
不实现 shouldComponentUpdate 函数,有两种情况触发重新渲染
- 当 state 发生变化时
- 当父级组件的 Props 传入时
React.PureComponent:
默认实现了 shouldComponentUpdate 函数
仅在 props 与 state 进行浅比较后,确认有变更时才会触发重新渲染。
错误边界:
1 | class ErrorBoundary extends React.Component { |
componentDidCatch:捕获报错的具体类型,并将错误类型上传到服务端去。
用户执行某个操作时,触发了bug,引发了崩溃,页面会突然白屏,但渲染时的报错,只能通过 componentDidCatch 捕获。这是在做线上错误监控时,极其容易忽略的点。
React 的请求应该放在哪里,为什么?
对于异步请求,应放在 componentDidMount 中操作从时间顺序看,除 componentDidMount 还可以有以下选择:
constructor:可放,从设计言不推荐,主要用于初始化state与函数绑定,不承载业务逻辑且随着类属性流行,constructor已很少用componentWillMount:已被标记废弃,在新的异步渲染架构下会触发多次渲染,易引发 Bug,不利未来 React 升级后的代码维护
类组件和函数组件的区别?
相同点:
函数组件和类组件==使用方式==和==最终呈现效果==上是完全一致的。
很难从使用体验上区分两者,而且现代浏览器,闭包和类的性能是在极端场景下才会有区别。所以基本认为两者作为组件是完全一致的。
不同点:
基础认知:本质上代表两种==不同设计思想==与心智模式
- 类组件的根基是
OOP,面向对象编程; - 函数组件的根基是
FP,也就是函数式编程;
函数式编程:假定输入和输出,存在某种特定的映射关系时,那么输入一定的情况下,输出必然是确定的。
本质上,最大的不同:==相较于类组件,函数组件更纯粹、简单、易测试==。
使用场景:
- 在不使用 Recompose 或者
Hooks的情况下如需使用生命周期,就用类组件,限定场景是固定的。 - 在 recompose 或
Hooks的加持下,类组件与函数组件的能力边界完全相同,都可使用类似生命周期等能力
设计模式:
- 类组件可以实现继承
- 函数组件缺少继承能力
React不推荐使用继承,组合由于继承
未来趋势:
==函数组件==成为了社区未来主推的方案。
类组件不能适应未来趋势的原因:
this的模糊性- 业务逻辑散落在生命周期中
- React组件,代码缺少标准的拆分方式
使用Hooks函数组件可以提供比原生更细腻的逻辑组织与复用,而且能更好的适应时间切片与并发模式。
如何设计React组件
- 把只作展示、独立运行、不额外增加功能的组件,称为哑组件或无状态组件、==展示组件==;
- 把处理业务逻辑与数据状态的组件称为有状态组件、==灵巧组件==。灵巧组件一定包含至少一个灵巧组件或展示组件。
展示组件
展示组件受制于外部的 props 控制,具有极强的通用性,复用率很高
代理组件:常用于封装常用属性,减少重复代码。
即对UI库的二次封装,对于常用属性给默认值,如果需要修改属性,直接传入props覆盖默认值即可。
虽然这样的封装看起来多此一举,但是切断了外部组件的强依赖性。
两个问题:
- 如果当前组件库不能使用,是否能实现业务上的无痛切换;
- 如需批量修改基础组件的字段,如何解决?
==代理组件的设计模式==很好地解决了以上问题业务上看,代理组件隔绝 Antd,仅是一个组件 Props API 层的交互
样式组件:也是一种代理组件,只是又细分了处理样式领域,将当前的关注点分离到当前组件内
复杂的样式管理对于Button是没有意义的,如果直接使用Button在属性上进行修改,对于工程代码而言,这是编写大量的面条代码。StyleButton的思路就是,样式判断逻辑附令到自身上来,面向未来改动的时候会更加友好。
布局组件基本设计与样式组件完全一样,基于自身特性做了一个小小的优化
灵巧组件
灵巧组件面向业务,功能更丰富、复杂性更高,复用度更低;
展示组件专注于组件本身特性,灵巧组件专注于组合组件。
容器组件
几乎没有复用性,主要用在拉取数据与组合组件两个方面。(没有冗余的样式和逻辑处理)
高阶组件:
React 中复用组件逻辑的高级技术,是基于 React 的组合特性形成的设计模式;
高阶组件的参数是组件,返回值为新组件的函数。
作用:
- 逻辑复用
- 链式调用
- 渲染劫持
例子:登录态的判断。【数据埋点】
例子:渲染劫持
通过控制 render 函数修改输出内容,常见的场景是显示加载元素
缺陷:
- 丢失静态函数
- refs属性不能透传
使用Storybook工具对basic组件进行组件管理
setState 是同步更新还是异步更新
合成事件:
- React 给 document 挂上事件监听
- DOM 事件触发后冒泡到 document
- React 找到对应的组件造出一个合成事件出来
- 并按组件树模拟一遍事件冒泡
React 17 之前的事件冒泡流程图:
事件委托挂载在document上
React 17 之后的事件冒泡流程图:
事件委托不再挂载在document上,而是挂载在DOM容器上
1 | class Coun t extends Component{ |
是否觉得 React 的 setState 执行像是一个队列?
React 根据队列逐一执行,合并 state 数据完成后执行回调,根据结果更新虚拟 DOM触发渲染。
异步更新(非真异步)——原因:
- 保持内部的一致性(如果把setState改成同步了,但是props不是)
- 启用并发更新
在源码中,通过isBatchingUpdates判断setStates是先存进队列还是直接更新。true:执行异步操作,false:直接更新。
在 React 的生命周期事件和合成事件中可拿到isBatchingUpdates 控制权将状态放进队列,控制执行节奏。
setState 之后发生了什么
React 利用状态队列机制实现了 setState 的“异步”更新,避免频繁的重复更新 state。
首先将新的 state 合并到状态更新队列中,然后根据更新队列和 shouldComponentUpdate 的状态来判断是否需要更新组件。
在“异步”中,
如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行;
如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
1 | class Demo extends Component { |
setState 本身代码的执行肯定是同步的,这里的异步是指是多个 state 会合成到一起进行批量更新。 同步还是异步取决于它被调用的环境。
- 如果
setState在 React 能够控制的范围被调用,它就是异步的; - 如果
setState在原生 JavaScript 控制的范围被调用,它就是同步的;
1.异步情况:在合成事件处理函数,生命周期函数
2.同步情况:在原生事件处理函数,定时器回调函数,Ajax 回调函数
1 | //setTimeout事件 |
笔试题:
1 | class Count extends Component{ |
如果面向组件跨层通信
Context 存储的变量难以追溯数据源以及确认变动点。当组件依赖Context时,会提升组件耦合度,不利于组件的复用与测试。
Virtual DOM的工作原理是什么
Fackbook的初衷
- 简化前端开发
- 防止XSS。
==通过虚拟DOM来规避风险==。因为直接操作DOM会带来XSS的风险,也可能因为技术水平的限制,带来性能的问题。(如果你心爱的东西不喜欢有人去触碰,最好的办法是把它封起来,与使用者相隔离,因此有了我们今天看到的虚拟DOM)
JSX所描述的结构,会转译成React.createElement函数:
1 | // JSX描述 |
- React 会持有一颗虚拟 DOM 树。在状态变更后,会触发虚拟 DOM 树的修改,再以此为基础修改真实 DOM
React.createElement 返回的结果应是一个 JavaScript obiect
1 | { |
diff 函数,去计算状态变更前后的虚拟 DOM 树差异;
渲染函数,渲染整个虚拟 DOM 树或者处理差异点;
优势
- 性能优越
- 规避XSS
- 可跨平台(RN,小程序)
边界:
大量的直接操作 DOM 容易引起网页性能下降。这时 React 基于虚拟 DOM 的 diff 处理与批处理操作,可降低 DOM 的操作范围与频次,提升页面性能
什么时候虚拟DOM慢呢?
首次渲染或者微量操作的时候,虚拟DOM就会比真实的DOM更慢。
虚拟 DOM 一定可以规避 XSS 吗?
虚拟 DOM 内部确保字符转义,确实可做到这点,但 React 存在风险,因为 React 留有 dangerouslySetlnnerHTML API 绕过转义。
跨平台的成本更低
在 React Native 后,前端社区从虚拟 DOM 中体会到跨平台的无限前景,所以在后续发展中,都借鉴虚拟 DOM。
缺点
- 内存占用较高
- 无法进行极致优化
因为当前网页的虚拟DOM包含真实DOM的完整信息,而且由于是Object,内存占用肯定会有所上升。
虽然虚拟DOM足以应对绝大部分应用的性能要求,但在一些性能要求高的应用中无法进行针对性的优化。
与其他框架相比,React的diff有何不同
diff算法是指,生成更新补丁的方式。主要应用于虚拟DOM树变化,更新真实DOM。
- 真实的 DOM 首先会映射为虚拟 DOM;
- 当虚拟 DOM 变化后,会根据差异计算生成 patch。patch 是结构化的数据,包含增加、更新、移除等;
- 根据 patch 去更新真实的DOM,反馈到用户界面上

diff算法:
- 更新时机——触发更新、进行差异对比的时机。(setState,hooks调用之后,此时树的节点发生变化,开始比对)
- 遍历算法——深度优先遍历
- 优化策略
深度优先遍历——从根节点出发,沿着左子树方向进行纵向遍历,直到找到叶子节点为止然后回溯前一个节点,进行右子树节点遍历,直到遍历完所有可达节点
虽然深度优先遍历保证了组件的生命周期时序不错乱,但传统的 diff 算法带来一个严重的性能瓶颈,复杂程度为 O(n3),其中 n 表示树的节点总数。
React 用了一个非常经典的手法将复杂度降低为 O(n)就是分治,即通过“分而治之”这一巧妙的思想分解问题。
将单一节点比对,转化为了三种类型节点比对。React从==树、组件、元素==三个方面进行了优化。
策略一:忽略节点跨层级操作场景,提升比对效率;
需进行==树比对==,即对树进行分层比较两棵树==只对同一层次节点进行比较==,如发现节点已不存在则该节点及其子节点会被完全删除,不会用于进一步比较提升了比对效率
策略二:如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构
如果组件是同一类型则进行树比对,如果不是则直接放入补丁中。
只要父组件类型不同,就会被重新渲染,这就是shouldComponentUpdate/PureComponent/React.memo可以提高性能的原因
策略三:同一层级子节点,可通过标记 key 的方式进行列表对比。
元素比对主要发生在同层级中,通过标记节点操作生成补丁。
节点操作包含了插入、移动、删除等。
其中节点排序,同时涉及插入、移动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要的作用。
通过标记 key 的方式,React 可以直接移动 DOM 节点,降低内耗
Fiber
react16引入了fiber机制,进行了优化。
1、Fiber 机制下节点与树分别采用 FiberNode 与 FiberTree 进行重构
- FiberNode使用了双链表的结构,可以直接找到兄弟节点和子节点,使得整个更行过程可以随时暂停、恢复。
- FiberTree是通过FiberNode构成的树。
2、Fiber 机制下整个更新过程由 current 与 worklnProgress 两株树,双缓冲完成
- 当worklnProgress更新完成后,通过修改current的相关指针指向的节点,直接抛弃老树。虽然非常简单粗暴,却非常合理。
其他框架
PReact diff 算法相较于React,整体设计思路相似。
最层次的元素采用真实DOM对比操作,并没有采用Fiber的设计。
Vue 2.0 使用了 snabbdom,整体思路与 React 相同。
但在元素对比时,如果新旧两元素是同一元素,且没有设置 key 时,snabbdom 在 diff 子元素中会一次性对比==旧节点==、==新节点==及它们的==首尾元素==四个节点,以及==验证列表==是否有变化。
Vue3.0 整体变化不大。
最后
React拥有完整的diff算法策略,且拥有随时中断更新的时间切片能力。在大批量更新的极端情况下,拥有更友好的交互体验。
PReact可以在一些对性能要求不高,仅需要渲染的简单场景下使用。
Vue的diff策略整体与React对齐,虽然缺乏时间切片能力,但并不意味这Vue的性能更差,因为在Vue3初期引入过,后来因为收益不高移除掉了。除了高帧率动画、其他场景几乎都可以防抖节流去提高乡音性能。
如何根据React diff算法原理优化代码?
- 根据 diff 算法的设计原则,应尽量避免跨层级节点移动,
- 通过设置唯一 key 进行优化,尽量减少组件层级深度,因为过深的层级会加深遍历深度,带来性能问题
- 设置 shouldComponentUpdate 或者 React.pureComponet 减少 diff 次数
React的渲染异常会造成什么后果
“错误边界” 相关内容:如果渲染异常,在没有任何降级保护措施的情况下,页面会直接显示白屏。
通用方案:getDerivedStateFromError/componentDidCatch
getDerviedStateFromError和componentDidCatch的区别是前者展示降级UI,后者记录具体的错误信息,它只能用于class组件
1 | class ErrorBoundary extends React.Component{ |
==错误边界无法捕获自身的错误,也无法捕获事件处理、异步代码(setTimeout、requestAnimationFrame)、服务端渲染的错误==
预防
在渲染层,render 中 return 后的 JSX,都是在进行数据的拼装与转换
- 如果在拼装的过程中出现错误,那直接会导致编译的失败
- 但如果在转换的过程中出现错误,就很不容易被发现
前端数据基本上都是通过后端业务接口获取,那么是数据否可靠,就成为了一个至关重要的问题。
这个问题被称为null-safety,也就是空安全,目前对于这个问题比较成熟的解决方案是使用idx
idx 在使用时需要配置 Babel 插件,再引入idx 库。然后通过 idx 函数包裹需要使用的 object,再在回调函数中取需要的值。
idx的代码既不优雅,也不简洁,还需要引入babel插件,所以使用者寥寥无几。
优雅的解决方案:
Es202,可选链操作符
兜底
应该限制崩溃的层级。错误边界加到哪里,崩溃就止步于哪里,其他组件还可正常使用;
所以只需给关键的 UI 组件添加错误边界,那就可应用==高阶组件(或者自定义hooks)==
需保障方案在项目中的覆盖量,统计兜底页面成功兜底次数,最后兜底页面展示时能及时完成线上报警。
每个公司至少会接入统计工具,如百度统计、Google 统计完成业务分析,只需在代码中,添加一行统计代码
如何提升React代码的可维护性
1、预防与兜底
预防:从上线前开始可对代码做哪些措施防止出现线上问题
兜底:上线后又可以做哪些方案加快线上故障的定位速度
预防
通过使用人工或者工具审查的方式去实现。
人工审查代码的方式,标准称谓是 Code Review基于React 写法的易错点,团队内部会总结出一些实践准则。
工具审查的方式,标准称谓是静态代码检查工具(ESLint)
兜底
在线环境的代码通常是经过 UglifyJS / terser混淆并压缩的,所以直接看报错信息不能得知对应的源码是什么样的,不利于排查问题。
最理想的情况莫过于改造编译流水线,在发布过程中上传 sourcemap 到报错收集平台。
在 Webpack 中添加 sourcemap 相关插件就可在编译过程,直接上传 sourcemap 到 Sentry 的报错平台
在使用 Sentry 捕获报错时,就能够直接查看对应的源码了:
使用 Mozilla 开源的工具 sourcemap,直接恢复对应的源代码信息。
2、可改变性
从代码层面来讲,可变性代表了代码的可拓展能力。
两个思路提升代码的可拓展性:
- 从组件的角度出发,通过分离容器组件与展示组件的方式分离模块。其中推荐了 Storybook 来沉淀展示组件。
- 框架状态管理框架中有相对成熟的设计模式,比如 Redux 中的action、reducer 等,它的边界很清楚很容易明白业务逻辑该如何拆解、如何放入模块中。
3、稳定性
在前端项目中,无论是单元测试还是集成测试,整体覆盖比例都很低。常常通过人工测试“点点点”的方式保证稳定性。
前端测试并不好写。针对 UI 层不好写:
国内业务迭代模式都非常快,快到 UI层难以有稳定的测试代码,所以通常不会花太多时间去写组件的测试。基于实际情况,有条件写测试的话,也是尽量给核心业务写测试,更利于整体项目的稳定性。
4、依从性
遵循约定,提升代码可读性、减少人为因素,加强工具干预:
- 针对样式的Stylelint
- 针对JS的ESLint
- 针对代码提交的commitlint
- 针对编辑器风格的Editorconfig
- 针对代码风格的Prettier
React Hooks使用限制有哪些?
为什么使用Hooks
1、组件之间难以复用状态逻辑
如果涉及场景更复杂,多级组件需共享状态,就需使用 Redux 或 Mobx 来解决了。
既然是每个人都遇到的问题,最好考虑从 React 层提供 API 来解决。(高阶组件)
2、复杂的组件变得难以理解
主要指出生命周期函数没能提供最佳的代码编程实践范式
如 componentDidMount,在这里设置页面标题、拉取用户信息、拉取按钮权限信息。ComponentDidMount 函数内部逻辑随意堆砌,内容杂乱,缺乏专注性,往往还会对上下文产生依赖。
在componentDidMount中使用事件注册、订阅消息等,都需要在componentWillUnmount中去取消它。订阅与取消订阅并没有直接关联在一起,而是通过生命周期函数去使用这非常的反模式,也就导致组件难以分解,且到处都是状态逻辑。
3、人和机器都容易混淆类
this首当其冲,值捕获的问题。- 还有一个与
this相关的问题,就是用bind函数包一下来绑定事件。虽然现在通过了类属性方案,也可使用Babel 插件提前开发,但整个提案仍是草案阶段。 - 在类中难以做编译优化,
React团队一直在做前端编译层的优化工作,如常数折叠、内联展开及死码删除。
方案:
- 不要在 React 的循环、条件或嵌套函数中调用 Hook。
- 在React函数组件中调用Hook
防范措施
因 React 的内在设计原理,所以不可能绕过限制规则,但可在代码中禁止错误的使用方式。
工程化的东西最终应落地到工具上,其实只需在 ESLint 中引入 eslint-plugin-react-hooks 完成自动化检查就可以了在处理代码编写方式问题时,应优先想到从 Lint 工具入手
useEffect 与 useLayoutEffect的区别
相同点
使用方式上:
useLayoutEffect 的函数签名与 useEffect 相同,使用方式完全一致,甚至在一定程度上可以相互替换。
运行效果:
useEffect 与 useLayoutEffect 两者都是用于处理副作用。这些副作用包括改变 DOM、设置订阅、操作定时器等
不同点
使用场景:
大多数场景下可直接使用 useEffect,但代码引起页面闪烁,就推荐使用useLayoutEffect 处理
如有直接操作 DOM 样式或引起 DOM 样式更新的场景,更推荐使用 useLayoutEffect
独有能力:
- Effect:异步处理副作用;
- LayoutEffect:同步处理副作用;
设计原理
标记为 HookLayout 的 effect 会在所有的 DOM 变更之后同步调用,所以可以使用它来读取 DOM 布局并同步触发重渲染。
计算量较大的耗时任务必然会造成阻塞,所以就需根据实际情况酌情考虑。如果非必要情况下,使用标准的 useEffect 可以避免阻塞
useEffect 依赖为空数组与 componentDidMount 区别
在 render 执行之后,componentDidMount 会执行,如果在这个生命周期中再一次 setState ,会导致再次 render ,返回了新的值,浏览器只会渲染第二次 render 返回的值,这样可以避免闪屏。
但是 useEffect 是在真实的 DOM 渲染之后才会去执行,这会造成两次 render ,有可能会闪屏。
实际上 useLayoutEffect 会更接近 componentDidMount 的表现,它们都同步执行且会阻碍真实的 DOM 渲染的。
- Post title: React 知识点总结
- Create time: 2022-11-11 13:15:00
- Post link: 2022/11/11/React知识点总结/
- Copyright notice: All articles in this blog are licensed under BY-NC-SA unless stating additionally.