React常见面试题梳理

872 阅读34分钟

类组件的生命周期

constructor

  • 执行时机:组件初始化的时候执行
  • 应用场景:初始化一些内容,比如state的初始化、组件中的方法绑定this可以在此方法中操作

componentDidMount

  • 执行时机:组件第一次渲染完成后执行
  • 应用场景:调用接口获取远程数据、监听dom事件等等

render 方法

  • 执行时机:首次渲染执行,每次状态改变后会执行
  • 应用场景:返回组件中要渲染的内容

shouldComponentUpdate(nextProps, nextState, nextContext)

  • 执行时机:每次状态后会触发看,接收的参数是更新后的状。(只在更新阶段调用,初始化阶段不会被调用)
  • 应用场景:它的返回值是个boolean值,如果返回false,render方法就不会继续执行了,反之render会执行。可以在此生命周期函数中添加判断条件,来阻止一些不必要的重新渲染。比如:某个状态并没有在页面上展示,那么状态改变了就不需要在调用render方法重新渲染UI了。

getDerivedStateFromProps(props, state)

  • 执行时机:在render之前执行,且在组件首次渲染和状态更新后都会执行。
  • 应用场景:此生命周期函数的返回值是一个对象的话,它会更新状态,如果是null的话,不会更新任何状态。

getSnapshotBeforeUpdate(prevProps, prevState)

  • 执行时机:在render之后组件更新之前执行,且只在状态更新之后执行,首次渲染不执行。
  • 应用场景:在此生命周期方法的入参中有更新前的state和更新前的props,返回值会传递到componentDidUpdate中。

componentDidUpdate(prevProps, prevState, snapshot?)

  • 执行时机:组件状态更新完页面重新渲染后执行,入参是更新前的props、更新前的state以及getSnapshotBeforeUpdate的返回值。
  • 应用场景:在此生命周期方法中可以在状态更新后且重新渲染结束后执行DOM的相关操作。

componentWillUnmount()

  • 执行时机:组件卸载之前调用
  • 应用场景:清除DOM事件的监听、页面退出前的一些操作可以在此生命周期中执行。

Hooks相关

useState

useState 是 React 中的一个基本 Hook,它用于在函数组件中添加状态(state)。与类组件中的 this.state 相似,useState 允许你在函数组件中存储和更新组件的局部状态。

useState 的作用

  1. 添加状态: useState 允许你在函数组件中声明一个新的状态变量,这个变量可以在组件的渲染周期中保持不变,除非你显式地更新它。
  2. 更新界面: 当你使用 useState 更新状态时,React 会重新渲染这个组件,以反映状态的新值。这使得你能够根据用户输入、服务器响应或其他事件来动态更新界面。
  3. 简化状态管理: 通过 useState,你可以轻松地在函数组件中管理状态,而不需要将组件转换为类组件或使用其他状态管理库(如 Redux)。
  4. 提升代码可读性和可维护性: 将状态逻辑和 UI 逻辑保持在同一个函数组件中,可以使代码更加简洁和易于理解。这也有助于减少组件之间的耦合,提高代码的可维护性。

如何使用 useState

要使用 useState,你需要在函数组件中调用它,并传入一个初始状态值。useState 会返回一个数组,其中包含两个元素:当前状态值和一个用于更新状态的函数。

import React, { useState } from 'react';

const ExampleComponent = () => {
  // 声明一个新的状态变量,初始值为 'Hello, world!'
  const [message, setMessage] = useState('Hello, world!');

  const handleClick = () => {
    // 更新状态,这会导致组件重新渲染
    setMessage('Hello, React!');
  };

  return (
    <div>
      <p>{message}</p>
      <button onClick={handleClick}>Change Message</button>
    </div>
  );
};

export default ExampleComponent;

在这个示例中,我们使用 useState 创建了一个名为 message 的状态变量,以及一个名为 setMessage 的函数来更新这个状态。当用户点击按钮时,handleClick 函数会被调用,setMessage 函数会更新 message 的值,从而导致组件重新渲染并显示新的消息。

useEffect

import { useEffect } from "react";

const Hooks = () => {

  useEffect(() => {
    console.log('useEffect 执行')
  })
  return (
      <div>hello hooks</div>
      
  )
}

export default Hooks;

执行时机

useEffect接收两个参数,第一个是需要执行的回调函数,第二个是依赖项列表。

  1. 依赖项传空数组[]只会在组件初始化的时候执行一次。
  2. 第二个参数如果不传,第二个参数会默认将所以得状态传入,所以组件状态有更新的时候就会执行,并且在组件初始化的时候也会执行一次。
  3. 第二个参数传传某个状态值,在组件初始化的时候执行一次,且在状态值更新的时候执行。

useEffect的返回值

useEffect如果返回的是一个函数的话,在这个函数中可以监听到组件卸载的时机,因此可以在此函数中做一些组件卸载前的一些操作,比如移除dom事件监听。这个函数不单单是在组件卸载前执行,组件内部状态更新之后也会执行,但是在此函数中获取到的状态值更新前的状态值。

应用场景

  1. 可以实现react组件的生命周期
  2. 监听dom事件,对dom进行操作
  3. 获取远程数据
  4. 移除dom事件监听

useMemo

