前端宝典之二:React高级用法HOC、Hooks对比、异步组件

桃子叔叔 2024-08-24 09:03:02 阅读 69

本文分文三部分:

HOC高阶组件 higher order componentHooks 16.8版本后新增的钩子API异步组件使用lazy和suspense两个api实现组件代码打包分割和异步加载

一、HOC高阶组件

1、定义

高阶组件不是组件而是函数,是react中用于复用组件逻辑的高级技巧,HOC本身不是react一部分,是基于react组合特性而形成的设计模式。

2、特性

HOC是纯函数,参数是组件,返回值也是组件,不会修改传入的组件也不会继承复制其行为,没有副作用

3、使用原因

代码复用:

HOC可以封装组件共享的行为,如状态逻辑、事件监听器、状态持久化等。

逻辑抽象:

HOC可以将一个复杂组件的共同逻辑抽象出来,简化组件结构。

属性代理:

HOC可以代理组件的属性,简化组件的属性要求。

配置时机:

HOC可以在组件渲染前后进行配置,进行性能追踪、日志打点

4、实现方式

4.1 属性代理

通过组合的方式,将组件包装在容器上。下面的例子是

(1)操作props

<code>import React form 'react'

// 返回stateless的函数组件

function HOC(WrappedComponent) { -- -->

const newProps = { type: 'HOC'}

return props => <WrappedComponent { ...props} { ...newProps}></WrappedComponent>

}

// 返回有状态的类组件

function HOC(WrappedComponent) {

return class extends React.Component {

render() {

const newProps = { type: 'HOC'}

return <WrappedComponent { ...this.props} { ...newProps}></WrappedComponent>

}

}

}

(2)操作state

import React from'react';

// 函数式 HOC

function withCounter(Component) {

return function WithCounter(props) {

const [count, setCount] = React.useState(0);

const increment = () => {

setCount(count + 1);

};

return (

<div>

<Component { ...props} count={ count} increment={ increment} />

</div>

);

};

}

// 原始函数式组件

function MyFunctionalComponent(props) {

return (

<div>

<h1>Count: { props.count}</h1>

<button onClick={ props.increment}>Increment</button>

</div>

);

}

// 增强后的函数式组件

const CounterComponent = withCounter(MyFunctionalComponent);

export default CounterComponent;

(3)通过props实现条件渲染

function HOC(WrappedComponent) {

return (props) =>(

<div>

{

props.isShow?

<WrappedComponent { ...props}></WrappedComponent>

: <div>暂无数据</div>

}

</div>

)

}

4.2 配置时机

(1)类组件实现日志打点

以下是一个使用 React Higher-Order Component (HOC) 在类组件渲染前后进行性能追踪和日志打点的示例:

import React from'react';

// HOC 用于性能追踪和日志打点

function withPerformanceTracking(WrappedComponent) {

return class extends React.Component {

componentDidMount() {

console.log(`Component ${ WrappedComponent.name} is mounting.`);

this.startTime = performance.now();

}

componentDidUpdate() {

console.log(`Component ${ WrappedComponent.name} updated.`);

}

componentWillUnmount() {

console.log(`Component ${ WrappedComponent.name} is unmounting.`);

const endTime = performance.now();

const duration = endTime - this.startTime;

console.log(`Component ${ WrappedComponent.name} rendered in ${ duration} milliseconds.`);

}

render() {

return <WrappedComponent { ...this.props} />;

}

};

}

// 要被增强的原始组件

class MyComponent extends React.Component {

render() {

return (

<div>

<h1>Hello, World!</h1>

</div>

);

}

}

// 使用 HOC 增强组件

const TrackedMyComponent = withPerformanceTracking(MyComponent);

export default TrackedMyComponent;

代码中的performance 是浏览器提供的一个全局对象,用于获取性能相关的信息。

它包含了一些方法和属性,可以用于测量网页性能、获取时间戳等。在上述代码中,使用 performance.now() 方法来获取当前的高精度时间戳,以便计算组件渲染所花费的时间。

