2025-05-13
ReactJS
00

目录

常见问题
React类组件中的constructor中为什么一定要使用super
使用自定义Hook复用逻辑
Hook为什么只能用在React函数的最顶层
useState与useReducer为什么返回一个数组,而不是一个对象
函数组件中的setState没有回调函数怎么办
React重要版本
hook解决了什么问题
函数组件与类组件如何选择
函数组件当中如何实现forceUpdate
什么是合成事件
为什么React17不需要导入JSX
React中常见的性能优化
1. 复用组件
2. 避免组件不必要的重新render
3. 缓存策略/减少运算
Hooks
useEffectEvent
useActionState
suspend 与 useDeferredValue

最近 React 方面 又学了些新东西,还是写写笔记吧, 不然会忘。

常见问题

React类组件中的constructor中为什么一定要使用super

  1. 首先,先明确super不是React的知识点,而是es6的。

    • es6规定,子类的构造函数必须执行一次super函数,否则会报错。
    • 如果不写构造函数,es6默认生成构造函数constructor(..args){super(...args)}
    • super只能在派生类构造函数和静态方法中使用,不能在调用super之前引用this。
    • 使用super是必须指明是方法调用还是函数调用,如果是方法调用则只能在子类的构造函数中。
  2. 在React中super有两个参数super(props, context)

使用自定义Hook复用逻辑

使用自定义Hook复用逻辑

React Hook规则

  • 以 use 开头的函数被称为 Hook
    • 没有 调用 Hook 的函数不需要 变成 Hook
    • 自定义 Hook 命名必须以 use 开头,后面跟一个大写字母。
  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调⽤
  • 只能在 React 的函数组件中 或 ⾃定义的 Hook 中 调用Hook
  • 自定义 Hook 共享的只是状态逻辑,不是状态本身。

hook的使用

  • 提取组件间的共享逻辑为自定义组件
  • 自定义 Hook 让你可以在组件间共享逻辑。
  • 每次组件重新渲染时,所有的 Hook 会重新运行。
  • 自定义 Hook 的代码应该和组件代码一样保持纯粹。
  • 把事件处理函数传到自定义 Hook 中
  • 不要创建像 useMount 这样的自定义 Hook。保持目标具体化。

Hook为什么只能用在React函数的最顶层

hooks是作为一个单链表存储在fiber.memoizedState上的,因为这些hook没有名字,所以为了区分它们,我们必须保证这个链表节点顺序的稳定性。

useState与useReducer为什么返回一个数组,而不是一个对象

如果源码中返回的是个对象,那么状态值和修改状态值的函数名字都写死了,不方便用户多处使用。

函数组件中的setState没有回调函数怎么办

先看一下class组件的setState的回调函数