useMemo有两个参数,第一个参数是一个函数,用于业务场景使用,第二个参数是依赖项。依赖项有变动后函数会执行。useMemo是一个强大的性能优化工具,适用于需要缓存计算结果以减少不必要计算的场景。然而,使用时也需谨慎,避免过度使用导致代码复杂性增加或性能问题。

useMemo的作用

  1. 缓存计算结果: useMemo是React中的一个Hook,用于缓存计算结果,并在后续的渲染中重复使用,从而提高组件性能
  2. 避免不必要的计算: 通过缓存,useMemo可以帮助避免在每次渲染时都进行不必要的计算。
import React, { useState, useMemo } from 'react';

const ExampleComponent = () => {
  // 假设我们有一个状态值,它会在某些时候更新
  const [count, setCount] = useState(0);
  // 另一个状态值,与 useMemo 无关,仅用于展示效果
  const [text, setText] = useState('');

  // 使用 useMemo 缓存计算结果
  const memoizedValue = useMemo(() => {
    // 这里进行复杂的计算,假设是一个耗时操作
    console.log('正在进行复杂计算...');
    return count * 2; // 假设我们的计算结果是 count 的两倍
  }, [count]); // 依赖数组,只有当 count 变化时才会重新计算

  // 处理文本输入变化
  const handleTextChange = (event) => {
    setText(event.target.value);
  };

  // 处理按钮点击,增加 count
  const handleIncrement = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h1>useMemo 示例</h1>
      <p>Count: {count}</p>
      <p>Memoized Value: {memoizedValue}</p> {/* 显示缓存的计算结果 */}
      <input type="text" value={text} onChange={handleTextChange} placeholder="输入文本..." />
      <button onClick={handleIncrement}>增加 Count</button>
    </div>
  );
};

export default ExampleComponent;

在这个示例中,我们有一个 count 状态和一个 text 状态。count 可以通过点击按钮来增加,而 text 则用于展示一个文本输入框。

useMemo 被用来缓存 count * 2 的计算结果,并将其存储在 memoizedValue 变量中。我们传递了一个函数给 useMemo,这个函数返回我们需要缓存的值。同时,我们还传递了一个依赖数组 [count],这意味着只有当 count 发生变化时,useMemo 才会重新执行那个函数并更新 memoizedValue

当我们在文本框中输入文字时,text 状态会更新,但由于 text 不是 useMemo 的依赖项,所以 memoizedValue 不会被重新计算。同样地,当我们点击按钮增加 count 时,useMemo 会重新计算 memoizedValue,因为 count 是它的依赖项。

这样,我们就通过 useMemo 成功地缓存了计算结果,并避免了在每次渲染时都进行不必要的计算。

useCallback

useCallback的作用和useMemo类似,但useCallback缓存的是函数,而不是一个具体的值。具体来说,useCallback会返回一个回调函数,该函数会在依赖项不改变的情况下不会改变。这样在可以确保子组件的整个生命周期内,该函数不会重新被创建。

useCallback 的作用

  1. 提高性能: 通过缓存函数,useCallback 可以避免在每次组件渲染时都创建新的函数实例,从而减少内存占用和提高性能。
  2. 避免不必要的渲染: 当你将回调函数传递给子组件时,如果回调函数在每次父组件渲染时都改变,那么子组件也会不必要地重新渲染。使用 useCallback 可以确保回调函数是稳定的,从而避免这种情况。
import React, { useState, useCallback } from 'react';

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

  // 使用 useCallback 缓存回调函数
  const incrementCount = useCallback(() => {
    setCount(count + 1);
  }, [count]); // 依赖项数组,只有当 count 变化时才会重新创建回调函数

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementCount}>Increment</button>
    </div>
  );
};

export default ExampleComponent;

在这个示例中,incrementCount 是一个通过 useCallback 创建的记忆化回调函数。只有当 count 变化时,incrementCount 才会被重新创建。这意味着,如果你有一个子组件接收了 incrementCount 作为 prop,并且 count 没有变化,那么子组件不会因为 incrementCount 的改变而重新渲染。这有助于提高性能并避免不必要的渲染。

useRef

  useRef是react组件的整个生命周期中用于存储可以可变值或获取DOM引用的钩子,与useState不同的是,useRef不会在组件重新渲染后重置其存储的值,因此它非常适合用于保持跨渲染周期的一致性。

useRef的作用

  1. 引用DOM元素: useRef 可以用于获取对 DOM 元素的直接引用,这在需要直接操作 DOM 时非常有用。
  2. 存储可变值: useRef 也可以用作一个“容器”来存储一个可在组件生命周期内变化的值,而不会触发重新渲染。这与 useState 不同,后者在值变化时会触发组件的重新渲染。
  3. 保持跨渲染周期的一致性: 由于 useRef 的值不会在组件重新渲染时重置,因此它非常适合用于存储需要在多个渲染周期之间保持一致的信息。
// 引用 DOM 元素
import React, { useRef, useEffect } from 'react';