这个对象在大多数现代浏览器中都可用,无需额外引入或配置,可直接在 JavaScript 代码中使用。

(2)函数式组件实现日志打点

以下是一个使用高阶组件(HOC)在 React 函数式组件渲染前后进行性能追踪和日志打点的示例:

首先创建一个名为 withPerformanceTracking 的高阶组件:

import React, { useEffect, useRef } from 'react';

const withPerformanceTracking = (WrappedComponent) => {

const PerfTrackedComponent = (props) => {

const startTimeRef = useRef(null);

const endTimeRef = useRef(null);

useEffect(() => {

startTimeRef.current = performance.now();

return () => {

endTimeRef.current = performance.now();

const duration = endTimeRef.current - startTimeRef.current;

console.log(`Component ${ WrappedComponent.name} rendered in ${ duration} milliseconds.`);

};

}, []);

return <WrappedComponent { ...props} />;

};

return PerfTrackedComponent;

};

export default withPerformanceTracking;

然后在你的函数式组件中使用这个高阶组件:

import React from 'react';

import withPerformanceTracking from './withPerformanceTracking';

const MyComponent = ({ data }) => {

return (

<div>

<p>{ data}</p>

</div>

);

};

export default withPerformanceTracking(MyComponent);

在上述代码中:

withPerformanceTracking 高阶组件接收一个 WrappedComponent(被包装的组件)作为参数。

PerfTrackedComponent 中,使用 useEffect 在组件挂载时记录开始时间(startTimeRef),在组件卸载时记录结束时间(endTimeRef),然后计算渲染时间并输出到控制台。

最后将 WrappedComponent 渲染出来并传递所有的 props

对于日志打点,你可以根据具体需求在 useEffect 的返回函数中添加更多的日志记录逻辑,比如将数据发送到服务器或者记录到特定的日志文件中。

为什么使用了useRef

useRef的详细分析将在Hooks部分记录,这里先简单介绍一下上面代码中使用useRef的原因:

保存可变值在整个组件生命周期内有效

useRef 创建的 ref 对象在组件的整个生命周期内保持不变,其 .current 属性可以用来存储任何可变的值

withPerformanceTracking 高阶组件中,startTimeRefendTimeRef 需要在 useEffect 的回调函数以及其返回的清理函数中都能够访问到同一个变量,用来记录开始时间和结束时间。如果使用普通的变量,在每次组件重新渲染时,这些变量会被重新初始化,导致无法正确记录时间。

例如,当组件第一次挂载时,useEffect 中的 startTimeRef.current 被设置为开始时间。当组件卸载时,在 useEffect 的返回函数中,可以通过 endTimeRef.current 获取结束时间并进行计算,这期间 startTimeRefendTimeRef 始终指向同一个引用,保证了数据的一致性。避免不必要的组件重新渲染

useRef 创建的 ref 对象不会触发组件的重新渲染。如果使用 useState 来保存开始时间和结束时间,每次更新 state 都会导致组件重新渲染,这可能会带来不必要的性能开销,特别是在这种只用于记录时间而不需要更新 UI 的场景下。

useRef 保存的值只是单纯的存储在内存中,不会引发组件的重新渲染,更适合用于存储这种不需要影响 UI 渲染的数据。

二、Hooks

1、Hooks定义

Hooks是16.8版本以后新增的钩子API,它允许在函数式组件中使用状态(state)和其他 React 特性,而无需将组件转换为类组件。Hooks 解决了类组件存在的一些痛点,如代码复用性、可读性和理解成本等问题,使代码更加简洁和可维护。

2、React Hooks 的优点

2.1 代码复用性提高

可以将一些通用的逻辑(如数据获取、表单处理等)提取到自定义 Hook 中,然后在多个组件中复用。

例如,创建一个 useForm 的自定义 Hook 来处理表单状态和验证逻辑,多个表单组件都可以使用它。

2.2 使函数式组件更强大

