前端面试题 | React(2023)

1,325 阅读10分钟

近期总结了一些React的知识,都是较为高频的前端面试题,尽量解答完整详细,并分享给大家一起学习。如有问题,欢迎指正。

1. React的生命周期

React 组件的生命周期分为三个阶段:挂载、更新和卸载。以下是每个阶段中可用的生命周期方法和它们的作用:

挂载阶段

  • constructor:组件实例化时被调用,可以进行组件的初始化工作,例如绑定事件处理程序、设置状态或实例化对象。
  • static getDerivedStateFromProps(props, state):在组件挂载之前被调用,用于根据 props 来更新组件的状态。它返回一个对象,表示要更新的组件状态,或者返回 null,表示不需要更新状态。
  • render:根据当前的 props 和 state 渲染组件的 UI。
  • componentDidMount:在组件挂载后被调用,可以进行异步请求、添加事件监听器或启动定时器等操作。

更新阶段

  • static getDerivedStateFromProps(props, state):在组件更新之前被调用,用于根据 props 更新组件的状态。
  • shouldComponentUpdate(nextProps, nextState):在组件更新之前被调用,可以根据新的 props 和 state 判断是否需要重新渲染组件。返回 true 表示需要重新渲染,返回 false 表示不需要。
  • render:根据新的 props 和 state 重新渲染组件的 UI。
  • getSnapshotBeforeUpdate(prevProps, prevState):在 render 方法之后、更新 DOM 之前被调用,可以获取组件更新前的一些信息。它返回一个值,该值会作为第三个参数传递给 componentDidUpdate 方法。
  • componentDidUpdate(prevProps, prevState, snapshot):在组件更新后被调用,可以进行 DOM 操作、网络请求或更新组件的状态等操作。

卸载阶段

  • componentWillUnmount:在组件卸载之前被调用,可以清除定时器、移除事件监听器或取消网络请求等操作。

2. React Hooks

什么是React Hooks?

React Hooks 是 React v16.8.0 引入的新特性,它使函数组件能够拥有状态和其他 React 特性。它们是一组 API,可以让我们在不编写类组件的情况下使用 React 的功能。

React Hooks 包括以下 API:

  • useState:用于在函数组件中添加状态。
  • useEffect:用于在函数组件中添加副作用。
  • useContext:用于在函数组件中访问 context。
  • useReducer:用于在函数组件中管理复杂的状态。
  • useCallback:用于在函数组件中缓存回调函数,以避免不必要的重新渲染。
  • useMemo:用于在函数组件中缓存值,以避免不必要的重新计算。
  • useRef:用于在函数组件中存储可变的值。
  • useImperativeHandle:用于在函数组件中公开 ref。
  • useLayoutEffect:与 useEffect 相同,但在 DOM 更新之前同步执行
  • useDebugValue:用于在自定义 Hooks 中显示调试信息。

2.1 useState

  • useState 和 setState 有什么区别?

useState 和 setState 都用于添加状态,但它们之间有几个重要的区别:

// useState
function Counter() {
 // count 是当前状态,setCount 是更新状态的函数
  const [count, setCount] = useState(0);
  /* ... */
}
// setState
this.setState({ count: this.state.count + 1 });
  • useState 是一个函数,可以在函数组件中调用,而 setState 是一个方法,必须在类组件中调用。
  • useState 返回一个数组,包含当前状态和一个更新状态的函数,而 setState 接受一个新状态并将其与当前状态合并。
  • useState 中的状态可以是任何值,而 setState 中的状态必须是一个对象。

需要注意的是,setState 的更新是异步的,也就是说,调用 setState 后,并不会立即更新组件的状态,而是将更新的操作放入队列中,等待后续处理。如果需要在 setState 完成后立即执行一些操作,可以在 setState 的第二个参数中传递一个回调函数,这个回调函数会在更新完成后被调用。
this.setState({ count: this.state.count + 1 }, () => { console.log('count updated'); });

2.2 useEffect

什么是useEffect?

useEffect 是 React Hooks 中用于在函数组件中添加副作用的 API。副作用指的是与组件渲染无关的操作,比如数据获取、订阅、DOM 操作等。useEffect 会在组件渲染时执行一次,之后在每次组件重新渲染时执行。

useEffect的用法?