const ExampleComponent = () => {
  const inputRef = useRef(null);

  useEffect(() => {
    // 在组件挂载后,inputRef.current 指向了实际的 DOM 元素
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Focus on me!" />
    </div>
  );
};

export default ExampleComponent;

在这个示例中,useRef 被用来获取对输入框的引用,并在组件挂载后自动聚焦到该输入框。

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

const ExampleComponent = () => {
  const renderCount = useRef(0);
  const [, setForceUpdate] = useState();

  const incrementRenderCount = () => {
    renderCount.current++;
    setForceUpdate({}); // 强制组件重新渲染
  };

  return (
    <div>
      <p>Render Count: {renderCount.current}</p>
      <button onClick={incrementRenderCount}>Increment</button>
    </div>
  );
};

export default ExampleComponent;

在这个示例中,useRef 被用来存储一个渲染计数器,它不会在组件重新渲染时重置。每次点击按钮时,计数器都会增加,并且由于我们使用了 useState 来强制组件重新渲染,你可以看到计数器的值是如何跨渲染周期保持一致的。

forwardRef

forwardRef 是 React 中的一个高级 API,它用于转发从父组件传递到子组件的 ref。通常,ref 是用来直接获取 DOM 元素或组件实例的引用的,但在某些情况下,你可能需要将这个引用转发给子组件中的一个特定元素或组件。

forwardRef 的作用

  1. 转发引用: forwardRef 的主要作用是允许你将父组件传递的 ref 转发给子组件内部的一个特定元素或子组件,而不是直接将 ref 附加到子组件本身。
  2. 保持封装性:使用 forwardRef 可以在不破坏组件封装性的情况下,允许父组件访问子组件内部的元素或子组件。这有助于维护组件的独立性和可重用性。
  3. 与高阶组件结合使用: forwardRef 经常与高阶组件(HOC)一起使用,以允许 HOC 将 ref 转发给其包裹的组件。

如何使用 forwardRef

要使用 forwardRef,你需要做两件事:

  1. 创建一个函数组件,并使用 React.forwardRef 来包装它。这个函数组件将接收两个参数:propsref
  2. 在函数组件内部,将接收到的 ref 附加到你想要转发的元素或子组件上。
import React, { forwardRef, useRef } from 'react';

// 定义一个使用 forwardRef 的组件
const FancyInput = forwardRef((props, ref) => {
  // 在这里,我们将 ref 转发给 input 元素
  return <input ref={ref} {...props} />;
});

const ParentComponent = () => {
  const inputRef = useRef(null);

  const focusInput = () => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  };

  return (
    <div>
      <FancyInput ref={inputRef} placeholder="Type something..." />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
};

export default ParentComponent;

在这个示例中,FancyInput 组件使用了 forwardRef 来接收父组件传递的 ref,并将其转发给内部的 input 元素。这样,父组件 ParentComponent 就可以通过 inputRef 直接访问到 FancyInput 组件内部的 input 元素,并在需要时调用它的 focus 方法。

useContext

useContext 是 React 中的一个 Hook,它允许你在不经过组件树逐层传递的情况下,订阅 React 的 Context。Context 提供了一种方式,能够让数据在组件树中跨层级传递,而不必通过逐层传递 props

useContext 的作用

  1. 跨层级传递数据: 使用 useContext,你可以在组件树的深层组件中直接访问顶层组件提供的数据,而不需要在每一层都显式地传递 props
  2. 简化状态管理: 对于需要在多个组件之间共享的状态,使用 Context 和 useContext 可以简化状态的管理和传递。
  3. 避免“属性蔓延”: 当你需要在组件树中传递大量的 props 时,使用 Context 可以避免这种情况,使得代码更加简洁和易于维护。
  4. 增强组件复用性: 由于 Context 提供了跨层级的数据访问能力,这使得组件更加容易复用,因为你不再需要关心组件在树中的位置,只要它能访问到所需的 Context 即可。

如何使用 useContext

要使用 useContext,你需要先创建一个 Context 对象,并使用 React.createContext 方法来初始化它。然后,在顶层组件中使用 Context.Provider 来包裹子组件,并通过 value 属性传递你想要共享的数据。在子组件中,你可以调用 useContext Hook 并传入 Context 对象来获取共享的数据。

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

// 创建一个 Context 对象
const ThemeContext = createContext();

const ThemeProvider = ({ children }) => {
  const theme = 'dark'; // 假设这是你要共享的状态

  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  );
};

const ChildComponent = () => {
  // 使用 useContext Hook 来获取 Context 中的值
  const theme = useContext(ThemeContext);

  return (
    <div>
      The current theme is: {theme}
    </div>
  );
};

const App = () => {
  return (
    <ThemeProvider>
      <ChildComponent />
    </ThemeProvider>
  );
};

export default App;

在这个示例中,我们创建了一个 ThemeContext,并在 ThemeProvider 组件中通过 Context.Provider 将其值设置为 'dark'。然后,在 ChildComponent 中,我们使用 useContext Hook 来获取 ThemeContext 的值,并将其显示在屏幕上。这样,无论 ChildComponent 在组件树中的哪个位置,只要它被 ThemeProvider 包裹,它就能访问到 ThemeContext 的值。

useSelector

useSelector是react-redux提供的一个hook,用来从redux的store中提取数据。

useSelector的作用

  1. 从redux store中提取数据: useSelector是React-Redux提供的一个hook,它允许组件从Redux store中选择并订阅部分数据。
  2. 避免没必要的重新渲染: 当redux store中的数据发生变化时。只有与useSelector选择的数据相关联的组件会重新渲染。这种精确选择数据的方式可以提高性能,避免不必要的渲染。
  3. 引用比较触发重渲染: useSelector默认对选择函数的返回值进行引用比较(===),并且仅在返回值改变时触发重渲染。但请注意,它并不会阻止父组件重渲染导致的子组件重渲染行为。