函数式组件在引入 Hooks 之前功能相对有限,无法管理内部状态和处理副作用。有了 Hooks 后,函数式组件可以像类组件一样进行这些操作,并且代码更加简洁。

例如,无需再编写类组件中繁琐的 this 绑定和生命周期方法。

2.3 可读性和可维护性增强

类组件中的生命周期方法和状态逻辑可能分散在不同的方法中,而函数式组件中的 Hooks 可以将相关的逻辑集中在一起。

例如,useEffect 可以将所有的副作用逻辑放在一个地方,代码结构更加清晰。

3、常见的 React Hooks

3.1 useState

功能:用于在函数式组件中添加状态(state)。

import React, { useState } from 'react';

const Counter = () => {

const [count, setCount] = useState(0);

const increment = () => {

setCount(count + 1);

};

return (

<div>

<p>Count: { count}</p>

<button onClick={ increment}>Increment</button>

</div>

);

};

useState 返回一个数组,包含当前状态值(这里是 count)和一个更新状态的函数(这里是 setCount)。每次调用 setCount 都会触发组件的重新渲染。

3.2 useEffect

用于处理副作用,例如数据获取、订阅事件、手动修改 DOM 等操作。

import React, { useState, useEffect } from 'react';

const DataFetcher = () => {

const [data, setData] = useState(null);

useEffect(() => {

// 模拟数据获取

fetch('https://api.example.com/data')

.then(response => response.json())

.then(data => setData(data));

}, []);

return <div>{ data? JSON.stringify(data) : 'Loading...'}</div>;

};

useEffect 接收一个函数作为参数。第二个参数是一个依赖项数组,当依赖项发生变化时,useEffect 中的函数会重新执行。如果依赖项数组为空([]),则该副作用仅在组件挂载时执行一次

3.3 useEffect实现生命周期功能

useEffect 可以模拟类组件中的 componentDidMount(挂载后)、componentDidUpdate(更新后)和 componentWillUnmount(卸载前)的组合。

import React, { useEffect } from 'react';

const FunctionalComponent = () => {

// 模拟 componentDidMount

useEffect(() => {

console.log('Component mounted');

// 如果要模拟 componentWillUnmount,可以返回一个清理函数

return () => {

console.log('Component will unmount');

};

}, []); // 空数组表示仅在组件挂载时执行一次

// 模拟 componentDidUpdate(依赖项变化时执行)

useEffect(() => {

console.log('Component updated');

}, [someDependency]); // 当 someDependency 变化时执行

return <div>Functional Component</div>;

};

3.3 useEffect 和 useLayoutEffect的区别

一、执行时机

useEffect

在组件渲染到屏幕之后执行。它会在浏览器完成渲染并绘制屏幕后触发,通常用于执行那些不会阻塞浏览器更新屏幕的操作,如数据获取、订阅事件等。例如,当使用 useEffect 进行网络请求获取数据时,用户不会感觉到页面卡顿,因为页面已经先显示出来了。

useLayoutEffect

在浏览器完成渲染但尚未绘制到屏幕之前执行。它会在 DOM 更新之后、浏览器进行绘制之前触发。这意味着如果在 useLayoutEffect 中进行了 DOM 操作,这些操作会在浏览器绘制之前完成,从而避免出现视觉上的闪烁或不一致。

二、潜在的性能影响

useEffect

由于它在浏览器绘制之后执行,对用户界面的初始显示速度影响较小。但是,如果副作用操作耗时较长,可能会导致后续的交互出现延迟。比如,如果在 useEffect 中进行了大量的计算或频繁的网络请求,可能会在用户操作时引起卡顿。

useLayoutEffect

因为它在浏览器绘制之前执行,如果其中的操作耗时过长,会阻塞浏览器的绘制,导致页面在一段时间内没有响应,给用户一种卡顿的感觉。所以在 useLayoutEffect 中应该尽量避免执行复杂或耗时的操作,以保持页面的响应性。

三、使用场景

useEffect

