前端宝典之二: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
高阶组件中,startTimeRef
和 endTimeRef
需要在 useEffect
的回调函数以及其返回的清理函数中都能够访问到同一个变量,用来记录开始时间和结束时间。如果使用普通的变量,在每次组件重新渲染时,这些变量会被重新初始化,导致无法正确记录时间。
例如,当组件第一次挂载时,useEffect
中的 startTimeRef.current
被设置为开始时间。当组件卸载时,在 useEffect
的返回函数中,可以通过 endTimeRef.current
获取结束时间并进行计算,这期间 startTimeRef
和 endTimeRef
始终指向同一个引用,保证了数据的一致性。避免不必要的组件重新渲染
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>
);
};
在上面的例子中,只有当num1
或num2
发生变化时,才会重新执行计算函数来获取新的sum
值。如果num1
和num2
都没有变化,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 useMemo
与useCallback
的区别
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 useMemo
与React.memo
的区别
React.memo
用于优化函数组件的子组件渲染。结合 useMemo
和 React.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
是父组件,它包含两个状态 count
和 otherValue
,以及两个按钮分别用于改变这两个状态。当点击 “改变 count” 按钮时,count
状态改变,父组件重新渲染。由于 ChildComponent
使用了 React.memo
并且它只依赖于 value
(这里是 count
),当 otherValue
改变时,ChildComponent
不会重新渲染,只有当 count
改变时它才会重新渲染,从而避免了不必要的渲染。
注意:
React.memo
仅对 props 的浅层比较。如果 props 是复杂的对象或数组,可能需要使用 useMemo
确保 props 的准确性。
3.7 结合使用 React.memo
和 useMemo
**
如果子组件的 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
用于在组件挂载时添加全局的 error
和 unhandledrejection
事件监听器来捕获错误,并在组件卸载时移除这些监听器。
二、使用错误边界组件
在需要捕获错误的地方,将组件包裹在错误边界组件中。例如:
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
初始值被设置为 true
。 if (willMount.current) cb();
:
在函数内部,当 willMount.current
为 true
时,会调用传入的 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。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。