useSelector如何使用

import React from 'react';
import { useSelector } from 'react-redux';

const MyComponent = () => {
  const myValue = useSelector(state => state.myReducer.myValue);

  return (
    <div>
      The value is: {myValue}
    </div>
  );
};

在这个例子中,useSelector 被用来从 Redux store 中的 myReducer 部分提取 myValue 状态。

hooks的原理是什么?

hook 的数据就是保存在 fiber.memoizedState 的链表上的,每个 hook 对应一个链表节点。

hook 的执行分为 mountXxx 和 updateXxx 两个阶段,第一次会走 mountXxx,创建 hook 链表,之后执行 updateXxx。

hooks为什么不能写在判断条件内?

在react项目中,一个组件中的hooks会保存在fiber.memoizedState的链表上,而链表的每个节点是通过next属性链接的,如果其中一个hooks因判断条件不成立,那这条链表就会断掉,导致后面的hooks会找不到。如果在判断条件中写hooks,编译期间就会报错。

useEffect副作用队列的顺序?

节点层级越深,越靠前

effect的回调收集过程是倒着收集的。

从最底层节点,往上收集。

hooks解决了什么问题?

React Hooks解决了React开发中的几个问题,主要包括:

  • 状态逻辑复用困难: 在Hooks出现之前,组件间的状态逻辑复用主要依赖高阶组件(HOC)和render props,但这两种方式都有其局限性,如props命名冲突和组件树嵌套过深。Hooks允许创建自定义Hook,使得状态逻辑的复用变得简单直接。
  • 复杂组件难以理解: 类组件中,生命周期方法可能包含多种不相关的业务逻辑,导致代码难以阅读和维护。Hooks将状态逻辑与组件的渲染逻辑分离,使得代码更加清晰和易于理解。
  • 函数组件无状态: 之前的React中,函数组件无法拥有状态,需要使用状态时需转换为类组件。Hooks使得函数组件也可以拥有状态,代码更加简洁和易读。
  • 副作用管理分散: 之前的React中,副作用需要在生命周期方法中进行管理,导致代码逻辑分散。Hooks使用useEffect方法来集中管理副作用,易于维护。

综上所述,React Hooks提供了一种更加简洁、灵活和可重用的方式来处理组件的状态和副作用。

setState是同步还是异步?

  1. 合成事件与生命周期: 在react合成事件与生命周期函数中,setState表现为异步。多次调用会合并更新,并会在未来的某个时机批量处理,不会立即反映到状态中。
  2. 原生事件与定时器: 在原生事件与定时器中执行setState,表现是同步的,执行setState后会立即更新状态并触发组件重新渲染。
  3. 异步操作的本质: setState的异步性并非指其内部实现是异步的,而是由于调用顺序和更新机制导致在合成事件和钩子函数中无法立即获取更新后的值。
  4. 确保最新数据: 若需在setState完成后执行某函数并使用最新状态,可利用其回调函数。该回调在状态更新且组件重新渲染后被调用。

综上所述,setState的同步异步性取决于其使用环境和方式。

什么是合成事件?

合成事件是react对浏览器原生事件的封装

  1. 目的: 合成事件是react模拟DOM事件的一个事件对象,旨在提供一个 跨浏览器 的事件接口。使得开发者能够通过统一的方式完成对不同浏览器事件的处理。
  2. 特性: 合成事件拥有与浏览器原生事件相似的API,如stopPropagation和preventDefault方法,并增加了如nativeEvent属性等。在React中,所有事件都是合成的,而非原生DOM事件,但可以通过e.nativeEvent属性获取原生事件。
  3. 工作原理: React采用顶层事件代理机制,保证冒泡一致性,并引入事件池避免频繁创建和销毁事件对象,提高性能。事件不是直接挂载到jsx定义的DOM节点上,而是通过事件代理挂载到某个祖先节点上。

React 16.x及以前的合成事件:

  1. 事件委托到document;
  2. 部分事件还是会绑定到当前元素;
  3. 存在React事件和原生事件的映射关系,比如onMouseLeave会映射成原生的mouseout事件;
  4. 事件池机制。

React 17后的合成事件:

  1. 事件委托到root;
  2. React capture阶段的合成事件提前到原生事件capture阶段执行;
  3. 移除事件池机制;
  4. 事件有优先级。

react v18有几种渲染模式?

两种渲染模式

  1. 传统的同步渲染模式(Legacy Mode): 开发者可以通过使用不同的API(如ReactDOM.createRoot和ReactDOM.render)来选择不同的渲染模式
  2. 并发渲染模式(Concurrent Mode): 并发渲染模式通过Fiber架构将渲染任务拆分为多个可中断和可恢复的小任务,并根据优先级进行调度,从而提高了渲染效率和用户体验。

React Fiber相关

React Fiber是什么?