适用于大多数常见的副作用场景,特别是那些不需要立即影响页面布局和视觉效果的操作。比如:

发送网络请求获取数据并更新组件状态。订阅和取消订阅事件。定期执行的定时器操作等。

useLayoutEffect

通常用于需要同步处理 DOM 布局的情况,以避免视觉上的闪烁或不一致。例如:

测量 DOM 元素的尺寸或位置,并根据这些信息进行布局调整。在某些情况下,需要在更新状态后立即读取 DOM 元素的属性并进行相应的操作。

以下是一个简单的代码示例展示它们的区别:

import React, { useState, useEffect, useLayoutEffect } from 'react';

const ExampleComponent = () => {

const [count, setCount] = useState(0);

// 使用 useEffect

useEffect(() => {

console.log('useEffect: After render');

}, [count]);

// 使用 useLayoutEffect

useLayoutEffect(() => {

console.log('useLayoutEffect: Before paint');

}, [count]);

return (

<div>

<p>Count: { count}</p>

<button onClick={ () => setCount(count + 1)}>Increment</button>

</div>

);

};

export default ExampleComponent;

在这个示例中,当点击按钮增加 count 值时,useLayoutEffect 的日志会先输出(在浏览器绘制之前),然后 useEffect 的日志会输出(在浏览器绘制之后)。

3.4 useContext

数据传递,用于在函数式组件中访问 React 的上下文(Context)

注意⚠️:useContext() 总是在调用它的组件 上面 寻找最近的 provider。它向上搜索, 不考虑 调用 useContext() 的组件中的 provider。

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext('light');

export default function MyApp() {

const [theme, setTheme] = useState('light');

return (

<>

<ThemeContext.Provider value={ theme}>

<Form />

</ThemeContext.Provider>

<Button onClick={ () => {

setTheme(theme === 'dark' ? 'light' : 'dark');

}}>

Toggle theme

</Button>

</>

)

}

function Form({ children }) {

return (

<Panel title="Welcome">code>

<Button>Sign up</Button>

<Button>Log in</Button>

</Panel>

);

}

function Panel({ -- --> title, children }) {

const theme = useContext(ThemeContext);

const className = 'panel-' + theme;

return (

<section className={ className}>

<h1>{ title}</h1>

{ children}

</section>

)

}

function Button({ children, onClick }) {

const theme = useContext(ThemeContext);

const className = 'button-' + theme;

return (

<button className={ className} onClick={ onClick}>

{ children}

</button>

);

}

useContext 接收一个 Context 对象作为参数,并返回该上下文中的值。通过这种方式,组件可以轻松访问在父组件层次结构中定义的共享数据,而无需通过层层传递 props

3.5 useReducer

(1) 基本介绍

它通常用于管理复杂的状态逻辑。类似于 Redux 中的 reducer 概念,useReducer 接收一个 reducer 函数和一个初始状态initialValue作为参数,并返回当前状态和一个分发dispatch函数。

Reducer 函数:

这是一个纯函数,它接收当前的状态和一个动作(action)作为参数,并根据动作的类型来返回一个新的状态。

格式通常为 (state, action) => newState初始状态:

作为应用状态的初始值。它可以是一个基本数据类型、对象或数组等。分发(Dispatch)函数:

用于触发状态的更新。通过调用这个函数并传入一个动作对象, reducer 函数会根据这个动作来计算新的状态。

类似于 useState,但更适用于复杂的状态管理逻辑,特别是涉及多个子值或复杂的状态更新操作。

(2)实例

import { useReducer } from 'react';

function reducerfunc(state, action) {

if (action.type === 'incremented_age') {

return {

age: state.age + 1

};

}

throw Error('Unknown action.');

}

export default function Counter() {

const [state, dispatch] = useReducer(reducerfunc, { age: 42 });

return (

<>

<button onClick={ () => {

dispatch({ type: 'incremented_age' })

}}>

Increment age

</button>

<p>Hello! You are { state.age}.</p>

</>

);

}

