什么是 React 组件
React 组件允许用户将 UI 拆分成独立可复用的代码片段,并对每个片段进行独立构思。React 组件从概念上类似于 JavaScript 函数,接受任意的入参 Props,返回用于描述页面展示内容的 React 元素。
React 组件分类
按定义分类
- 类组件,使用 ES6 的 class 定义,维护 state,有生命周期
- 函数组件,使用普通函数定义,可以通过 hooks 维护状态和副作用
按状态分
- 有状态组件,组件返回结果,受时间、空间或上下文影响
- 无状态组件,通常是纯展示 UI 组件,容易复用
按定位分
- 展示型组件,接收 props,负责 UI 展示
- 容器组件,管理 states,负责数据获取和组件间通信,多用于状态提升
按 React 内置类型分类
- 有状态组件
- ClassComponent,由 class 创建
- ContextProvider,由 createContext 创建
- 无状态组件
- IndeterminateComponent,FunctionCompoent 挂载前的初始类型
- FunctionComponent,即函数组件
- ForwardRef,由 React.forwardRef 创建,接收 ref 并转发给子组件
- MemoComponent,由 React.memo 创建,条件渲染子组件
- SimpleMemoCompoent,由 React.memo 创建且不指定条件
- FiberNode
- HostRoot,由 ReactDOM.render 创建
- HostPortal,由 React.createPortal 创建,多用于模态框
- HostComponent,对应元素节点
- HostText,对应文本节点
- 内置类型
- Fragment,分组子列表,无需向 DOM 添加额外节点,可用短语法<>
- Profiler,测量 React 应用多久渲染一次以及渲染一次的“代价”
- StrictMode,严格模式,用来突出显示应用程序中潜在问题的工具
- Suspense,等待目标代码加载,并且可以指定一个加载界面,在用户等待时显示
- PureCompoent,浅层对比 prop 和 state 实现
- 有状态组件
类组件和函数组件的区别
说明 | 类组件 | 函数组件 |
---|---|---|
回调钩子 | 生命周期 | useEffect / useLayoutEffect |
this | 有,事件处理函数需绑定 this | 无 |
state | 有,this.setState 更新 | 无,useState / useReducer 引入 |
实例化 | 是 | 否 |
性能 | 现代浏览器中,闭包和类的原始性能只有在极端场景才会有明显差别 | 使用 Hooks 某些情况更加高效,避免了 class 需要的额外成本,如创建类实例和在构造函数绑定事件处理器的成本,符合语言习惯的代码不需要很深的组件库嵌套 |
受控组件和非受控组件的区别
- 受控组件
- React 的 state 是表单元素的“唯一数据源”,控制用户输入过程中表单发生的操作
- 表单元素的 value 跟随 state 变化,默认值由 defaultValue 设置
- 表单元素需要被 React 组件包裹
- 每种数据变化都需要编写事件处理函数
- 不支持 value 只读的表单元素,如
<input type="file" />
的 value 由用户设置
1 | export default function MyInput(props) { |
- 非受控组件
- 表单数据交由 DOM 节点处理
- 使用 ref 从 DOM 节点获取表单数据
- 表单元素无需被 React 组件包裹
- 只关心业务需要的数据变化,减少代码量
- 集成 React 和非 React 代码,不推荐使用
1 | export default function MyInput(props) { |
什么是高阶组件
- 高阶组件(Higher-Order Components,HoC)是参数为组件,返回值为新组件的函数,某种角度上就是高阶函数
- 高阶组件是 React 中复用组件逻辑的一种高级技巧
- 高阶组件不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式
什么是 Pure Component
React.PureComponent
与 React.Component
相似,区别是 React.PureComponent
并未直接实现 shouldComponentUpdate
,它是以浅层对比 prop 和 state 方式实现了 shouldComponentUpdate
:React.PureComponent
无法检查对象的深层差别, prop 和 state 使用深层数据结构时,调用 forceUpdate()
来确保组件正确更新,使用 immutable 对象 加速嵌套数据的比较。
展示组件和容器组件的区别
React 组件按照用途可以分为展示组件和容器组件。
React 推荐所有新组件,无论是展示组件,还是容器组件,都采用函数组件 + Hook 方式编写
- 展示组件
- 关心页面 UI,有自己的 HTML 标签和样式
- 如果有状态,仅与 UI 相关。与其他组件、store 无关
- 不关心数据源,通过 props 获取数据,并执行回调
- 容器组件
- 关心功能实现,无自己的 HTML 标签和样式
- 有状态。包含请求数据源等副作用。状态提升时,维护多个子组件的状态
- 可以由第三方库生成,如 react-redux 的
connect()
和 Relay 的createFragmentContainer
React 分离展示组件和容器组件的优势:
- 关注点分离,便于维护
- 提高展示组件的复用度,便于调整 UI
- 便于通过如
this.props.children
传递组件本身,减少相同 props 层层传递
如何劫持 React 组件提高组件复用度
劫持 React 组件又被称为渲染劫持,即将已有组件包装,注入新属性和功能,输出高阶组件,来实现组件复用。
劫持需要遵守高阶组件的约定:
- 不要改变原始组件,仅组合组件
- 保持组件的接口与已有组件相似,透传与自身无关的 props 给已有组件
- 最大化可组合性,确保函数签名类型一致,输入函数,返回函数,输入组件,返回组件
- 包装显示名称便于调试,如
withSubscription(CommentList)
如何设计一个 React 组件
- 将设计好的 UI 划分为组件层级
- 根据单一功能原则分离 UI 与数据源的结构一一对应
- 明确组件的包含关系
- 用 React 创建一个静态版本
- 将静态数据通过 props 父组件到子组件单向传递
- 构建应用
- 简单应用,自上而下,从高层组件到低层组件构建
- 大型应用,自下而上,从低层组件到高层组件构建,同时为低层组件编写测试
- 确定 UI state 的最小(且完整)表示
- 排除通过 props 传递来的数据
- 排除不随时间变化的数据
- 排除可以由其他 state 或 props 计算得出的数据
- 确定 state 放置位置
- 找出根据 state 渲染的所有组件
- 找出这些组件的共同上级组件
- state 应该放置在共同上级组件或者更高层级的组件中
- 添加反向数据流
- state 只能由拥有它们的组件更改
- 在该组件添加修改 state 的回调函数
- 将该回调函数通过 props 传递给子组件,在子组件中,如事件处理函数中调用
React 组件与 Web Components 共存的最佳实践
- 访问 Web Components 的命令式 API:使用 ref 与 DOM 节点进行交互
- 引入第三方 Web Components:编写 React 组件包装该 Web Components
- Web Components 触发事件:React 组件中手动添加事件处理器来处理事件
React.Suspence 组件和 React.lazy 函数有什么作用?
React.Suspense 可以指定加载指示器,以防其组件树中的某些子组件尚未具备渲染条件。它的 fallback 属性接受任何在组件加载过程中你想展示的 React 元素(通常为 loading 指示器)。
React.lazy 函数能让你像渲染常规组件一样处理动态引入的组件,即懒加载。React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 default export 的 React 组件。
1 | // 该组件是动态加载的 |
React.Suspense 组件和 React.lazy 函数通常配合使用来实现动态引入(懒加载)和优雅降级(loading 指示器)。
当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用哪个API?
static getDerivedStateFromError(error)
componentDidCatch(error, info)
static getDerivedStateFromError()
此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state 来处理降级渲染。
componentDidCatch()
此生命周期在后代组件抛出错误后被调用。 它接收两个参数:error —— 抛出的错误。info —— 带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息。它应该用于记录错误之类的情况。
Error boundaries 是 React 提供的用来处理错误的方案。Error boundaries 是 React 组件,它会在其子组件树中的任何位置捕获 JavaScript 错误,并记录这些错误,展示降级 UI 而不是崩溃的组件树。Error boundaries 组件会捕获在渲染期间,在生命周期方法以及其整个树的构造函数中发生的错误。如果 class 组件定义了生命周期方法 static getDerivedStateFromError()
或 componentDidCatch()
中的任何一个(或两者),它就成为了 Error boundaries。
1 | class ErrorBoundary extends React.Component { |
为什么列表中需要有不重复的key,如果有重复的 key 更新时会发生什么?
在一个组件中渲染列表时,需要给每个列表元素分配一个 key 属性,key 帮助 React 识别哪些元素改变了,比如被添加或删除。不指定显式的 key 值时,React 将默认使用索引作为列表项目的 key 值。
当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。当从头部插入式,React 无法意识到是从头部插入,而是会逐个更新,这会带来性能问题。新增 key 之后,使得更新的效率提高了。
React 性能优化方案
- 使用生产版本 React
- 使用 TerserPlugin 来对代码进行压缩
- 使用开发者工具中的分析器对组件进行分析
- 虚拟化长列表
- 避免调停,通过覆盖生命周期方法 shouldComponentUpdate 来进行提速
- 使用 React.PureComponent 对煎蛋 props 和 state 进行浅比较
- 使用不可变对象,例如使用
Object.assign
来更新对象