React fiber 是react16引入的一个新的 调度算法 ,用于实现react的协程调度增量渲染。 fiber将渲染工作分解成一系列小的任务单元(简称fiber节点),这些任务单元在执行的过程中可以中断、暂停。从而使得react可以在浏览器空闲的时候完成渲染任务,而不是一次性渲染。这种机制提高了React应用的性能和响应性,特别是在处理大型组件树时,能够避免阻塞主线程导致的页面卡顿问题。Fiber节点构成了一个类似双向链表的数据结构,每个节点代表一个组件实例或DOM元素,通过childsiblingreturn等属性相互连接

React Fiber的主要优化目标是什么?

  1. 渲染过程可中断,可暂停: 这是因为react fiber的数据结构是一个链表,链表的每个节点是一个fiber节点,分别使用childsiblingreturn属性相互连接
  2. 任务优先级: react fiber会为每个任务分配优先级,这使得在调度算法过程中可以优先执行高优先级任务。
  3. 调度算法 提供任务优先级调度算法,使得优先级较高的任务会优先执行,如用户输入或动画,从而避免长时间占用主线程导致的页面卡顿问题。

React Fiber中的Fiber架构是怎样的?

Fiber定义: fiber是react中的一个工作单元,也是一个数据结构,一个fiber节点代表一个react元素,包含了组件的类型、key、props、state等信息。

Fiber的目的: Fiber的出现是为了解决react中处理频繁更新以及复杂组件的性能瓶颈问题,通过任务切片的方式,将任务分解成多个小任务,并且在浏览器空闲时执行这些任务,以提高流畅度。

Fiber工作流程:

  • 调和阶段: 创建与标记更新节点,收集副作用列表。
  • 提交阶段: 遍历副作用列表,正式提交更新,处理layout effects。

React Fiber中的调度器是如何工作的?

  • React Fiber架构中的调度策略

    • 时间切片: 将耗时任务分割成小块,每帧执行一部分,避免阻塞主线程,保证页面响应性。
    • 优先级调度: 根据任务优先级和紧急程度合理安排执行顺序,高优先级任务优先执行。
  • 经典调度策略对比

    • 先到先得(FCFS) :简单但不合理,对短进程和I/O密集型进程不利,易导致长时间等待。
    • 轮转调度: 基于时钟的抢占策略,能更公平地分配CPU时间,但可能因频繁切换影响性能。
  • React 调度策略的优势

    • 提高性能:通过时间切片和优先级调度,确保重要任务及时执行,避免页面卡顿。
    • 提升用户体验:保证页面在用户交互过程中保持响应,提升整体流畅度。

React Fiber如何处理组件的更新?

  • Fiber节点: 每个React组件都被表示为一个Fiber节点,包含组件状态、属性及指向父、子、兄弟节点的指针,形成Fiber树1。
  • 工作循环:React Fiber采用工作循环概念,在浏览器空闲时执行更新,遍历Fiber树,计算需更新部分,并应用到DOM,保证不阻塞主线程,实现良好用户体验1。
  • 优先级调度: 根据更新紧急程度调整任务执行顺序,确保高优先级任务优先处理,可暂停低优先级任务以处理更重要任务1。
  • 增量渲染: 通过分批处理渲染任务,避免一次性完成所有工作,尤其适用于处理大量数据或复杂UI,防止长时间阻塞用户界面1。
  • beginWork入口: 处理节点更新入口,判断节点及其子树是否有更新,有更新则生成新Fiber并标记,返回子节点继续处理;无子节点则返回null,进入回溯阶段

Redux

什么是Redux?

Redux是一个 JavaScript 状态管理库

  • 定义与用途: Redux是一个专门用于JavaScript应用状态管理的库,它提供了一个可预测的全局状态容器,使得应用状态的管理更加有序和可控。Redux主要用于集中管理React应用中多个组件共享的状态,但它也可以与其他视图库如Vue、Angular等配合使用12。
  • 核心特性: Redux使用单一的常量状态树(对象)来保存整个应用的状态,这个状态树不能直接被改变。当数据变化时,会创建一个新的状态对象(通过actions和reducers实现)。Redux还提供了实时代码编辑与时间旅行调试器相结合的功能,使得应用更易于测试和调试

什么是React-redux?

React-Redux是一个连接 React 和Redux的库,它使得在React应用中使用Redux进行状态管理变得更加方便和高效。React-Redux通过将Redux的核心概念和React组件相结合,提供了一种简单的方法来将React组件与Redux应用程序状态连接起来,使得开发者能够更专注于应用的业务逻辑和用户体验。React-Redux的核心是connect函数,它允许开发者将Redux的state映射到React组件的props上,并允许组件分发actions来更新state。此外,React-Redux还提供了Provider组件,使得Redux的store能够在React组件树中共享和访问。

redux解决了哪些问题?

  1. 集中式状态管理: redux提供了一个集中式存储,将整个应用的状态保存在一个地方(store)中,解决了组件间的数据共享,便于维护和管理。
  2. 状态可预测性: redux提供了唯一的修改状态的方式,通过action触发reducer纯函数来修改状态,使得整个应用的状态变化变得可预测。
  3. 组件间通信: 通过store,各connect组件可以共享数据并进行通信,遵循view->action->reducer的改变state的路径

Redux中间件是什么?

Redux中间件是指在action和reducer之间的处理层,用于扩展dispatch的功能,允许在action到达reducer之前对其进行拦截和处理。中间件可以改变数据流,实现如异步action、action过滤、日志输出等等。它本质上是一个函数,对store.dispatch进行了改造,在发出action和执行reducer之间添加了其他功能。