useReducer 返回一个由两个值组成的数组:

当前的 state,首次渲染时为你提供的 初始值如上面的{ age: 42 }dispatch 函数,让你可以根据交互修改 state

为了更新屏幕上的内容,使用一个表示用户操作的 action 来调用 dispatch 函数:

function handleClick() {

dispatch({ type: 'incremented_age' });

}

React 会把当前的 state 和这个 action 一起作为参数传给 reducer 函数,然后 reducer 计算并返回新的 state,最后 React 保存新的 state,并使用它渲染组件和更新 UI

(3)原理

1、初始化:

在组件首次渲染时,useReducer 使用给定的初始状态来初始化状态。

2、状态更新:

当调用分发函数(dispatch)并传入一个动作对象时:

useReducer 将当前状态和动作传递给 reducer 函数。

reducer 函数根据动作的类型和当前状态计算出新的状态。

React 使用新的状态来重新渲染组件。

3.6 useMemo

用于缓存计算结果,以避免不必要的重复计算。

(1)基本用法

useMemo接收两个参数:

一个计算函数,该函数返回一个值。一个依赖项数组,用于决定是否需要重新计算。

import React, { useMemo } from 'react';

const MyComponent = ({ num1, num2 }) => {

// 计算两数之和

const sum = useMemo(() => {

console.log('计算两数之和');

return num1 + num2;

}, [num1, num2]);

return (

<div>

<p>两数之和:{ sum}</p>

</div>

);

};

在上面的例子中,只有当num1num2发生变化时,才会重新执行计算函数来获取新的sum值。如果num1num2都没有变化,sum将直接使用之前缓存的结果,不会重新计算。

(2)性能优化方面的作用

在一些复杂计算或者计算开销较大的场景中,useMemo可以显著提升性能。

例如,假设有一个复杂的计算函数:

import React, { useMemo } from 'react';

const ComplexCalculationComponent = ({ data }) => {

const complexResult = useMemo(() => {

console.log('进行复杂计算');

// 进行复杂的计算,比如大量循环和数学运算

let result = 0;

for (let i = 0; i < 1000000; i++) {

result += data * i;

}

return result;

}, [data]);

return (

<div>

<p>复杂计算结果:{ complexResult}</p>

</div>

);

};

data不变时,complexResult不会重新计算,避免了不必要的计算资源浪费,从而提高了应用的性能。

3.7 useMemouseCallback的区别

useMemo用于缓存计算结果,它关注的是计算的值。

useCallback用于缓存函数,它关注的是函数本身,目的是避免父组件重新渲染时子组件因为接收了新的函数引用而不必要地重新渲染。

例如:

import React, { useMemo, useCallback } from 'react';

const ParentComponent = () => {

const [count, setCount] = React.useState(0);

// 使用 useMemo 缓存计算结果

const memoizedValue = useMemo(() => count * 2, [count]);

// 使用 useCallback 缓存函数

const memoizedCallback = useCallback(() => console.log('Clicked'), []);

return (

<div>

<p>Count: { count}</p>

<p>Memoized Value: { memoizedValue}</p>

<button onClick={ () => setCount(count + 1)}>Increment Count</button>

<ChildComponent onClick={ memoizedCallback} />

</div>

);

};

const ChildComponent = ({ onClick }) => {

console.log('Child Component Rendered');

return <button onClick={ onClick}>Child Button</button>;

};

在上面的例子中,memoizedValue 是使用 useMemo 缓存的计算结果,memoizedCallback 是使用 useCallback 缓存的函数。当 count 变化时,memoizedValue 会重新计算,但 memoizedCallback 不会重新创建,这有助于减少 ChildComponent 的不必要重新渲染(如果 ChildComponent 依赖 onClick 的引用相等来判断是否重新渲染)。

3.7 useMemoReact.memo的区别

React.memo 用于优化函数组件的子组件渲染。结合 useMemoReact.memo 可以更有效地控制组件的渲染和性能。

