什么是 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
2
3
4
5
6
7
export default function MyInput(props) {
const [username, setUsername] = useState('');
const handleChange = useCallback(() => {
setUsername(e.target.value);
}, []);
return <input name="username" value={username} onChange={handleChange} />
}
  • 非受控组件
    • 表单数据交由 DOM 节点处理
    • 使用 ref 从 DOM 节点获取表单数据
    • 表单元素无需被 React 组件包裹
    • 只关心业务需要的数据变化,减少代码量
    • 集成 React 和非 React 代码,不推荐使用
1
2
3
4
5
6
7
8
9
export default function MyInput(props) {
const {defaultName} = props;
const inputRef = React.createRef();
const handleChange = useCallback((e) => {
console.log(inputRef.current.value);
e.preventDefault;
}, []);
return <input defaultValue={defaultName} ref={inputRef} onChange={handleChange}/>
}

什么是高阶组件

  • 高阶组件(Higher-Order Components,HoC)是参数为组件返回值为新组件的函数,某种角度上就是高阶函数
  • 高阶组件是 React 中复用组件逻辑的一种高级技巧
  • 高阶组件不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式

什么是 Pure Component

React.PureComponentReact.Component 相似,区别是 React.PureComponent 并未直接实现 shouldComponentUpdate,它是以浅层对比 prop 和 state 方式实现了 shouldComponentUpdateReact.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
2
3
4
5
6
7
8
9
10
11
12
13
// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
return (
// 显示 <Spinner> 组件直至 OtherComponent 加载完成
<React.Suspense fallback={<Spinner />}>
<div>
<OtherComponent />
</div>
</React.Suspense>
);
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以显降级 UI
return { hasError: true };
}

render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的降级 UI
return <h1>Something went wrong.</h1>;
}

return this.props.children;
}
}

为什么列表中需要有不重复的key,如果有重复的 key 更新时会发生什么?

在一个组件中渲染列表时,需要给每个列表元素分配一个 key 属性,key 帮助 React 识别哪些元素改变了,比如被添加或删除。不指定显式的 key 值时,React 将默认使用索引作为列表项目的 key 值。

当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。当从头部插入式,React 无法意识到是从头部插入,而是会逐个更新,这会带来性能问题。新增 key 之后,使得更新的效率提高了。

React 性能优化方案

  1. 使用生产版本 React
  2. 使用 TerserPlugin 来对代码进行压缩
  3. 使用开发者工具中的分析器对组件进行分析
  4. 虚拟化长列表
  5. 避免调停,通过覆盖生命周期方法 shouldComponentUpdate 来进行提速
  6. 使用 React.PureComponent 对煎蛋 props 和 state 进行浅比较
  7. 使用不可变对象,例如使用 Object.assign 来更新对象