有哪些Redux中间件?

Redux-thunk

允许action创建函数返回一个函数,使得可以在函数中执行异步逻辑,如API调用等。

如何使用: 下面是一个使用redux-thunk中间件的简单示例,包括如何配置store,以及如何创建和使用异步action

  1. 安装redux-thunk
npm install redux redux-thunk
  1. 配置store

在配置store时,需要应用redux-thunk中间件。

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers' // 假设已经配置了reducers

// 创建store并应用thunk中间件
const store = createStore(rootReducer, applyMiddleware(thunk));
export default store;
  1. 创建异步Action

使用Redux-thunk,可以创建返回函数额action creator,这些函数可以包含异步逻辑。

// actions.js
export const fetchDataRequest = () => ({
  type: 'FETCH_DATA_REQUEST'
});

export const fetchDataSuccess = (data) => ({
  type: 'FETCH_DATA_SUCCESS',
  payload: data
});

export const fetchDataFailure = (error) => ({
  type: 'FETCH_DATA_FAILURE',
  payload: error
});

// 异步action creator
export const fetchData = () => {
  return async (dispatch) => {
    dispatch(fetchDataRequest());
    
    try {
      const response = await fetch('https://api.example.com/data');
      const data = await response.json();
      
      dispatch(fetchDataSuccess(data));
    } catch (error) {
      dispatch(fetchDataFailure(error.toString()));
    }
  };
};
  1. 创建 Reducer

根据异步action来更新state

// reducers.js
const initialState = {
  loading: false,
  data: [],
  error: ''
};

const rootReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'FETCH_DATA_REQUEST':
      return {
        ...state,
        loading: true
      };
    case 'FETCH_DATA_SUCCESS':
      return {
        ...state,
        loading: false,
        data: action.payload
      };
    case 'FETCH_DATA_FAILURE':
      return {
        ...state,
        loading: false,
        error: action.payload
      };
    default:
      return state;
  }
};

export default rootReducer;
  1. 在组件中使用异步Action

最后,在你的React组件中使用这个异步action。

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchData } from './actions';

const DataComponent = () => {
  const dispatch = useDispatch();
  const { loading, data, error } = useSelector(state => state);

  useEffect(() => {
    dispatch(fetchData());
  }, [dispatch]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div>
      <h1>Data</h1>
      <ul>
        {data.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default DataComponent;

在这个示例中,我们配置了一个使用redux-thunk中间件的Redux store,创建了一个异步action creator用于获取数据,并在React组件中调用了这个异步action。当数据被成功获取时,它会被显示在组件中;在加载或错误状态下,会显示相应的消息。

Redux-saga

Redux-Saga的工作原理基于Generator函数和事件监听模式。它通过将异步操作逻辑封装在Sagas中,集中管理应用的副作用(如异步请求)。Sagas通过yield Effects(包含指令的文本对象)来暂停和恢复执行,Effects是简单的对象,包含给middleware解释执行的信息。Redux-Saga包括Watcher Saga和Worker Saga,Watcher Saga监听actions,Worker Saga执行异步任务。通过这种方式,Redux-Saga使得异步流程的管理变得简单、高效且易于测试

如何使用:

  1. 安装依赖
npm install redux redux-saga @redux-saga/core @redux-saga/effects
  1. 配置store

创建或更新你的store配置,以包含redux-saga中间件。

// store.js
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers';
import rootSaga from './sagas';

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  rootReducer,
  applyMiddleware(sagaMiddleware)
);

sagaMiddleware.run(rootSaga);

export default store;
  1. 创建Saga

sagas.js文件中,定义你的saga逻辑。

// sagas.js
import { takeEvery, call, put } from '@redux-saga/core/effects';
import axios from 'axios';

// 工作者saga,用于处理异步请求
function* fetchData(action) {
  try {
    const response = yield call(axios.get, 'https://api.example.com/data');
    yield put({ type: 'FETCH_DATA_SUCCESS', payload: response.data });
  } catch (error) {
    yield put({ type: 'FETCH_DATA_FAILURE', payload: error.message });
  }
}

// 监听者saga,用于监听特定的action
function* watchFetchData() {
  yield takeEvery('FETCH_DATA_REQUEST', fetchData);
}

// rootSaga,用于组合多个saga
export default function* rootSaga() {
  yield all([
    watchFetchData(),
    // 可以在这里添加更多的saga
  ]);
}

// 注意:如果你没有导入all,需要从'@redux-saga/core/effects'中导入它
import { all } from '@redux-saga/core/effects';
  1. 定义Action和 Reducer

actions.jsreducers.js文件中,定义你的action creator和reducer。

// actions.js
export const fetchDataRequest = () => ({
  type: 'FETCH_DATA_REQUEST'
});

export const fetchDataSuccess = (data) => ({
  type: 'FETCH_DATA_SUCCESS',
  payload: data
});

export const fetchDataFailure = (error) => ({
  type: 'FETCH_DATA_FAILURE',
  payload: error
});

// reducers.js
const initialState = {
  loading: false,
  data: [],
  error: null
};

const rootReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'FETCH_DATA_REQUEST':
      return {
        ...state,
        loading: true
      };
    case 'FETCH_DATA_SUCCESS':
      return {
        ...state,
        loading: false,
        data: action.payload
      };
    case 'FETCH_DATA_FAILURE':
      return {
        ...state,
        loading: false,
        error: action.payload
      };
    default:
      return state;
  }
};