通常在父组件中使用 useMemo 来缓存计算结果,在子组件中使用 React.memo 来避免不必要的重新渲染。

以下是一个使用 React.memo 来避免子组件不必要重新渲染的代码示例:

import React, { useState } from 'react';

// 子组件

const ChildComponent = React.memo(({ value }) => {

console.log('子组件渲染');

return <div>{ value}</div>;

});

function ParentComponent() {

const [count, setCount] = useState(0);

const [otherValue, setOtherValue] = useState('initial');

const handleClick = () => {

setCount(count + 1);

};

const handleOtherValueChange = () => {

setOtherValue('changed');

};

return (

<div>

<button onClick={ handleClick}>改变 count</button>

<button onClick={ handleOtherValueChange}>改变 otherValue</button>

<ChildComponent value={ count} />

</div>

);

}

export default ParentComponent;

在这个例子中:

ChildComponent 是一个子组件,使用 React.memo 包裹。它接收 value 作为 props 并在组件内显示。ParentComponent 是父组件,它包含两个状态 countotherValue,以及两个按钮分别用于改变这两个状态。当点击 “改变 count” 按钮时,count 状态改变,父组件重新渲染。由于 ChildComponent 使用了 React.memo 并且它只依赖于 value(这里是 count),当 otherValue 改变时,ChildComponent 不会重新渲染,只有当 count 改变时它才会重新渲染,从而避免了不必要的渲染。

注意:

React.memo 仅对 props 的浅层比较。如果 props 是复杂的对象或数组,可能需要使用 useMemo 确保 props 的准确性。

3.7 结合使用 React.memouseMemo**

如果子组件的 props 是函数类型,React.memo 用于整个子组件的渲染优化,而 useMemo 用于优化 props 中特定函数的计算。具体的使用方式取决于具体的应用场景和性能需求。如果函数的计算成本较高且依赖特定的状态,useMemo 可以帮助减少不必要的计算;如果希望整个子组件在 props 未变化时不重新渲染,React.memo 是合适的选择。

import React, { useState, useMemo } from 'react';

// 子组件

const ChildComponent = React.memo(({ expensiveFunction }) => {

// 在子组件中调用这个函数

const result = expensiveFunction();

return <div>{ result}</div>;

});

function ParentComponent() {

const [count, setCount] = useState(0);

const [otherValue, setOtherValue] = useState('initial');

// 一个昂贵的计算函数

const expensiveCalculation = useMemo(() => {

console.log('进行了昂贵的计算');

return () => count * 2;

}, [count]);

return (

<div>

<button onClick={ () => setCount(count + 1)}>改变 count</button>

<button onClick={ () => setOtherValue('changed')}>改变 otherValue</button>

<ChildComponent expensiveFunction={ expensiveCalculation} />

</div>

);

}

export default ParentComponent;

在这个例子中:

ChildComponent 使用 React.memo 来避免在 props 未变化时不必要的重新渲染。expensiveCalculation 函数使用 useMemo 来缓存计算结果,只有当 count 发生变化时才重新计算。

3.8 useRef

(1)基本介绍

1、保存可变值:

useRef 创建的 ref 对象在组件的整个生命周期内保持不变,其 .current 属性可以用来存储任何可变的值,并且这个值在组件的多次渲染之间会被保留下来。

useState 不同,改变 useRef.current 值不会触发组件的重新渲染。

2、访问 DOM 元素:

在类组件中,通常使用 ref 属性来获取 DOM 元素或 React 组件实例。在函数式组件中,可以使用 useRef 来达到同样的目的。

(2)工作原理

在函数组件的首次渲染时,useRef 创建一个带有 .current 属性的对象,并将其返回。

在后续的渲染中,useRef 返回的是同一个对象,不会因为组件的重新渲染而重新创建。

这使得 .current 属性保存的值在组件的整个生命周期中都可以被访问和修改。

(3)实例

1、保存一个简单的变量:

import React, { useRef } from 'react';