jsx
export class ClassComponent extends React.Component { constructor(props) { super(props); this.state = {count: 0}; } handle = () => { const {count} = this.state; this.setState({count: count + 1}, function(state, props) { console.log("a", state, props); // 组件重新渲染后被调用 }); }; render() { const {count} = this.state; return ( <div className="class border"> {this.props.name} <button onClick={this.handle}>{count}</button> </div> ); } }

在FunctionComponent中可以使用useEffect, useLayoutEffect模拟

jsx
export function FunctionComponent({name}) { const [count, setCount] = React.useState(0); React.useEffect(() => { console.log("useEffect count", count); //sy-log }, [count]); React.useLayoutEffect(() => { console.log("useLayoutEffect count", count); //sy-log }, [count]); return ( <div className="function border"> {name} <button onClick={() => setCount(count + 1)}>{count}</button> </div> ); }

React重要版本

  • 16.4 新增生命周期
  • 16.8 新增hooks
  • 17.0 垫脚石版本

hook解决了什么问题

随着class Component的功能越来越多,组件也变得越来越臃肿,很多不相关的逻辑混合在生命周期函数中,这时组件就变得杂乱而没有条理,查找确切的逻辑使用范围就变得困难,调试和优化也变得越来越难。

  1. 在组件之间复用状态逻辑很难

React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。如果你在 React DevTools 中观察过 React 应用,你会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。尽管我们可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。

你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。

具体将在自定义 Hook 中对此展开更多讨论。

  1. 复杂组件变得难以理解

我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMountcomponentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。

为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

我们将在使用 Effect Hook 中对此展开更多讨论。

  1. 难以理解的 class

除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。即便在有经验的 React 开发者之间,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。

另外,React 已经发布五年了,我们希望它能在下一个五年也与时俱进。就像 SvelteAngularGlimmer等其它的库展示的那样,组件预编译会带来巨大的潜力。尤其是在它不局限于模板的时候。最近,我们一直在使用 Prepack 来试验 component folding,也取得了初步成效。但是我们发现使用 class 组件会无意中鼓励开发者使用一些让优化措施无效的方案。class 也给目前的工具带来了一些问题。例如,class 不能很好的压缩,并且会使热重载出现不稳定的情况。因此,我们想提供一个使代码更易于优化的 API。

为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。

函数组件与类组件如何选择

函数组件和类组件都可以取代对方。可以参考以下规则选择:

  1. 颗粒度

函数组件颗粒度更小,是函数式编程的优先选择。

颗粒度体现在state定义与useEffect、useLayoutEffect上,函数组件可以写多个,也可以拆成自定义hook调用。但是相比之下,类组件 的state和每个生命周期函数在组件中都最多能用一次,拆到组件外也比较繁琐。

  1. 实例

类组件有实例,如果需要用到实例的话,类组件是首选。

  1. 复用状态逻辑

函数组件和类组件都可以复用状态逻辑。

类组件可以通过hoc、render props,但是容易形成嵌套地狱,参考antd3 form的createForm和react-redux的connect一起用在一个组件里的时候,四层括号,闹心~。但是函数组件用一个自定义hook就完事了,你看antd4 form的useForm。

  1. 学习成本

类组件有个this,合成事件中可能会比较难以理解,但是函数组件并没有这种设计,上手简单~

  1. 其他

具体API

函数组件当中如何实现forceUpdate

在React源码中判断如果useState和useReducer前后两次值相等,则放弃更新

如下三种使用forceUpdate的方法:useReducer、useState或者useForceUpdate,

js
import {useCallback, useReducer, useState} from "react"; export default function FunctionComponentForceUpdate(props) { console.log("omg"); //sy-log // const [count, forceUpdate] = useState(0); // ! 方法1 // const [, forceUpdate] = useReducer((x) => x + 1, 0); // ! 方法2 const forceUpdate = useForceUpdate(); const handleClick = () => { // forceUpdate(count + 1); // forceUpdate((prev) => prev + 1); forceUpdate(); }; return ( <div> <h3>FunctionComponentForceUpdate</h3> <button onClick={handleClick}>count</button> </div> ); } function useForceUpdate() { const [state, setState] = useState(0); // const [, setState] = useReducer((x) => x + 1, 0); const update = useCallback(() => { setState((prev) => prev + 1); // setState(); }, []); return update; }

什么是合成事件

React为了 实现跨平台兼容性,对于事件处理有自己的一套代码。

React中有自己的事件系统模式,即通常被称为React合成事件。之所以采用这种自己定义的合成事件,一方面是为了抹平差异性,使得React开发者不需要自己再去关注浏览器事件兼容性问题,另一方面是为了统一管理事件,提高性能,这主要体现在React内部实现事件委托,并且记录当前事件发生的状态上。

事件委托,也就是我们通常提到的事件代理机制,这种机制不会把时间处理函数直接绑定在真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件监听和处理函数。当组件加载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件放生时,首先被这个统一的事件监听器处理,然后在映射表里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升。

记录当前事件发生的状态,即记录事件执行的上下文,这便于React来处理不同事件的优先级,达到谁优先级高先处理谁的目的,这里也就实现了React的增量渲染思想,可以预防掉帧,同时达到页面更顺滑的目的,提升用户体验。

为什么React17不需要导入JSX

在React@16中,JSX 语法被 @babel/parset-react 编译为React.createElement()的语法躺,因此必须手动导入React(即使没有直接使用),否则React.createElement会报错。

React17引入了新的JSX运行时,Babel和TypeScript等工具会自动从React包中导入必要的JSX运行时函数,而不在依赖React.createElement

jsx
/ 你的代码(无需导入 Reactconst element = <div>Hello</div>; // Babel 转换后 import { jsx as _jsx } from 'react/jsx-runtime'; const element = _jsx('div', { children: 'Hello' });

webpack编译后的结果为

js
function jsxWithValidation () { // <h1><h2><h3>3333</h3></h2></h1> return ( (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx) ("h1", { children: (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx) ("h2", { children: (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx) ("h3", { children: "3333" }) }) }) ); } ;; function jsxWithValidationDynamic(type, props, key) { return jsxWithValidation(type, props, key, false); }

为什么这样做?

  • 减少冗余代码:不再需要手动写 import React
  • 性能优化:新的运行时生成的代码可能更紧凑。
  • 未来兼容性:为 React 的未来功能(如服务器组件)做准备。

如何启用新转换?

  • 使用 Babel 7.9.0+ 或 TypeScript 4.1+。
  • 如果是 Create React App 或 Next.js,它们默认已支持 React 17+ 的转换。

React中常见的性能优化

1. 复用组件

在协调阶段,组件复用的前提是必须同时满足三个条件:同一层级下、同一类型、同一个key值。所以我们尽量保证这三者的稳定性。

key值标记了节点在当前层级下的唯一性,因此我们尽量不要取index

2. 避免组件不必要的重新render

组件重新render会导致组件进入协调,协调的核心就是我们常说的vdom diff, 所以协调本身就是比较耗时的算法,因此如果能够减少协调,复用旧的fiber节点,那么肯定会加快渲染完成的速度。组件如果没有进入协调阶段,我们称为进入bailout阶段,意思就是这层组件退出更新。让组件进入bailout阶段,有以下方法

memo

memo 允许你的组件在props没有改变的情况下跳过重新渲染

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)

这里的arePropsEqual是一个函数, 用户可以自定义,如果没有定义,默认使用浅比较。

比较组件更新前后的props是否相同,如果相同,则进入bailout阶段。

源码地址

image.png

使用示例:避免不必要的字组件变更

tsx
const App: React.FC = () => { // ... const SearchInputRef = useRef(memo(SearchInput)); return <div> {...} <SearchInputRef.current /> </div> }

shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState, nextContext)

PureComponent

PureComponentComponent,但是前者会浅比较props和state以及减少错过必要更新的概率。

Context

Context 本身就是一旦Provider传递的value变化,所有消费这个value的后代组件都要更新,因此应该精良精简使用Context

Context使用场景: 当祖先组件想要和后代组件快速通信。

3. 缓存策略/减少运算

useMemo

useMemo 是一个React Hook, 它在每次重新渲染的时候能够缓存计算的结果。

const cachedValue = useMemo(calculateValue, dependcies)

useMemo可以缓存参数,可以对比useCallback使用,useCallback(fn, deps)相当于useMemo(() => fn, deps)。

参考资料

Hooks

useEffectEvent

注:以下内容来自deepSeek(比react官方介绍更加清晰)

useEffectEvent 是 React 正在实验中的一个新 Hook,旨在解决在 useEffect 中使用最新 props 或 state 而不触发重新执行的问题。目前它还不是 React 稳定版的一部分,但了解它的用法对未来开发很有帮助。

基本概念

useEffectEvent 允许你定义一个不会成为 useEffect 依赖项的"稳定引用"函数,但又能访问最新的 props 和 state。

为什么需要它

在传统的 useEffect 中,我们经常遇到这样的问题

jsx
function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { function createConnection() { // 连接逻辑 } const connection = createConnection(); connection.on('message', (receivedMessage) => { // 这里需要最新的 message 状态 console.log(message); // 可能不是最新的 }); return () => connection.disconnect(); }, [roomId]); // 这里如果添加 message 依赖,会导致频繁重连 }

使用 useEffectEvent 解决

jsx
import { experimental_useEffectEvent as useEffectEvent } from 'react'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); const onMessage = useEffectEvent((receivedMessage) => { console.log(message); // 总是能获取最新的 message }); useEffect(() => { function createConnection() { // 连接逻辑 } const connection = createConnection(); connection.on('message', onMessage); return () => connection.disconnect(); }, [roomId]); // 不需要添加 onMessage 或 message 作为依赖 }

使用场景

  • 事件监听器中需要最新状态
  • 避免不必要的 effect 重新执行
  • 需要在 effect 中访问最新值但不希望作为依赖项

注意事项

  • 目前仍是实验性 API,未来可能有变化
  • 只能在组件顶层使用,不能在循环或条件语句中使用
  • 返回的函数不应该被调用在渲染过程中,只能在 effect 或其他事件处理函数中调用

替代方案 在 useEffectEvent 正式发布前,可以使用 useRef 和 useCallback 的组合来模拟类似行为:

jsx
function useEventCallback(fn) { const ref = useRef(fn); useEffect(() => { ref.current = fn; }); return useCallback((...args) => ref.current(...args), []); }

useEffectEvent 提供了一种更官方、更简洁的解决方案来处理这类常见问题。

useActionState

React 19 引入的实验性 API

在组件的顶层调用 useActionState 即可创建一个随 表单动作被调用 而更新的 state。在调用 useActionState 时在参数中传入现有的表单动作函数以及一个初始状态,无论 Action 是否在 pending 中,它都会返回一个新的 action 函数和一个 form state 以供在 form 中使用。这个新的 form state 也会作为参数传入提供的表单动作函数。

jsx
import { useActionState } from 'react'; async function loginAction(prevState, formData) { const username = formData.get('username'); const password = formData.get('password'); try { // 模拟服务端验证 if (username === 'admin' && password === '123') { return { success: true, message: '登录成功!' }; } else { return { success: false, error: '用户名或密码错误' }; } } catch (error) { return { success: false, error: '服务器错误' }; } } export default function LoginForm() { const [state, formAction, isPending] = useActionState(loginAction, { success: null, error: null, message: null, }); return ( <form action={formAction}> <div> <label>用户名:</label> <input type="text" name="username" required /> </div> <div> <label>密码:</label> <input type="password" name="password" required /> </div> <button type="submit" disabled={isPending}> {isPending ? '登录中...' : '登录'} </button> {state.error && <p style={{ color: 'red' }}>{state.error}</p>} {state.success && <p style={{ color: 'green' }}>{state.message}</p>} </form> ); }

suspend 与 useDeferredValue

React Suspense 是 React 16.6 引入的一个组件,主要用于处理异步操作(如数据获取、代码分割等)时的加载状态管理。以下是 Suspense 组件主要做的事情:

核心功能

  1. 加载状态管理:
  • 当子组件尚未准备好渲染时(如数据正在加载),显示 fallback UI
  • 当子组件准备好后,无缝切换到实际内容
  1. 协调异步操作:
  • 与 React.lazy() 配合实现代码分割
  • 与并发特性配合实现数据获取的暂停与恢复

底层原理

  1. 抛出 Promise:
  • 子组件在数据加载期间会抛出 Promise 对象
  • React 捕获这些 Promise 并暂停渲染
  1. 协调渲染:
  • 当 Promise 解决后,React 重新尝试渲染组件
  • 如果成功则显示内容,否则继续显示 fallback

Suspense 是 React 异步渲染模型的核心组件之一,为未来的并发特性提供了基础支持。

先来看一下suspend的示例

jsx
let cache = new Map(); export function fetchData(url: string) { if(cache.has(url)) { cache.set(url, getData(url)); } return cache.get(url) } async function getData(url) { await new Promise(resolve => { setTimeout(resolve, 1000) }); return ['a', 'b', 'c'].slice(0, Math.floor(Math.random() * 3)) } const SearchResults = ({query}) => { const arr = use(fetchData(`/search?q=${query}`)); if(query === '') return null; if(arr.length === 0) { return <p>No matches for <i>"${query}"<i><p> } return (<ul> {arr.map(it => ( <li key={it}>{it}</li> ))} </ul>) } export default function App() { const [query, setQuery] = useState(''); return <> <label> Search results: <input value={query} onChange={e => setQuery(e.target.value)} /> </label> <Suspense fallback={<h2>Loading...</h2>}> <SearchResults query={query}> </Suspense> </> }

Suspend 与 React.lazy() 实现代码分割示例

jsx
import React, { Suspense, lazy } from 'react'; function App() { return ( <div> <h1>我的应用</h1> {/* 使用 Suspense 包裹懒加载组件 */} <Suspense fallback={<div>加载中...</div>}> {lazy(() => import('./LazyComponent'))} </Suspense> </div> ); }

useDeferedValue 是配合Suspend 进行延迟展示(请求不会延时)。

可以让你延迟更新 UI 的某些部分

todo

  • useOptimistic
  • useSyncExternalStore
  • useTransition

本文作者:郭郭同学

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!