export default rootReducer;
  1. 在组件中使用Action

最后,在你的React组件中,使用useDispatchuseSelector来调用action并读取store中的状态。

// DataComponent.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchDataRequest } from './actions';

const DataComponent = () => {
  const dispatch = useDispatch();
  const { loading, data, error } = useSelector(state => state);

  useEffect(() => {
    dispatch(fetchDataRequest());
  }, [dispatch]);

  if (loading) return <p>Loading...</p>;
  if (error) return  <p>Error: {error}</p>;;
  return (
    <div>
      <h1>Data</h1>
      <ul>
        {data.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

Redux-toolkit

Redux-toolkit是官方推荐的编写Redux逻辑的方法,它简化了Redux的配置过程。

  • 安装:首先需要安装@reduxjs/toolkit和react-redux,可以使用npm或yarn进行安装。
  • 创建Slice:Slice是Redux-toolkit中的一个重要概念,它包含了一个reducer函数和相关的action creators。通过createSlice函数可以方便地创建一个Slice。
  • 配置Store:使用configureStore函数创建Redux store,并将创建的Slice添加到store中。
  • 在项目中使用:在React组件中,可以使用useSelector和useDispatch hooks来访问和修改Redux store中的状态。

如何使用:

  1. 安装依赖
npm install @reduxjs/toolkit react-redux
  1. 创建slice

src/redux/counterSlice.js 文件中创建一个简单的 counter slice

 // src/redux/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

export const counterSlice = createSlice({
    name: 'counter',
    initialState: {
        value: 0
    }
    reducers: {
        increment: (state) => {
            state.value += 1
        },
        decrement: (state) =>{
            state.value -= 1
        }
    }
})

export const { increment, decrement } = countSlice.actions
export default countSlice.reducer;
  1. 配置Store

src/redux/store.js 文件中配置 Redux store。

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './countSlice'

export const store = configureStore({
    reducer: {
        counter: counterReducer,
    },
})
  1. React 组件中使用

在 Counter组件中使用 useSelectoruseDispatch hooks 来访问和修改 Redux store 中的状态。

import { useDispatch, useSelector } from 'react-redux'
import { increment, decrement } from '../redux/countSlice';

export default function() {
    const count = useSelector(state => state.counter.value);
    const dispatch = useDispatch();
    const handlleIncrement = () => {
        dispatch(increment())
    }

    const handlleDecrement = () => {
        dispatch(decrement())
    }
    return (
        <div>
            <h1>{count}</h1> 
            <button onClick={handlleIncrement}>增加按钮</button> 
            <button onClick={handlleDecrement}>减少按钮</button>             
        </div>
    )  
}
  1. 在入口处(index.js)中使用 Provider 组件将 Redux store 提供给应用。
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { store } from './redux/store';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

什么是受控组件?

受控组件是 React 中由状态控制的表单元素

  • 定义:受控组件是指其值由React组件的state来控制的表单元素,如输入框、选择框等。
  • 特点:组件的状态全程响应外部数据,即React中的状态变化会直接影响输入框的值。
  • 工作原理:受控组件通过form的输入元素的value属性绑定到React的状态上,然后通过onChange事件来更新状态。用户在输入框中的输入会触发onChange事件,然后更新React状态,进而重新渲染组件。
  • 意义:使用受控组件,React能够动态跟踪表单元素值的改变,确保表单的状态与React组件的状态保持一致,从而提供更可靠的表单处理和数据绑定机制。

什么是高阶组件hoc

定义

高阶组件是react中用于组件复用和逻辑抽象的设计模式。它是一个函数,接收一个组件作为参数,并且返回一个新的组件。

功能

  1. 复用逻辑: 高阶组件允许开发者在不改变原始组件的情况下,通过封装公共逻辑或者状态,使多个组件能够共享这些功能,避免重新编写相同的代码,提高开发效率。
  2. 功能增强:高阶组件可以在不修改原始组件的前提下,为其添加新的功能或特性,例如数据获取,权限校验等。
  3. 逻辑抽象: 高阶组件可以将复杂的逻辑抽象起来,使得原始组件更加简洁和专注,有助于提高代码可维护性和可读性。

实际应用场景

  1. 数据请求: 创建一个高阶组件用来处理数据加载功能,可以在多个组件中复用这一逻辑。
  2. 权限控制: 通过高阶组件对组件进行权限控制,只有满足特性权限的用户才能访问某些功能。
  3. 日志记录 创建一个高阶组件来记录组件的渲染信息,帮助调试和监控。

下面是一个实现数据请求的高阶组件

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

// 数据请求高阶组件
function withDataFetching(WrappedComponent, url) {
  return function DataFetchingComponent(props) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
      const fetchData = async () => {
        try {
          const response = await axios.get(url);
          setData(response.data);
          setLoading(false);
        } catch (err) {
          setError(err);
          setLoading(false);
        }
      };

      fetchData();
    }, [url]); // 依赖url,当url变化时重新获取数据

    return (
      <div>
        {loading && <div>Loading...</div>}
        {error && <div>Error: {error.message}</div>}
        {!loading && !error && (
          <WrappedComponent {...props} data={data} />
        )}
      </div>
    );
  };
}