const Counter = () => {

const countRef = useRef(0);

const increment = () => {

countRef.current += 1;

console.log(countRef.current);

};

return (

<div>

<button onClick={ increment}>Increment</button>

</div>

);

};

export default Counter;

在这个例子中,countRef 用来保存一个计数器的值,每次点击按钮时,countRef.current 的值会增加,但不会触发组件的重新渲染。

2、访问 DOM 元素:

import React, { useRef } from 'react';

const InputExample = () => {

const inputRef = useRef(null);

const focusInput = () => {

inputRef.current.focus();

};

return (

<div>

<input ref={ inputRef} type="text" />code>

<button onClick={ -- -->focusInput}>Focus Input</button>

</div>

);

};

export default InputExample;

这里创建了一个输入框和一个按钮,通过 useRef 获取对输入框元素的引用,当点击按钮时,调用 focus 方法将焦点设置到输入框上。

3、在组件之间共享数据:

import React, { useRef } from 'react';

const ParentComponent = () => {

const sharedDataRef = useRef({ message: 'Initial message' });

const updateData = () => {

sharedDataRef.current.message = 'Updated message';

};

return (

<div>

<ChildComponent sharedData={ sharedDataRef.current} />

<button onClick={ updateData}>Update Data</button>

</div>

);

};

const ChildComponent = ({ sharedData }) => {

return <p>{ sharedData.message}</p>;

};

export default ParentComponent;

ParentComponent 中,使用 useRef 创建一个保存共享数据的 ref 对象,然后将其传递给 ChildComponent。当在 ParentComponent 中更新 sharedDataRef.current 的值时,ChildComponent 中显示的内容也会相应更新,因为它们共享了同一个引用。

4、使用Hooks实现Error Boundaries

在 React 中,错误边界(Error Boundaries)是一种用于捕获和处理组件树中 JavaScript 错误的机制,从 React 16 版本开始支持。使用 React Hooks 来创建错误边界,可以通过以下步骤实现:

一、创建错误边界组件

创建一个函数式组件来作为错误边界组件:

import React, { useState, useEffect } from 'react';

function ErrorBoundary() {

const [hasError, setHasError] = useState(false);

const [errorMessage, setErrorMessage] = useState('');

useEffect(() => {

const logError = (error, info) => {

setHasError(true);

setErrorMessage(`${ error.toString()} - ${ info}`);

// 这里可以添加将错误信息发送到服务器等其他逻辑

};

// 在组件挂载时添加全局错误处理

window.addEventListener('error', logError);

window.addEventListener('unhandledrejection', logError);

// 在组件卸载时移除事件监听器

return () => {

window.removeEventListener('error', logError);

window.removeEventListener('unhandledrejection', logError);

};

}, []);

if (hasError) {

return <div>发生错误: { errorMessage}</div>;

}

return null;

}

export default ErrorBoundary;

在这个组件中,使用 useState 来管理 hasError(是否发生错误)和 errorMessage(错误信息)的状态。useEffect 用于在组件挂载时添加全局的 errorunhandledrejection 事件监听器来捕获错误,并在组件卸载时移除这些监听器。

二、使用错误边界组件

在需要捕获错误的地方,将组件包裹在错误边界组件中。例如:

import React from 'react';

import ErrorBoundary from './ErrorBoundary';

import SomeComponentThatMightThrowError from './SomeComponentThatMightThrowError';

const App = () => {

return (

<ErrorBoundary>

<SomeComponentThatMightThrowError />

</ErrorBoundary>

);

};

export default App;

这里将 SomeComponentThatMightThrowError 组件包裹在 ErrorBoundary 组件中,当 SomeComponentThatMightThrowError 组件在渲染过程中发生 JavaScript 错误时,ErrorBoundary 组件会捕获到这个错误,更新状态并显示错误信息。

注意事项:

1、错误边界只能捕获组件树中其下方的组件的错误。如果错误发生在错误边界组件自身的渲染函数或生命周期方法中,错误边界无法捕获该错误。