useEffect 接受两个参数,第一个参数是一个函数,用于定义需要执行的副作用操作,第二个参数是一个数组,用于指定需要监测的变量,当数组中的变量发生变化时,useEffect 会重新执行:

useEffect(() => {
  // 执行副作用操作
}, [变量1, 变量2, ...]); //如果不需要监测任何变量,可以将第二个参数省略,这样 useEffect 会在每次组件重新渲染时都执行一次。

注意,依赖项数组中的每个元素都会被浅比较,如果数组中的任何一个元素的值发生变化,那么 effect 就会重新执行。因此,如果依赖项是一个引用类型的值,需要确保在更新它时使用新的引用,否则 effect 可能无法正确触发。

useEffect如何只在组件挂载时执行一次?或useEffect第二个参数用法?

  1. 空数组
    该 effect 只会在组件挂载时执行一次,而不会在组件更新时再次执行。这个行为类似于 componentDidMount 生命周期方法。
  2. 不传参数
    该 effect 会在组件挂载时执行,并在每次组件更新时执行。这个行为类似于 componentDidMount 和 componentDidUpdate 生命周期方法的结合体。
  3. 数组不为空
    该 effect 只会在依赖项的值发生变化时执行,类似于 componentDidUpdate。

如何在 useEffect 中实现异步操作?

useEffect(() => {
  async function fetchData() {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    setData(data);
  }

  fetchData();
}, []);
//由于 useEffect 不支持 async/await,因此需要在 useEffect 中定义一个 async 函数,然后在函数中进行异步操作。

useEffect如何清除副作用?

useEffect中返回一个函数清除副作用。清除函数会在组件卸载时执行,并且也会在下一次副作用操作之前执行,以确保组件的状态是干净的。

// 清除定时器
useEffect(() => {
  const timer = setInterval(() => {
    // ...
  }, 1000);

  return () => {
    clearInterval(timer);
  };
}, []);

useEffect实现生命周期方法?

useEffect(() => {
  // componentDidMount
  console.log('mounted');

  return () => {
    // componentWillUnmount
    console.log('unmounted');
  };
}, []);

useEffect(() => {
  // componentDidUpdate
  console.log('updated');
}, [count]);

在 React 18 中,useEffect 可能会在某些情况下执行两次,如何只执行一次?

1.在 useEffect 的第二个参数中添加依赖项,避免 useEffect 在不必要的情况下被重复执行:

useEffect(() => {
  // ...
}, [state]);
//这里的 state 就是需要监测的变量。只有当 state 发生变化时,useEffect 才会被重新执行

2.使用 useCallback 和 useMemo 避免不必要的重新渲染。这两个 Hook 可以缓存函数和计算结果,避免不必要的重新计算。

2.3 怎么在hooks中使用缓存?或如何缓存计算结果和函数?或用于性能优化的hooks?

  • useMemo

接受两个参数:第一个参数是一个函数,用于计算需要缓存的结果;第二个参数是一个数组,用于指定缓存结果的依赖项,当依赖项发生变化时,useMemo 会重新计算结果并返回新值。

import React, { useMemo } from 'react';

function MyComponent({ prop1, prop2 }) {
  const memoizedValue = useMemo(() => {
    // 计算缓存结果的函数
    return computeExpensiveValue(prop1, prop2);
  }, [prop1, prop2]);

  return <div>{memoizedValue}</div>;
}
//computeExpensiveValue 函数的计算结果将被缓存,只有当 prop1 和 prop2 发生变化时,useMemo 才会重新计算结果。
  • useCallback useCallback 也接受两个参数:第一个参数是一个函数,需要被缓存的函数;第二个参数是一个数组,用于指定缓存函数的依赖项,当依赖项发生变化时,useCallback 会返回一个新的函数。
import React, { useCallback } from 'react';

function MyComponent({ prop1, prop2 }) {
  const memoizedCallback = useCallback(() => {
    // 需要被缓存的函数
    doSomething(prop1, prop2);
  }, [prop1, prop2]);

  return <button onClick={memoizedCallback}>Click me</button>;
}

useMemo和useCallback的不同点:

  1. 作用不同:useMemo 的作用是缓存计算结果,而 useCallback 的作用是缓存函数的引用
  2. 参数不同:useMemo 接受计算函数和依赖项数组两个参数,而 useCallback 接受回调函数和依赖项数组两个参数。
  3. 返回值不同:useMemo 返回计算结果,而 useCallback 返回一个回调函数。

