React 基础理论 & API 使用
本文主要记录一些关于 React 的基础理论、核心概念以及常用 API 的使用方法,供查漏补缺。
原文地址
React 简介
React 是一个由 Facebook(现在称为 Meta)开发的开源 JavaScript 库,主要用于构建用户界面,特别是单页应用程序(SPA)的开发。React 不仅限于 Web 开发,通过React Native,开发者还可以使用几乎相同的组件化开发方式来构建原生移动应用程序,实现了跨平台的代码复用。由于其灵活性和高效性,React 已成为现代 Web 开发中最受欢迎的前端库之一。
核心特点
组件化编程:React 将页面和功能分解为可复用的组件,每个组件可以管理自己的状态 and 渲染逻辑,大大提高了代码的可维护性和可重用性。Virtual DOM:引入虚拟 DOM 的概念,它是一个树形数据结构,用来表示真实 DOM 的抽象。当状态发生改变时,在Render阶段会先计算VDOM的最小更新,然后在Commit阶段生成真实 DOM,减少了浏览器的重排和重绘,提高了性能。声明式编程:React 使用声明式的方式定义页面的 UI 和状态逻辑,让代码更容易理解。JSX:允许开发者在 JS 中混写HTML-like的语法,这种语法糖被称为JSX,可以更直观和简洁的描述组件的结构。单向数据流:React 应用遵循单向数据流的原则,父组件向子组件传递状态(props)和回调函数,子组件通过调用这些回调来通知父组件的状态变更,这有助于保持数据流的清晰和可预测性。这点有别于Vue的双向绑定。
安装
在搭建 React 框架时,我们现在通常使用目前更主流、构建速度更快的 Vite,它是现代前端开发的优选脚手架。
bash
# 使用 Vite 创建项目
npm create vite@latest my-react-app -- --template react
如果你选择其他框架或工具链,也有对应的安装方式:
Next.js:npx create-next-app@latestUmiJS:使用create-umi
组件通讯方式
在 React 中,组件间的通信主要有以下几种方式:
- 通过
props向子组件传递数据:父组件通过属性将数据传递给子组件。 - 通过回调函数向父组件传递数据:父组件向子组件传递一个函数,子组件调用该函数并传入数据。
- 使用
Refs调用子组件暴露的方法 :通过forwardRef和useImperativeHandle钩子,父组件可以访问子组件内部定义的方法。 - 通过
Context进行跨组件通信 :使用createContext和useContext实现跨层级的状态共享。 - 使用状态管理库 :如
Redux、MobX或Zustand等进行全局状态管理。
生命周期
经典生命周期
在React 16.3之后的生命周期可以分为三个阶段:
挂载阶段(Mounting):
constructor: 组件实例化时调用,初始化 state 和绑定 thisgetDerivedStateFromProps: (React 16.3新增)在组件实例被创建后续更新时被调用,用于根据 props 来计算 staterender: 根据 state 和 props 渲染UI到虚拟 DOM。componentDidMount: 组件已经被渲染到 DOM 后调用,常用于发起网络请求、设置定时器等。
更新阶段(Updating):
getDerivedStateFromProps: 同挂载阶段。shouldComponentUpdate: 判断是否需要更新 DOM,返回 true/false。render: 状态或props改变时再次渲染 UI。getSnapshotBeforeUpdate: (React 16.3新增)在 DOM 更新前调用,可以获取一些信息用于在 componentDidUpdate 中使用。componentDidUpdate: 组件更新后立即调用,可以进行 DOM 操作或网络请求。
卸载阶段(Unmounting):
componentWillUnmount: 组件将要卸载时调用,清理工作如取消网络请求、清除定时器等。
从React 16.3开始,componentWillMount, componentWillReceiveProps, 和 componentWillUpdate 被标记为不安全的,并最终在React 17中被废弃。React推荐使用getDerivedStateFromProps和useState、useEffect等Hooks来替代。
Hooks 生命周期模拟
对于 React 函数组件,现在的实践更倾向于使用如下的 Hooks 生命周期:
-
useState: 用于组件内部状态管理。 -
useEffect: 用于处理副作用,可模拟以下生命周期:- 模拟挂载阶段 (
componentDidMount) : 依赖数组传空[]。
javascriptuseEffect(() => { /* 只在挂载后执行 */ }, []);- 模拟更新阶段 (
componentDidUpdate): 不传依赖数组或传入特定依赖。
javascriptuseEffect(() => { /* 每次渲染后都执行 */ }); useEffect(() => { /* count 变化后执行 */ }, [count]);- 模拟卸载阶段 (
componentWillUnmount) : 在useEffect中返回一个清理函数。
javascriptuseEffect(() => { return () => { /* 组件卸载前执行 */ }; }, []); - 模拟挂载阶段 (
-
useContext: 用于从上下文中消费值。 -
useRef: 用于持久化一个可变的引用对象,不会引起组件重新渲染。 -
useReducer: 用于有复杂状态逻辑的组件,替代某些 useState 的使用场景。 -
useCallback和useMemo: 用于优化性能,避免不必要的函数或计算的重新创建。
父子组件生命周期调用顺序
在函数组件中,挂载阶段的执行顺序如下:
- 父组件执行函数体(首次渲染)。
- 子组件执行函数体(首次渲染)。
- 子组件执行
useEffect(挂载完成)。 - 父组件执行
useEffect(挂载完成)。
更新阶段:
- 父组件重新渲染。
- 子组件重新渲染。
- 子组件
useEffect清理函数执行。 - 父组件
useEffect清理函数执行。 - 子组件
useEffect执行。 - 父组件
useEffect执行。
组件类 API
PureComponent
PureComponent 是 Component 的子类,是基于 shouldComponentUpdate 的一种优化方式。使用 PureComponent 的主要优点在于它自动执行了浅比较来检查 props 和 state 是否有变化,没有变化的时候不会重新渲染,从而提高了性能,减少了不必要的计算和 DOM 操作。
jsx
import React, { PureComponent } from 'react';
class MyComponent extends PureComponent {
render() {
return (
<div>
{this.props.text}
</div>
);
}
}
export default MyComponent;
memo
React.memo 是 React 中用于函数组件的性能优化手段,它是一个高阶函数,用来包装一个函数组件,并利用引用地址比较(浅比较)来决定是否重新渲染该组件。当组件的 props 没有发生变化时(基于浅比较),则跳过重新渲染,从而提高性能。
jsx
import React, { memo } from 'react';
const MyComponent = memo((props) => {
// 组件逻辑...
return <div>{props.text}</div>;
});
// 自定义比较函数:可以通过传递第二个参数给memo来自定义比较逻辑,这允许你实现深度比较或其他定制化的比较策略。
const MyComponent = memo((props) => {...}, (prevProps, nextProps) => {
// 自定义比较逻辑
// 返回true如果 props 没有变化,无需重新渲染
// 返回false如果 props 有变化,需要重新渲染
return prevProps.text === nextProps.text;
});
createRef
createRef 是 React 中管理 DOM 元素或组件实例引用的一个现代、灵活的方法,有助于处理表单、动画交互、原生DOM操作等场景。
jsx
class MyComponent extends React.Component {
myInputRef = React.createRef();
componentDidMount() {
// 在组件挂载后访问DOM元素
this.myInputRef?.current?.focus();
}
render() {
return <input type="text" ref={this.myInputRef} />;
}
}
forwardRef
forwardRef 是 React 中的一个高阶组件(HOC),它允许我们将 React 的 refs 转发到被包裹的组件中,即使这个组件是一个函数组件。这在需要访问子组件的 DOM 节点或者想要从父组件传递一些引用到子组件的场景下非常有用。极大地增强了函数组件的能力,使得它们在处理需要直接操作DOM或传递引用的场景下更加灵活和强大。
jsx
import React, { forwardRef } from 'react';
// 第一个参数是React.forwardRef接收的render函数,它接收两个参数:props和ref
const MyForwardedComponent = forwardRef((props, ref) => {
// 现在你可以在这个函数组件内部使用ref了
return <input type="text" ref={ref} {...props} />;
});
// 使用forwardRef的组件时,可以像普通组件那样使用ref
class ParentComponent extends React.Component {
myInputRef = React.createRef();
focusInput = () => {
this.myInputRef?.current?.focus();
};
render() {
return (
<>
<MyForwardedComponent ref={this.myInputRef} />
<button onClick={this.focusInput}>Focus Input</button>
</>
);
}
}
createContext
createContext 是 React 中的一个API,用于创建一个"context"对象。Context 提供了一种在组件树中传递数据的方式,而不必显式地通过每一个层级手动传递 props。这使得在不同层级的组件中共享数据变得简单且高效,特别适合管理如主题、语言设置、认证信息等全局状态。
基本用法:
创建 Context: createContext(defaultValue)
jsx
import React from 'react';
// 创建一个context
const MyContext = React.createContext('light');
Provider组件: 注入值上下文
jsx
class App extends React.Component {
state = {
theme: 'light',
};
render() {
return (
// 通过Provider组件向上下文中注入值
<MyContext.Provider value={this.state.theme}>
<ComponentThatNeedsTheContext />
</MyContext.Provider>
);
}
}
Consumer组件: 读取上下文的方法
jsx
function ComponentThatNeedsTheContext() {
return (
<MyContext.Consumer>
{theme => /* 使用theme值 */}
</MyContext.Consumer>
);
}
或者使用 useContext Hook:
jsx
import React, { useContext } from 'react';
function ComponentThatNeedsTheContext() {
const theme = useContext(MyContext);
// 现在可以使用theme值
return <div>{theme}</div>;
}
注意事项:
Context 会随着组件树的遍历而传递,无论组件是否使用了这个 Context。因此,应当谨慎使用,避免创建过多的 Context,尤其是嵌套使用时。
createElement
createElement 在我们的平时使用中较少,它用于创建 React 元素,是构成用户界面的基本单位,我们常写的 JSX 就是 createElement 的语法糖,所以还是很有必要了解这个 API 的。
基本语法:
js
React.createElement(
type, // 通常是一个字符串(对应HTML标签名)或一个React组件(函数组件或类组件的构造函数)。
[props], // 一个对象,用于传递给组件的属性。它可以包含事件处理器、样式等。
[...children] // 代表组件的子元素,可以是一个React元素、字符串或数字,也可以是这些类型的数组。
)
示例:
jsx
const element = React.createElement(
'div',
{ id: 'example', className: 'box' },
'Hello, world!'
);
这段代码等同于下面的JSX写法:
jsx
<div id="example" className="box">
Hello, world!
</div>
cloneElement
cloneElement 是 React 提供的一个方法,用于克隆并返回一个新的 React 元素,同时可以修改传入元素的 props,甚至可以添加或替换子元素。这个方法常用于在高阶组件中,或者任何需要基于现有元素创建一个具有额外 props 或不同子元素的新元素的场景。
基本语法:
jsx
React.cloneElement(
element, // 要克隆的React元素
[props], // 一个对象,包含了要添加或覆盖到原始元素props上的新属性
[...children] // 可选的,用于替换或追加子元素到克隆后的元素中
)
自定义 HOC
高阶组件(Higher-Order Components, HOC)是 React 中用于重用组件逻辑的一种高级技术。HOC 本身不是 React API 的一部分,而是一种从函数式编程原则中借来的模式。一个 HOC 是一个接受组件作为参数并返回一个新的增强组件的函数。
jsx
function withEnhancement(WrappedComponent) {
return function EnhancedComponent(props) {
// 添加额外的props或逻辑
const newProps = { ...props, enhancedProp: "Enhanced Value" };
// 渲染被包装的组件,并传递新的props
return <WrappedComponent {...newProps} />;
};
}
注意事项:
- 不要修改传入组件的props: 最好是通过组合新的 props 而不是修改原有的 props来保持纯净性。
- 命名约定 : 通常 HOC 函数名以
with开头,以表明它是一个 HOC。 - 文档和测试: 编写清晰的文档说明 HOC 的功能和用法,并确保充分测试,以防止引入bug。
Hooks
React Hooks 是React 16.8版本引入的一个新特性,在不编写类的情况下使用 React 的状态和其他生命周期特性。Hooks 使函数组件的功能更加丰富,使得函数组件逻辑更易于理解和重用。
useState
允许在函数组件中添加状态(state)。它返回一个状态变量和一个用来更新这个状态的函数。
jsx
const [count, setCount] = useState(0);
useEffect
useEffect 是 React Hooks 系统中的一个重要成员,它主要用于执行副作用操作,比如数据获取、订阅或者手动修改 DOM 等。此 Hook 允许你同步副作用与 React 组件的生命周期,替代了类组件中的一些生命周期方法,如 componentDidMount、componentDidUpdate 和 componentWillUnmount。
jsx
// useEffect 接收两个参数:一个包含副作用操作的函数,和一个依赖项数组
useEffect(() => {
// 副作用操作:订阅或数据获取等
document.title = `You clicked ${count} times`;
// 可选的清理函数,用于在下次effect执行前或组件卸载时清理副作用
return () => {
// 清理操作,例如取消网络请求或移除事件监听器
};
}, [count]); // 依赖项数组,当这些值变化时触发effect重新执行
useContext
useContext 是 React Hooks 系统中的一个 API,它使你能够在组件树中无需通过 props 逐层传递,就能访问到全局状态或其他组件上下文中的值。这对于管理如主题、语言、认证信息等跨多个组件共享的数据尤为有用。
jsx
import React, { useContext } from 'react';
function ComponentThatNeedsTheContext() {
const theme = useContext(MyContext);
// 现在可以使用theme值
return <div>{theme}</div>;
}
useRef
useRef 是 React Hooks 系统中的一个API,它用于创建一个可变的引用对象(ref),这个对象的.current属性被初始化为传递的参数(initialValue)。useRef的主要用途是在渲染之间持久化一个可变的值,并且可以用来直接访问 DOM 元素或在函数组件之间保持一些状态。
jsx
import React, { useRef } from 'react';
function TextInputWithFocusButton() {
// 初始化一个ref,用来存放input元素的引用
const inputEl = useRef(null);
const onButtonClick = () => {
// 当按钮被点击时,让input元素获取焦点
inputEl?.current?.focus();
};
return (
<>
{/* 将input元素的引用赋给useRef返回的对象 */}
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
useReducer
useReducer 是React中的一个Hook,它用于管理组件中的状态,特别适用于状态更新逻辑较复杂的场景。
基本用法:
jsx
import React, { useReducer } from 'react';
// 定义reducer函数
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
};
// 初始化状态
const initialState = { count: 0 };
function Counter() {
// 使用useReducer,传入reducer函数和初始状态
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
注意事项:
- 确保reducer函数是纯函数,即给定相同输入始终产生相同输出,不产生副作用。
- 选择合适的状态管理方式,对于简单的状态管理,useState可能更直观易用。
- 利用useCallback来记忆化dispatch函数,避免在每个渲染周期都创建新的函数引用,进而减少不必要的子组件重渲染。
useMemo
useMemo 是React中的一个Hook,用于优化性能,避免在每次渲染时都进行复杂的计算。它让你能够 memoize(记忆化)一个值,这个值是基于某些依赖项计算出来的,只有当这些依赖项改变时,才会重新计算这个值。
基本用法:
jsx
import React, { useMemo } from 'react';
function MyComponent({ list }) {
// 使用useMemo进行性能优化
const sortedList = useMemo(() => {
console.log('Sorting list');
return list.sort((a, b) => a - b);
}, [list]); // 依赖项数组,当list变化时才重新计算sortedList
return (
<div>
{sortedList.map(item => (
<div key={item}>{item}</div>
))}
</div>
);
}
注意事项:
- 不要过度使用: 虽然useMemo可以帮助优化性能,但是不必要的使用反而可能导致额外的性能开销,特别是在计算简单或频繁变化的值时。
- 理解其限制: useMemo不会阻止其依赖项内的对象或数组的内部变化触发重渲染。只有当依赖项的引用本身发生变化时,才会触发重计算。
- 与React.memo区别: React.memo是一个高阶组件,用于记忆化整个组件,防止不必要的渲染,而useMemo是记忆化组件内部的某个值或计算结果。
useCallback
useCallback 是 React中 的另一个性能优化 Hook,它用于记忆化函数。与 useMemo 相似,useCallback 也用于避免在每次渲染时都进行新的函数引用,但它的主要应用场景是当这些函数作为 props 传递给子组件时,帮助子组件避免不必要的重新渲染。
jsx
import React, { useCallback, useState } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
// 使用useCallback记忆化increment函数
const increment = useCallback(() => {
setCount(count + 1);
}, [count, setCount]); // 依赖项数组,当这些值变化时,才会生成新的increment函数
return <ChildComponent onClick={increment} />;
}
function ChildComponent({ onClick }) {
// ...
}
注意事项:
- 与useMemo的区别: useMemo 适用于记忆化计算值或对象,而 useCallback 专门用于记忆化函数。
- 避免闭包陷阱: 在使用 useCallback 时,需要注意函数内部引用的外部变量也应包含在依赖项数组中,以确保正确的重渲染逻辑。