2、错误边界不会捕获事件处理程序中的错误(例如 onClick 回调函数中的错误),但可以通过在 try-catch 块中包裹事件处理逻辑来处理这些错误。

3、可以在组件树的不同层次使用多个错误边界来分层捕获和处理错误,以提供更精细的错误处理。

5、自定义hooks

类组件中部分代码写在生命周期componentWillMount,如何在函数式组件中实现这个componentWillMount

// React hooks 模拟 componentWillMount

const useComponentWillMount = (cb) => {

const willMount = useRef(true);

if (willMount.current) cb();

willMount.current = false;

};

// React 组件

const MyComponent = () => {

useComponentWillMount(() => {

console.log("Component will mount");

});

useEffect(() => {

console.log("Component did mount");

}, []);

上面这段代码的分析:

5.1 useComponentWillMount 自定义 Hook

const willMount = useRef(true);

使用 useRef 创建了一个 willMount 的引用对象。在 React 中,useRef 创建的对象在组件的整个生命周期内保持不变,这里 willMount.current 初始值被设置为 trueif (willMount.current) cb();

在函数内部,当 willMount.currenttrue 时,会调用传入的 cb 回调函数。这是模拟 componentWillMount 中执行某些逻辑的地方。 willMount.current = false;

执行完 cb 之后,将 willMount.current 设置为 false,确保这个模拟的“挂载前”逻辑只执行一次。

5.2 MyComponent 组件

useComponentWillMount(() => { console.log("Component will mount"); });

在组件内部调用 useComponentWillMount 并传入一个回调函数。当组件开始渲染时,这个回调函数会在 useComponentWillMount 内部根据 willMount.current 的初始值为 true 而被执行一次,模拟了 componentWillMount 的行为,输出 "Component will mount"useEffect(() => { console.log("Component did mount"); }, []);

useEffect 是 React 的另一个 Hook,用于处理副作用。这里的 useEffect 传入了一个空的依赖数组 [],这意味着该 useEffect 的回调函数只会在组件挂载后执行一次,输出 "Component did mount"

这种模拟方式在某些情况下可以近似地实现类似 componentWillMount 的效果,但需要注意的是,在 React 的最新实践中,componentWillMount 已经被标记为不推荐使用,因为它可能会导致一些不可预测的行为和性能问题。而且 useEffect 通常更适合处理大多数的副作用和与组件生命周期相关的逻辑。

同时,在实际开发中,应该根据具体的需求和 React 的最佳实践来选择合适的方式来组织代码和处理组件的生命周期相关逻辑。

三、异步组件

使用lazy和suspense两个api实现组件代码打包分割和异步加载

1、创建一个异步组件

假设我们有一个名为 HeavyComponent 的组件,它可能需要一些时间来加载(比如进行复杂的计算或加载大量数据)。

创建一个新文件 HeavyComponent.js

import React from 'react';

const HeavyComponent = () => {

return <div>这是一个需要异步加载的重组件</div>;

};

export default HeavyComponent;

2、在主应用中使用 lazy 和 Suspense

import React, { lazy, Suspense } from 'react';

import './App.css';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

const App = () => {

return (

<div className="App">code>

<Suspense fallback={ -- --><div>正在加载...</div>}>

<HeavyComponent />

</Suspense>

</div>

);

};

export default App;

在上述代码中:

lazy 函数用于将 HeavyComponent 的加载变为异步的。它接受一个函数,该函数返回一个 Promise,当 Promise 解析时,就会得到实际的组件模块。

Suspense 组件用于在异步组件加载时显示一个 fallback(回退)内容,这里是一个简单的“正在加载…”的提示。当 HeavyComponent 还在加载时,fallback 内容会显示在页面上,一旦组件加载完成,就会显示 HeavyComponent

这样,当应用运行时,HeavyComponent 会在需要时才进行异步加载,从而减少初始加载时间,提高应用的性能和用户体验。

注意,这种方式需要使用支持动态导入(import())的构建工具,如 Webpack。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。