1、概念
React 是一个用于构建用户界面的 JavaScript 库,由 Facebook(现 Meta)开发并开源。它专注于视图层,采用声明式编程范式,让开发者能够高效、灵活地创建交互式 UI;
特点
1、组件化
React 应用由独立的、可复用的组件构成。每个组件封装了自己的结构(HTML 模版)、样式和逻辑,通过组合这些组件可以搭建出复杂的界面。组件可以是函数组件或类组件。
2、虚拟 DOM
React 在内存中维护一棵轻量级的虚拟 DOM 树。当组件的状态或属性发生变化时,React 会先计算出虚拟 DOM 的变化(diffing 算法),然后批量更新真实 DOM,从而减少昂贵的 DOM 操作,提升性能。
3、声明式编程
开发者只需描述界面应该呈现的状态(如"如果数据是加载中,显示 loading;否则显示列表"),React 会自动管理和更新 UI 来匹配这个状态,使代码更易理解和维护。
4、单向数据流
数据从父组件通过 props 向下传递到子组件,子组件不能直接修改父组件的数据,只能通过回调函数通知父组件。这种单向流动让数据变化更可预测。
5、JSX语法
一种 JavaScript 语法扩展,允许在 JavaScript 代码中编写类似 HTML 的标记,最终被编译为普通的 JavaScript 对象。JSX 让组件的结构更直观。
2、组件
1、组件的两种主要形式
函数组件:最简单的定义方式,它是一个接收 props 对象并返回 JSX 的 JavaScript 函数
js
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
类组件:使用 ES6 class 定义,必须继承 React.Component,并实现 render() 方法。类组件可以拥有内部状态(state)和生命周期方法
js
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
2、核心概念:Props 与 State
Props(属性):父组件传递给子组件的只读数据,子组件不能修改自己的 props
State(状态):组件内部管理的数据,可以通过 setState(类组件)或 useState Hook(函数组件)来更新,状态的改变会触发组件重新渲染
js
class Welcome extends React.Component {
state = {
n:123
}
function add(){
this.setState({ n: 20 })
}
render() {
return <h1>Hello, {this.state.n}</h1>;
}
}
3、事件
react中dom事件采用小驼峰命名,js中dom事件是小写,react中自定义事件通过props传递接收并执行
函数组件
js
function Button() {
const handleClick = () => {
console.log('按钮被点击');
};
return <button onClick={handleClick}>点击我</button>;
}
类组件(注意 this 绑定)
如果没有特殊处理,在事件处理函数中,this指向undefined
两种处理方式:1、通过bind指向this;2、声明箭头函数
js
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = { isOn: true };
// 为了在回调中使用 `this`,需要绑定
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.props.fn(123);
this.setState(prevState => ({ isOn: !prevState.isOn }));
}
// handleClick = () => {}
render() {
return (
<button onClick={this.handleClick} onMouseEnter={()=>{}}>
{this.state.isOn ? 'ON' : 'OFF'}
</button>
);
}
}
4、组件通信
父传子:父组件传递数据给子组件,子组件通过props接收
js
// 子组件:接收 props
function Child({ name, age }) {
return (
<div>
<p>姓名:{name}</p>
<p>年龄:{age}</p>
</div>
);
}
// 父组件:传递数据
function Parent() {
const user = { name: '张三', age: 20 };
return <Child name={user.name} age={user.age} />;
}
子传父:子组件调用props接收的函数,把参数传回去
js
// 子组件:接收父组件传来的回调函数
function Child({ onMessage }) {
const handleClick = () => {
const data = '子组件的数据';
onMessage(data); // 调用父组件传入的函数
};
return <button onClick={handleClick}>发送消息给父组件</button>;
}
// 父组件:定义回调函数并传给子组件
function Parent() {
const handleChildMessage = (msg) => {
console.log('来自子组件的消息:', msg);
};
return <Child onMessage={handleChildMessage} />;
}
3、理解setState
1、基本用法:setState({}, callback)
2、异步性:如果每次 setState 都立即触发渲染,那么多次调用会造成不必要的性能开销;为避免频繁的重复渲染,调用 setState 后,状态并不会立即改变,而是进入一个更新队列,React 会在事件处理函数执行完后批量处理这些更新,然后合并状态并重新渲染(注意:只有当setState处于html事件中时才是异步的)
3、合并更新:对象形式的 setState 会被合并,相同的属性会被后赋值的覆盖,函数形式的 setState 可以基于前一个状态计算新状态
js
class Index extends React.Component {
state = {
count: 0,
};
handleClick = () => {
this.setState(
{
count: this.state.count + 1,
},
() => {
console.log(this.state.count);
}
);
};
render() {
return (
<div>
<h1>hello world</h1>
</div>
);
}
}
4、生命周期(16.3 新版本)
挂载阶段:
constructor:初始化 state 和绑定事件处理函数
static getDerivedStateFromProps(props, state):静态方法,在渲染前调用,根据 props 更新 state
render:解析 JSX,生成虚拟 DOM
componentDidMount:组件挂载后立即调用,适合处理数据请求、添加订阅等副作用操作
更新阶段:
static getDerivedStateFromProps(props, state):静态方法,在组件因 props 更新、setState 或 forceUpdate 而重新渲染时都会被调用
shouldComponentUpdate(nextProps, nextState):决定组件是否重新渲染,常用于性能优化
render:重新解析 JSX 以生成新的虚拟 DOM
getSnapshotBeforeUpdate(prevProps, prevState):在最近一次渲染输出(提交到 DOM 之前)之前调用。它让组件能在 DOM 更新前捕获一些信息(如滚动位置),返回值将作为参数传递给 componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot):组件更新完成后调用,可以操作更新后的 DOM,或根据 snapshot 进行后续处理
卸载阶段 :
componentWillUnmount:组件卸载前调用,用于执行清理操作,如取消网络请求、移除定时器等
新版本移除了componentWillMount、componentWillReceiveProps、componentWillUpdate等生命周期函数
5、高阶组件
1、高阶函数:接收一个函数作为参数,且返回一个函数
2、高阶组件:接收一个组件,返回一个新组件
3、高阶组件的优势:
逻辑复用:当多个组件需要共享相同的逻辑(如权限检查、日志记录、数据获取、样式注入)时,可以将这些逻辑提取到 HOC 中
横切关注点:将关注点(如日志、认证)与组件本身分离,使组件更纯粹、更专注于 UI 渲染
增强组件:在不修改原始组件代码的情况下,为组件添加额外功能
js
// 定义高阶组件
function fn<P extends object>(WrappedComponent: ComponentType<P>) {
return class fnComponent extends Component<P & { loading?: boolean }> {
render() {
const { loading, ...props } = this.props;
if (loading) {
return <div>加载中...</div>;
}
return <WrappedComponent {...(props as P)} />;
}
};
}
// 使用高阶组件
const UserCard: React.FC<UserProps> = ({ name }) => <div>用户:{name}</div>;
// 传入一个组件,返回一个新组件,该组件为高阶组件
const UserCardWithLoading = withLoading(UserCard);
const App = () => (
<div>
<UserCardWithLoading name="张三" loading={true} />
<UserCardWithLoading name="李四" loading={false} />
</div>
);
6、Ref
在 React 中,ref提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素;它让我们能够直接操作子元素或组件实例,适用于需要命令式操作(如焦点管理、媒体播放、与第三方 DOM 库集成)的场景
1、类组件
js
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef(); // 创建 ref
this.comRef = React.createRef();
}
componentDidMount() {
this.myRef.current.focus(); // 访问 DOM 节点
this.comRef.current.focus(); // 访问 组件实例
}
render() {
return <div>
<input ref={this.myRef} />
<Component ref={this.comRef} />
</div>;
}
}
2、函数组件
js
import { useRef, useEffect } from 'react';
function MyComponent() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
3、forwardRef
在 React 中,ref 的作用是获取对 DOM 节点或类组件实例的引用;但函数组件默认没有实例,因此不能像类组件那样直接接收 ref 属性(即给函数组件添加 ref 会收到警告,且 ref 不会自动传递给组件内部的任何元素);forwardRef 正是为了允许函数组件接收 ref,并将其转发给子组件或 DOM 元素
1、使用 forwardRef 包装函数组件
js
import React, { useRef, useEffect } from 'react';
// 使用 forwardRef 包装函数组件
const FancyInput = React.forwardRef((props, ref) => {
return <input ref={ref} className="fancy-input" {...props} />;
});
function Parent() {
const inputRef = useRef();
useEffect(() => {
// 父组件可以直接操作子组件内部的 input 元素
inputRef.current.focus();
}, []);
return <FancyInput ref={inputRef} placeholder="请输入" />;
}
2、阶组件中的 ref 转发
当使用高阶组件(HOC)包装组件时,原有的 ref 会指向 HOC 外层容器,而不是被包装的组件。forwardRef 可以用来将 ref 传递给被包装的组件
js
// 一个简单的日志 HOC
function withLog(WrappedComponent) {
class WithLog extends React.Component {
componentDidMount() {
console.log('组件已挂载');
}
render() {
// 注意:这里 ref 是特殊属性,不会作为 props 传递
return <WrappedComponent {...this.props} />;
}
}
return WithLog;
}
// 使用 forwardRef 解决 ref 传递问题
function withLog(WrappedComponent) {
class WithLog extends React.Component {
componentDidMount() {
console.log('组件已挂载');
}
render() {
const { forwardedRef, ...rest } = this.props;
return <WrappedComponent ref={forwardedRef} {...rest} />;
}
}
return React.forwardRef((props, ref) => {
return <WithLog {...props} forwardedRef={ref} />;
});
}
3、结合 useImperativeHandle 自定义暴露给父组件的实例值
js
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
// 自定义暴露给父组件的实例值
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
}
}));
return <input ref={inputRef} {...props} />;
});
function Parent() {
const fancyRef = useRef();
const handleFocus = () => {
fancyRef.current.focus();
};
const handleClear = () => {
fancyRef.current.clear();
};
return (
<>
<FancyInput ref={fancyRef} />
<button onClick={handleFocus}>聚焦</button>
<button onClick={handleClear}>清空</button>
</>
);
}
7、context
React Context 提供了一种在组件树中共享数据的方式,而无需通过 props 逐层手动传递。它主要用于解决 prop drilling(属性钻取)的问题,即当多个深层次组件需要访问同一份数据时,可以使用 Context 跨层级直接传递
1、核心概念
Context 对象:通过 React.createContext 创建,包含 Provider 和 Consumer 两个组件
Provider:数据的提供者,接收一个 value 属性,将数据传递给所有子孙组件
Consumer:数据的消费者(函数组件中可以使用 useContext Hook 替代)
2、基本使用
js
import React, { useState, useContext, createContext } from 'react';
// 1. 创建 Context
const ThemeContext = createContext();
// 2. 创建 Provider 组件(封装了状态逻辑)
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
};
// 传递给子组件的值
const value = {
theme,
toggleTheme
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// 3. 消费 Context 的组件
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff'
}}
>
当前主题:{theme},点击切换
</button>
);
}
// 4. 顶层组件
function App() {
return (
<ThemeProvider>
<div>
<h1>Context 主题示例</h1>
<ThemedButton />
</div>
</ThemeProvider>
);
}
export default App;
8、PureComponent 纯组件(类组件)、React.memo
1、PureComponent (性能优化)
React.PureComponent 与 React.Component 类似,区别在于它已经内置了 shouldComponentUpdate 的实现,会对新旧 props 和 state 进行浅比较,只有当数据发生变化时才会触发重新渲染
注意:PureComponent 只进行一层比较(shallow compare),对于嵌套对象或数组,修改内部属性时引用并未改变,因此组件不会更新,如果需要对比数组,建议使用不可变数据,每次更新都返回新的对象/数组
应用:点击"添加"按钮时,只有新增的 ListItem 会渲染,已有的项因为 props.item 引用未变(利用展开运算符创建了新数组,但原有项对象引用未变),所以不会重新渲染
js
class ListItem extends React.PureComponent {
render() {
const { item } = this.props;
console.log('ListItem 渲染:', item.id);
return <div>{item.name}</div>;
}
}
class List extends React.Component {
state = {
items: [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' }
]
};
addItem = () => {
const newItem = { id: Date.now(), name: 'New' };
this.setState({ items: [...this.state.items, newItem] });
};
render() {
return (
<div>
<button onClick={this.addItem}>添加</button>
{this.state.items.map(item => (
<ListItem key={item.id} item={item} />
))}
</div>
);
}
}
2、React.memo(性能优化)
React.memo 是一个高阶函数组件,它会比较当前 props 与下一个 props 的属性值,当新的 props 与旧的 props 属性值不相等时,才会重新渲染组件,默认是进行浅比较
js
const ChildComponent = React.memo((props) => {
// 子组件内容
return (
<div>
{props.value}
</div>
);
});
9、Portal
React Portal 是 React 提供的一种将子节点渲染到父组件 DOM 层次结构之外的 DOM 节点中的能力。它允许你将组件的内容"传送"到 DOM 树的任意位置,同时保留 React 组件树的上下文(如 props、事件冒泡等)
1、应用场景
模态框(Modal):需要显示在所有页面内容之上,通常放在 的直接子级
工具提示(Tooltip):需要相对于某个元素定位,但不应被父元素的 overflow: hidden 或 z-index 限制
悬浮卡片(Dropdown)、通知提醒等
2、基本使用
js
import React from 'react';
import ReactDOM from 'react-dom';
function PortalExample() {
return ReactDOM.createPortal(
<div className="portal-content">
这段内容被渲染到 body 下
</div>,
document.body // 目标 DOM 节点
);
}
10、hooks
useState
useState 用来存储变量,useState 接收一个初始值,返回一个数组,数组的第一个元素是变量的值,第二个元素是更新变量的函数,当改变变量的值时,组件会重新渲染
js
const [ count , setCount ] = useState(0)
<button onClick={()=>{setCount(count+1)}}>{count}</button>
useReactive
useReactive 的使用和 useState 的使用一样,只是 useReactive 返回的是一个响应式的对象,而不是一个变量,更新对象的时候,组件会重新渲染
js
const state = useReactive({
count: 0
})
<button onClick={()=>{state.count++}}>{state.count}</button>
useSetState
useSetState 的使用和 useState 的使用一样,只是 useSetState 返回的是一个响应式的对象,而不是一个变量,更新对象的时候,组件会重新渲染
js
const [state, setState] = useSetState({
count: 0
})
<button onClick={()=>{setState({count: state.count + 1})}}>{state.count}</button>
useEffect
useEffect 是 React 中的一个钩子函数,用于处理副作用操作。副作用是指在组件渲染过程中,可能会对外部环境产生影响的操作,比如数据获取、订阅事件、操作 DOM 等
js
// dom初次渲染完成时执行,只执行一次,在此可以进行数据请求、订阅事件、操作 DOM 等操作
useEffect(() => {
},[])
// 当依赖值发生变化时执行(当依赖值为对象或数组时,只有当引用地址发生变化时才会执行,如果只是修改了对象或数组中的某个属性时,不会执行)
const [count, setCount] = useState(0);
useEffect(() => {
}, [count]);
// 清除副作用操作:副作用函数可以返回一个清除函数,用于清除副作用操作,比如取消订阅、清除定时器等
useEffect(() => {
const timer = setInterval(() => {
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
useLayoutEffect(不常用)
useLayoutEffect 是 React 提供的一个 Hook,其函数签名与 useEffect 完全相同,但执行时机不同。它会在真实DOM 变更之后,且浏览器进行绘制之前执行。这使得它适合在浏览器绘制前读取 DOM 布局并同步触发重新渲染;
应用场景:
1、测量 DOM 节点:获取元素尺寸、位置等,并基于这些值同步更新 UI(如 tooltip 定位、动态调整样式)
2、避免不必要的闪烁:当需要在渲染后立即修改 DOM,且不希望用户看到中间状态时
3、与第三方库集成:有些库需要直接操作 DOM 并依赖布局信息,可以在 useLayoutEffect 中进行初始化
4、滚动条位置恢复:在更新内容后需要立即恢复滚动位置
注意:由于 useLayoutEffect 是同步执行的,如果回调中执行了耗时操作,会延迟浏览器绘制,导致页面卡顿(会阻塞浏览器绘制)
js
useLayoutEffect(() => {
// 副作用逻辑
return () => {
// 清理逻辑(可选)
};
}, [dependencies]);
useMemo(性能优化)
useMemo 会在依赖项变化时重新计算新值,返回一个值(任何数据类型)
js
const ChildComponent =({ value })=> {
// 使用useMemo来避免在value变化时重复渲染
const expensiveOperation = useMemo(() => {
// 进行一些昂贵的操作
return performExpensiveOperation(value);
}, [value]);
return (
<div>
{expensiveOperation}
</div>
);
}
useCallback(性能优化)
useCallback 的用法与 useState 的用法基本一致,但最后会返回一个函数,用一个变量保存起来。返回的函数 a 会根据 b 的变化而变化,如果 b 始终未发生变化,a 也不会重新生成,避免函数在不必要的情况下更新
为什么需要该hook?
在 React 函数组件中,每次渲染都会重新创建内部定义的函数。如果将这些函数作为 props 传递给子组件,而子组件使用了 React.memo 进行优化,那么由于每次父组件渲染时函数引用都变了,子组件会认为 props 发生了变化,导致即使其他 props 没变,子组件也会重新渲染。useCallback 通过稳定函数引用,配合 React.memo 来减少这种不必要的渲染
js
const fn = useCallback(() => {
return function() {
console.log(b)
}
},[b])
useRef
useRef 是 React 提供的一个 Hook,它返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数;这个 ref 对象在组件的整个生命周期内保持不变(即每次渲染返回同一个对象),但可以修改它的 .current 属性来存储任意值
应用场景:
1、访问 DOM 元素
js
const scrollRef = useRef(null);
console.log(scrollRef.current);
<div ref={scrollRef} />
2、 存储可变值(跨渲染持久化)-> 当你需要一个值,在组件重新渲染后依然保持不变,并且修改它时不想触发重新渲染,就可以用 useRef
js
function Timer() {
const timerIdRef = useRef();
const startTimer = () => {
timerIdRef.current = setInterval(() => {
console.log('tick');
}, 1000);
};
const stopTimer = () => {
clearInterval(timerIdRef.current);
};
return (
<>
<button onClick={startTimer}>开始</button>
<button onClick={stopTimer}>停止</button>
</>
);
}
useCreation(性能优化)
来自于ahooks库,它是一个旨在强化和替代 useMemo 与 useRef 的自定义 Hook,主要用于精细化的性能优化
为什么需要 useCreation?
1、更可靠的 useMemo:React 官方文档曾提及,useMemo 不保证缓存的值永远不会被回收。在内存紧张时,React 可能会销毁之前缓存的值以便释放内存,下次渲染时即使依赖未变也会重新计算 。useCreation 则通过 useRef 来持有对象,保证了缓存值的稳定性,只要依赖不变,它绝不会被垃圾回收机制意外清除 。
2、避免 useRef 的重复创建开销:当使用 useRef 来存储一个创建开销很大的对象时,你可能会这样写:const subject = useRef(new Subject())。但问题在于,每次组件渲染,new Subject() 这个昂贵的构造函数都会被执行,尽管最终只有第一次的值被用到 。useCreation 接收一个工厂函数,它只在首次渲染和依赖变化时才执行这个函数创建对象,完美避免了重复的性能损耗
3、useCreation 的核心思想是结合 useRef 的持久化能力和手动依赖检查
js
const a = useCreation(() => {
return b + 1
},[b])
useMount
useMount 简化了使用 useEffect 的第二个参数
js
useMount(()=>{
// dom初始化完成
})
useUnmount
useUnmount 简化了使用 useEffect 的第二个参数
js
useUnmount(()=>{
// 组件销毁之前
})
useMemoizedFn
useMemoizedFn 是一个自定义的 React Hook,用于确定输入输出,useMemoizedFn 接受一个函数,返回一个函数,该函数会根据输入参数变化而变化,避免函数在不必要的情况下更新
js
// useMemoizedFn函数里面使用能拿到最新的b值
const getNewB = useMemoizedFn(() => {
return b + 1
})
useImperativeHandle
自定义暴露给父组件的实例值
js
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
// 自定义暴露给父组件的实例值
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
}
}));
return <input ref={inputRef} {...props} />;
});
function Parent() {
const fancyRef = useRef();
const handleFocus = () => {
fancyRef.current.focus();
};
const handleClear = () => {
fancyRef.current.clear();
};
return (
<>
<FancyInput ref={fancyRef} />
<button onClick={handleFocus}>聚焦</button>
<button onClick={handleClear}>清空</button>
</>
);
}
useInterval
useInterval 会返回一个函数,实现原理使用了 useEffect,可以直接使用,不用考虑要清除定时器,用法和 setInterval 一致
js
const getTime = useInterval(
() => {
console.log('定时器');
},
1000,
{ immediate: true },
);
getTime()
useDebugValue(不常用)
在开发 React 应用时,我们经常使用 React DevTools 来检查组件的 props、state 和 Hooks 状态。对于自定义 Hook,默认情况下 DevTools 只会显示 Hook 这样一个通用标签,无法直接看到 Hook 内部的具体状态;useDebugValue 允许你为自定义 Hook 添加一个描述性的标签,让调试信息更加直观
js
import { useState, useEffect, useDebugValue } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
}, [friendID]);
// 在 DevTools 中显示调试信息
useDebugValue(isOnline ? '在线' : '离线');
return isOnline;
}
自定义hook
在 React 16.8 引入 Hooks 之前,复用逻辑通常使用高阶组件(HOC)或 render props。这些模式虽然有效,但会导致组件树嵌套过深、代码难以理解。自定义 Hook 提供了一种更简洁、更直观的方式来共享逻辑,无需增加组件层级
应用规则:
1、名称必须以 use 开头:这是 React 的约定,用于区分普通函数和 Hook,同时帮助 lint 工具检查 Hook 规则
2、只能在函数组件或其他自定义 Hook 中调用 Hook:不能在普通 JavaScript 函数、循环或条件语句中调用 Hook
3、每次调用都有独立的状态:每次使用自定义 Hook 都会创建独立的 state 和 effect,互不影响
useWindowSize:监听窗口尺寸
js
import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // 空依赖表示只在挂载和卸载时执行
return size;
}
// 使用
function MyComponent() {
const { width, height } = useWindowSize();
return <div>窗口大小:{width} x {height}</div>;
}
11、错误边界
在 React 16 之前,组件内部的 JavaScript 错误会导致 React 内部状态被破坏,并在下一次渲染时触发难以理解的错误。React 16 引入了错误边界,使开发者能够优雅地处理局部 UI 的错误,避免整个应用白屏
错误边界是一个类组件,它定义了以下生命周期方法之一(或两者):
static getDerivedStateFromError(error):在子组件抛出错误后调用,用于更新 state 以显示备用 UI;它在渲染阶段调用,因此不允许副作用
componentDidCatch(error, info):在子组件抛出错误后调用,用于记录错误信息(如发送到日志服务);它可以在提交阶段调用,因此允许副作用
js
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state,下次渲染时显示备用 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你可以将错误记录到错误报告服务
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
// 自定义备用 UI
return <h1>出错了!请稍后重试。</h1>;
}
return this.props.children;
}
}
// 使用
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
12、redux(状态管理)
Redux 是一个可预测的 JavaScript 状态容器,通常与 React 等视图库一起使用,用于管理应用中的全局状态。它提供了一种集中式存储和管理状态的方式,并遵循严格的规则来更新状态,使状态变化可追踪、可调试;
1、为什么需要 Redux?
在复杂的前端应用中,组件间共享状态可能变得混乱:
多个组件需要访问同一份数据(如用户信息、主题、购物车)。
组件嵌套层级深,通过 props 逐层传递(prop drilling)导致代码冗余。
状态更新逻辑分散在多个组件中,难以追踪和维护。
Redux 通过将状态集中管理,组件可以随时访问和更新全局状态,同时保持状态变化的可预测性
2、核心概念
1、Store(存储)
单一的 JavaScript 对象树,保存整个应用的状态
通过 createStore 创建,提供 getState、dispatch、subscribe 等方法
2、Action(动作)
描述"发生了什么"的普通对象,必须包含 type 字段(字符串常量),可选携带数据(payload)
例如:{ type: 'ADD_TODO', payload: { text: '学习 Redux' } }
3、Reducer(归约器)
纯函数,接收当前状态和 action,返回新状态
签名:(previousState, action) => newState
必须纯函数:不能修改原状态,只能返回新对象;不能有副作用(如 API 调用)
3、三大原则
1、单一数据源:整个应用的全局状态存储在一个对象树中,放在唯一的 Store 中
2、状态只读:唯一改变状态的方式是触发 action,而不是直接修改状态
3、使用纯函数进行修改:Reducer 必须是纯函数,确保状态更新的可预测性
4、数据流(单向)
Redux 的数据流是严格的单向流动,清晰且易于调试:
1、用户交互或事件触发 store.dispatch(action)
2、Store 将当前的 state 和 action 传递给 reducer 函数
3、Reducer 根据 action.type 计算并返回新的 state
4、Store 保存新 state,并通知所有订阅者(如 React 组件)更新
5、旧版本示例(类版本)
1、定义 Action Types 和 Action Creators
js
// actions/types.js
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
// actions/counter.js
import { INCREMENT, DECREMENT } from './types';
export const increment = () => ({
type: INCREMENT
});
export const decrement = () => ({
type: DECREMENT
});
2、编写 Reducer
Reducer 是纯函数,根据 action 类型返回新状态
js
// reducers/counter.js
import { INCREMENT, DECREMENT } from '../actions/types';
const initialState = {
value: 0
};
export default function counterReducer(state = initialState, action) {
switch (action.type) {
case INCREMENT:
return { ...state, value: state.value + 1 };
case DECREMENT:
return { ...state, value: state.value - 1 };
default:
return state;
}
}
如果有多个 reducer,可以使用 combineReducers 合并
js
// reducers/index.js
import { combineReducers } from 'redux';
import counterReducer from './counter';
export default combineReducers({
counter: counterReducer
});
3、创建 Store
使用 createStore 创建 Redux store,并传入根 reducer
js
// store.js
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export default store;
4、组件中使用
类组件通过 connect 函数获取 Redux 的状态和 actions,并通过 props 使用
js
// components/Counter.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from '../actions/counter';
class Counter extends Component {
render() {
const { value, increment, decrement } = this.props;
return (
<div>
<h2>Counter: {value}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
}
// 将 Redux 状态映射到组件的 props
const mapStateToProps = (state) => ({
value: state.counter.value // 注意这里 state.counter 对应 combineReducers 中的 key
});
// 将 action creators 映射到组件的 props,自动绑定 dispatch
const mapDispatchToProps = {
increment,
decrement
};
// 使用 connect 创建连接后的组件
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
6、中间件(Middleware)
Redux 中间件提供了一种扩展 Redux 的方式,让你能够在 action 被派发到达 reducer 之前或之后执行额外的逻辑(如日志记录、异步处理、崩溃报告等)
1、应用:手写一个简单的日志中间件
js
// loggerMiddleware.js 日志中间件
const loggerMiddleware = store => next => action => {
console.log(' Dispatching:', action);
const result = next(action); // 调用下一个中间件或 reducer
console.log(' Next state:', store.getState());
return result; // 通常返回 action 本身(由 next 返回)
};
export default loggerMiddleware;
// 使用中间件
import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers';
import loggerMiddleware from './loggerMiddleware';
const store = createStore(
rootReducer,
applyMiddleware(loggerMiddleware)
);
2、redux thunk
Redux Thunk 是一个中间件,它扩展了 dispatch 的功能,允许 action creator 返回一个函数(称为 thunk)而不是一个普通对象
它解决了什么问题?
支持异步 action:让你可以在 action creator 中编写异步代码,而不必把副作用写在组件里。
访问 getState:在异步操作中可以根据当前状态做决策(如避免重复请求)。
组合与复用:可以将多个 action 组合成一个 thunk,实现更复杂的业务逻辑。
保持 action creator 纯净:虽然 thunk 函数本身有副作用,但它依然通过 dispatch 来触发真正的 action,保持了 action 对象的纯净性
actions/userActions.js
js
import {
FETCH_USERS_REQUEST,
FETCH_USERS_SUCCESS,
FETCH_USERS_FAILURE
} from './types';
// 普通 action creators
export const fetchUsersRequest = () => ({
type: FETCH_USERS_REQUEST
});
export const fetchUsersSuccess = (users) => ({
type: FETCH_USERS_SUCCESS,
payload: users
});
export const fetchUsersFailure = (error) => ({
type: FETCH_USERS_FAILURE,
payload: error
});
// Thunk action creator:返回一个函数,用于处理异步
export const fetchUsers = () => {
return async (dispatch, getState) => {
dispatch(fetchUsersRequest()); // 开始请求,设置 loading 状态
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
dispatch(fetchUsersSuccess(data)); // 请求成功,更新用户数据
} catch (error) {
dispatch(fetchUsersFailure(error.message)); // 请求失败,记录错误
}
};
};
reducers/userReducer.js
js
import {
FETCH_USERS_REQUEST,
FETCH_USERS_SUCCESS,
FETCH_USERS_FAILURE
} from '../actions/types';
const initialState = {
loading: false,
users: [],
error: ''
};
const userReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USERS_REQUEST:
return {
...state,
loading: true
};
case FETCH_USERS_SUCCESS:
return {
loading: false,
users: action.payload,
error: ''
};
case FETCH_USERS_FAILURE:
return {
loading: false,
users: [],
error: action.payload
};
default:
return state;
}
};
export default userReducer;
store.js
js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;