3. Hoc

  • 什么是高阶函数? 是指能够接受一个或多个函数作为参数,或者返回一个函数作为结果的函数
  • Hoc是什么?
    高阶组件(Higher-Order Component,HOC)是一个函数,它接受一个组件作为参数并返回一个新的组件。HOC 可以用于增强现有组件的功能,例如添加状态、操作 props 等。
function withTheme(Component) {
  return function ThemedComponent(props) {
    return (
      <div className="theme">
        <Component {...props} />
      </div>
    );
  };
}

4. React优化相关问题

4.1 更新性能优化

只要是组件继承自React.Component就会存在当父组件重新render的时候,子组件也会重新执行render,即使这个子组件没有任何变化。子组件只要调用setState就会重新执行render,即使setState的参数是一个空对象。(当组件的 props 或 state 发生变化时,React 会重新渲染该组件及其子组件,这可能会导致不必要的渲染和性能问题)

  • shouldComponentUpdate是什么?

shouldComponentUpdate 是 React 中的一个生命周期方法,用于判断组件是否需要重新渲染(返回为true的时候,当前组件进行render,如果返回的是false则不进行render)。
shouldComponentUpdate 方法中,我们只需要比较组件的关键属性,而不是全部属性,这样可以避免不必要的比较。关键属性通常是指会影响组件渲染的属性,例如文本内容、颜色、尺寸等。而不是一些不影响组件渲染的属性,例如 ID、创建时间等。

  • shouldComponentUpdate是如何进行优化的?

在 shouldComponentUpdate 方法中进行 props 和 state 的浅比较,判断它们是否发生了变化。

shouldComponentUpdate(nextProps, nextState) {
//通过 JavaScript 中的 Object.is 函数比较
  return !Object.is(this.props, nextProps) || !Object.is(this.state, nextState);
}
shouldComponentUpdate(nextProps, nextState) { 
// 比较 props 和 state 的值,决定是否需要重新渲染组件 
if (this.props.someProp === nextProps.someProp && this.state.someState === nextState.someState) { 
  return false; // 不需要重新渲染
} 
  return true; // 需要重新渲染 
}
  • PureComponent或React.memo是什么?

React15.3中新加了一个PureComponent类**
React在16.6版本增加了React.memo

React.PureComponent 是 React 中的一个纯组件,它默认实现了 shouldComponentUpdate 方法,可以帮助我们自动判断是否需要更新。React.PureComponent 内部会进行 props 和 state 的浅比较,从而避免不必要的组件更新。

class MyComponent extends React.PureComponent {
  // ...
}

React.memo类似于React.PureComponent,只不过用于函数组件而非class组件

const MyComponent = React.memo(function MyComponent(props) { /* render using props */ });
  • 使用immutable.js

Immutable 数据结构是指一旦创建,就不能再被修改的数据结构。
一旦创建了一个 Immutable 数据对象,就不能再修改它的值。因此,当我们需要修改一个 Immutable 数据对象时,实际上是创建了一个全新的对象,而不是修改原始的对象。 由于 Immutable 数据对象是不可变的,所以当组件的 propsstate 包含 Immutable 数据对象时,React 可以通过浅层比较(shallow comparison)来判断是否需要重新渲染组件。如果 Immutable 数据对象没有发生变化,那么 React 就不会重新渲染组件,从而提高了性能。

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: Immutable.Map({ key: 'value' })
    };
  }
  handleClick = () => {
    const newData = this.state.data.set('key', 'new value');
    this.setState({ data: newData });
  }
  render() {
    return (
      <div>
        <div>{this.state.data.get('key')}</div>
        <button onClick={this.handleClick}>Update</button>
      </div>
    );
  }
}

4.2 React.lazy() 和 Suspense

React 16.6 引入了 React.lazy()Suspense,这两个功能的目的是简化按需加载组件的过程。

React.lazy() 返回一个新的组件,这个组件的 render 方法将会渲染异步加载的组件 <Suspense> 是一个新的 React 组件,它接受一个 fallback 属性,指定在异步加载组件时显示的占位符。在这个例子中,如果 MyComponent 尚未加载完成,则显示 "Loading..."。

//React.lazy()
const MyComponent = React.lazy(() => import("./MyComponent"));
//Suspense
function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <MyComponent />
      </Suspense>
    </div>
  );
}