React深度解析三:高级用法HOC、Hooks、异步组件

本文分文三部分:

  • 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的原因:

  1. 保存可变值在整个组件生命周期内有效
    useRef 创建的 ref 对象在组件的整个生命周期内保持不变,其 .current 属性可以用来存储任何可变的值
    withPerformanceTracking 高阶组件中,startTimeRefendTimeRef 需要在 useEffect 的回调函数以及其返回的清理函数中都能够访问到同一个变量,用来记录开始时间和结束时间。如果使用普通的变量,在每次组件重新渲染时,这些变量会被重新初始化,导致无法正确记录时间。
    例如,当组件第一次挂载时,useEffect 中的 startTimeRef.current 被设置为开始时间。当组件卸载时,在 useEffect 的返回函数中,可以通过 endTimeRef.current 获取结束时间并进行计算,这期间 startTimeRefendTimeRef 始终指向同一个引用,保证了数据的一致性。
  2. 避免不必要的组件重新渲染
    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>
    );
};

在上面的例子中,只有当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)与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 用于在组件挂载时添加全局的 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、可以在组件树的不同层次使用多个错误边界来分层捕获和处理错误,以提供更精细的错误处理。

三、异步组件

使用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。

  • 20
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值