// 使用高阶组件的示例组件
function MyComponent({ data }) {
  return (
    <div>
      <h1>Data from API:</h1>
      {data && (
        <pre>{JSON.stringify(data, null, 2)}</pre>
      )}
    </div>
  );
}

// 包装后的组件
const MyComponentWithData = withDataFetching(MyComponent, 'https://api.example.com/data');

// 在应用中使用包装后的组件
function App() {
  return (
    <div>
      <MyComponentWithData />
    </div>
  );
}

export default App;

错误边界组件的作用是什么?

错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。

  1. 可以捕获子组件的错误,自身的错误捕获不到。
  2. 子组件的异步方法错误捕获不到
import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.log(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

function BuggyComponent() {
  throw new Error('An error occurred!');
}

function App() {
  return (
    <ErrorBoundary>
      <BuggyComponent />
    </ErrorBoundary>
  );
}

在这个例子中,我们定义了一个 ErrorBoundary 组件,它继承自 React.Component。在这个组件中,我们定义了 getDerivedStateFromErrorcomponentDidCatch 这两个生命周期方法。当子组件树中发生错误时,getDerivedStateFromError 方法会被调用,并返回一个新的状态对象,用于更新组件的状态。同时,componentDidCatch 方法也会被调用,并接收到错误对象和错误信息。

render 方法中,我们根据组件的状态来决定渲染内容。如果 hasError 状态为 true,则渲染一个错误信息;否则,渲染子组件。

App 组件中,我们使用 ErrorBoundary 组件来包裹一个有问题的 BuggyComponent 组件。当 BuggyComponent 组件抛出错误时,错误会被 ErrorBoundary 组件捕获,并渲染出错误信息。

React 的插槽(Portals)的理解,如何使用,有哪些使用场景?

React 官方对 Portals 的定义:

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案

Portals 是React 16提供的官方解决方案,使得组件可以脱离父组件层级挂载在DOM树的任何位置。通俗来讲,就是我们 render 一个组件,但这个组件的 DOM 结构并不在本组件内。

  1. 弹出层级方便管理,统一挂载到document下面, z-index
  2. 弹窗方便调试

代码示例

  1. 创建Portal组件

src目录下,创建一个新的文件,比如PortalComponent.js,并编写以下代码:

import React, { useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';

const Portal = ({ children }) => {
  const el = useRef(document.createElement('div'));

  useEffect(() => {
    // 将portal的容器插入到DOM中,这里我们选择body作为插入点
    document.body.appendChild(el.current);

    // 在组件卸载时,移除portal的容器
    return () => {
      document.body.removeChild(el.current);
    };
  }, []); // 空依赖数组表示这个effect只在组件挂载和卸载时运行一次

  return ReactDOM.createPortal(children, el.current);
};

export default Portal;
  1. 使用Portal组件:

在你的主应用组件(比如App.js)中,使用PortalComponent来渲染一些内容:

import React from 'react';
import PortalComponent from './PortalComponent';

function App() {
  return (
    <div className="App">
      <h1>Hello, React Portals!</h1>
      <PortalComponent>
        <div style={{ position: 'fixed', top: '10px', right: '10px', backgroundColor: 'rgba(0, 0, 0, 0.75)', color: 'white', padding: '10px' }}>
          This is a portal element!
        </div>
      </PortalComponent>
    </div>
  );
}

export default App;

React 中如何避免不必要的render?

React是基于虚拟DOM和diff算法的完美配合,实现了对DOM的最小粒度更新,对于日常开发都能满足需求。但是对于功能较复杂的业务场景就需要考虑到性能问题,然而为了解决提高性能或者用户体验,我们需要在开发的过程中运用React的一些特性来提高性能。最重要的一点就是避免不必要的render,以下方式可以避免一些不必要的render。

  • shouldComponentUpdate 和 PureComponent

在类组件中可以使用shouldComponentUpdate和PureComponent来减少不必要的render,从而达到目的。

  • 利用高阶组件

在函数组件中,并没有 shouldComponentUpdate 这个生命周期,可以利用高阶组件,封装一个类似 PureComponet 的功能

  • 使用 React.memo配合useMemo 或者useCallback

React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo只能用于函数组件。

React的diff算法

  • 概念与重要性:

    • 概念:React Diff算法用于比较虚拟DOM树之间的差异,高效找出需更新的最小部分。
    • 重要性:减少不必要的DOM操作,提高渲染性能。
  • 原理与策略:

    • 树的层级比较:根节点类型不同则销毁旧树,创建新树;类型相同则比较子节点。
    • key的作用:帮助识别变化的子元素,提高比较效率。
    • 优化策略:跨层级操作不优化;同类组件继续diff;不同类组件直接替换;同层子节点需唯一key。
  • 实现过程:

    • render阶段:可中断,生成fiber树,发生diff。
    • commit阶段:不可中断,执行DOM操作等。
    • 双缓存技术:current fiber树与workInProgress fiber树。
    • 节点对比逻辑:在reconcileChildFibers方法中实现,分单节点与多节点diff。

React的Diff算法通过高效比较和最小化DOM操作,显著提高了应用的性能和响应速度。