本文分文三部分:
- HOC高阶组件 higher order component
- Hooks 16.8版本后新增的钩子API
- 异步组件使用lazy和suspense两个api实现组件代码打包分割和异步加载
一、HOC高阶组件
1、定义
高阶组件不是组件而是函数,是react中用于复用组件逻辑的高级技巧,HOC本身不是react一部分,是基于react组合特性而形成的设计模式。
2、特性
HOC是纯函数,参数是组件,返回值也是组件,不会修改传入的组件也不会继承复制其行为,没有副作用
3、使用原因
代码复用:
HOC可以封装组件共享的行为,如状态逻辑、事件监听器、状态持久化等。
逻辑抽象:
HOC可以将一个复杂组件的共同逻辑抽象出来,简化组件结构。
属性代理:
HOC可以代理组件的属性,简化组件的属性要求。
配置时机:
HOC可以在组件渲染前后进行配置,进行性能追踪、日志打点
4、实现方式
4.1 属性代理
通过组合的方式,将组件包装在容器上。下面的例子是
(1)操作props
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.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">
<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)与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 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" />
<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、可以在组件树的不同层次使用多个错误边界来分层捕获和处理错误,以提供更精细的错误处理。
三、异步组件
使用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">
<Suspense fallback={<div>正在加载...</div>}>
<HeavyComponent />
</Suspense>
</div>
);
};
export default App;
在上述代码中:
lazy
函数用于将 HeavyComponent
的加载变为异步的。它接受一个函数,该函数返回一个 Promise
,当 Promise
解析时,就会得到实际的组件模块。
Suspense
组件用于在异步组件加载时显示一个 fallback
(回退)内容,这里是一个简单的“正在加载…”的提示。当 HeavyComponent
还在加载时,fallback
内容会显示在页面上,一旦组件加载完成,就会显示 HeavyComponent
。
这样,当应用运行时,HeavyComponent
会在需要时才进行异步加载,从而减少初始加载时间,提高应用的性能和用户体验。
注意,这种方式需要使用支持动态导入
(import())的构建工具,如 Webpack。