面试题:React 生命周期以及常用hooks

262 阅读10分钟

React类组件 的生命周期分为 三个阶段: 挂载 更新 和卸载

  1. 挂载阶段
  2. 更新阶段
  3. 卸载阶段
  4. 移除的方法
  5. 错误捕获阶段

挂载阶段(4):

1. constructor() 构造函数

2. getDerivedStateFromProps(props,state) :挂载和更新阶段都会执行

3. render() 渲染

4. componentDidMount():在render之后执行,数据获取、事件监听


问题1:constructor中一般用来做哪些事情

  • 使用super(props)获取父组件的props
  • this.state的初始化赋值
  • 在this上挂载方法:如this.xxx = this.xxx.bind(this)

问题2: super 里不传递props会怎么样

会导致在构造函数结束前 this.props的值是undefiend;


问题3: getDerivedStateFromProps 可以访问this吗

不可以,他是个静态方法,不能访问组件实例


问题4: getDerivedStateFromProps(props,state) 返回什么?

该方法需要返回一个新的对象作为新的state或者返回null表示state状态不需要更新 该生命周期 无论props发生变化还是state变化都会调用


问题5: render中要注意什么?

不要使用setState 会导致死循环

更新阶段(5)

1. getDerivedStateFromProps(props,state)

2. shouldComponentUpdate(nextProps,nextState)

3. render()

4. getSnapshotBeforeUpdate(prevProps,prevState)

5. componentDidUpdate()


问题1: shouldComponentUpdate(nextProps,nextState) 性能优化

默认返回true,如果不需要更新 返回false表示不需要更新; 不建议进行深层比较会影响效率

对于复杂的逻辑,可以考虑使用 PureComponent 或 React.memo(对于函数组件),它们内部实现了浅比较props和state 的 shouldComponentUpdate 逻辑。

⚠️ 不能在内部使用setState 会导致死循环


问题5: getSnapshotBeforUpdate 一般用来做什么?

在render之后,但是dom元素还没有被更新;返回一个snapshot的值,作为componentDidUpdate的第三个参数传入;

用于获取组件更新前的一些信息,如组件滚动条位置之类的;在组件更新后可以恢复一些ui视觉上的状态

卸载阶段(1)

1. componentWillUnMount()


不安全的生命周期(3)-v16 之后已废弃

1. componentWillmount

  1. 它可能会被调用多次,因为在某些情况下(如服务器端渲染),组件可能会被多次实例化和挂载

  2. 对于异步操作,在 componentWillMount 中调用异步操作可能会导致问题,因为它不会等待操作完成就开始渲染,可能导致组件渲染时数据还未准备好。

2. componentWillRecevieProps(nextProps):初始渲染时不会触发

这个方法允许你在组件接收到新 props 时执行某些操作,比如根据新 props 来更新状态(state)或执行一些副作用操作。

componentWillReceiveProps的行为·有时会导致混淆·,因为它会在组件接收到新的props 时被调用,而不管这些 props是否实际发生了变化。这可能会导致不必要的状态更新和性能开销

3. componentWillUpdate():组件即将更新前调用

componentWillUpdate 在异步渲染环境中可能会导致不一致的行为和难以调试的问题,因为它可能在不可预测的时机被触发。


错误捕获(2)-V16后引入的

在 React 16 之前,React 没有内置的机制来捕获组件渲染期间发生的错误,一旦渲染过程中出现错误,会导致整个 React 应用崩溃。componentDidCatch 的引入是为了解决这个问题,它允许开发者在组件树中捕获错误,从而提供了一种更友好的错误处理方式,避免应用崩溃,同时可以显示降级的 UI(fallback UI)或进行错误日志记录等操作。

v16引入的:

  • getDerivedStateFromError() 捕获后代组件中的JS错误并更新组件状态。
  • compnentDidCatch()你可以将错误信息发送到错误日志服务,例如 Sentry 或自己的后端服务

术语:统一错误处理可以 提高应用的健壮性和用户体验,避免因捕获到的错误导致整个应用的崩溃

1. static getDerivedStateFromError():统一误处理的工具

getDerivedStateFromError是一个静态方法,这意味着你不能在该方法中使用 this 关键字,因为它在组件实例化之前被调用。

2. compnentDidCatch: 配合1使用 记录错误

  • componentDidCatch主要是捕获渲染过程中的错误,不会捕获以下情况的错误:

    • 事件处理程序中的错误,需要使用 try-catch来捕获:onClick
    • 异步代码中的错误,需要使用 try-catch 或 .catch() 方法:setTimeout,Promise,await
    • 服务端渲染中的错误,需要使用其他机制:SSR

React 函数组件Hooks

React函数组件没有生命周期,常用的hooks有下面几个:

基础:

  1. 状态usestate():设置state
  2. 异步 useEffect():执行副作用
  3. 同步 useLayoutEffect(): 在所有dom变更后同步执行,在浏览器绘制之前

useEffect中的副作用操作会在浏览器渲染完成后异步执行不会阻塞页面的渲染。获取数据

useLayoutEffect 中的操作会在 DOM 变更后立即同步执行,在浏览器绘制之前运行。以避免布局抖动或闪烁

面试题😁:useLayoutEffect 怎么实现的同步?会在dom变更后 浏览器执行前 同步绘制


useEffect(回调函数,依赖数组):

  • 回调函数内部返回一个清理函数:该函数会在组件卸载或者重新渲染前被调用,用于清理上一次的副作用操作:如计时器,订阅
  • 第二个参数:
    • []: 仅在组件挂载和卸载时执行一次
    • 不提供数组:组件每次渲染都会更新
    • [xx,xx2]: 当xx和xx2变化的时候,副作用会重新执行

副作用操作:除了组件渲染以外的操作,如 数据获取,订阅,手动修改dom,记录日志等;


性能优化:

  1. useMemo():缓存变量,返回一个计算结果;进行复杂数据处理时使用
  2. useCallBack():缓存函数,返回一个函数;回调函数是子组件的事件处理器时使用;

useMemo(fn,依赖项) : 避免在每次渲染时都进行复杂的计算和重新创建对象。通过记住上一次的计算结果,只有在依赖项发生变化时才重新计算,从而提高性能。;通常用于避免在每次渲染时都执行昂贵的计算,例如,在渲染大量数据时进行复杂的数学运算或数据处理。

useCallback(fn, 依赖项): 避免在每次渲染时都重新创建函数。通过记住上一次创建的函数,只有在依赖项发生变化时才重新创建新的函数,从而提高性能。;useCallback则通常用于避免在每次渲染时都重新创建函数,特别是在回调函数作为子组件的事件处理器时,可以避免不必要的重新渲染。


上下文对象:

  1. useContext():共享数据

import { createContext } from "react";
const MyContext = createContext(null);
export default MyContext;
// import 上面的 MyContext;

<MyContext.Provide value={{a:"sss",b:"sss1"}}>
// 子组件
</MyContext.Provid>

// 子组件 
const {a,b}  = useContext(MyContext)
// 如果是类组件 
const valueObject = this.context;

ref:

  1. useRef():用于创建对dom的引用

创建ref的方法有4种:

  1. 字符串 不推荐了
  2. 类组件使用React.createRef()
  3. 函数式组件使用React.UseRef()
  4. 传入函数ref = {element => this.myref = element}
  1. useImperativeHandle(): 结合forwardRef,向父组件暴露方法或者属性,可通过ref.current.xx 获取

问题1:为什么需要结合forwardRef?

因为默认情况下, 函数式组件 不能像类组件一样接收ref;为了让函数式组件可以接收red,需要使用React.forwardRef 来转发 ref forwardRef 是一个高阶函数,它接收一个函数式组件作为参数。


// 这样是会报错的,因为不允许在函数式组件上使用ref
<myComponent  ref={this.myRef}>
// 子组件中使用forwardRef包裹
const myComponent  = (props,ref)=>{
    // 如果父组件中需要使用子组件中的方法 就需要使用 useImperativeHandle
    useImperativeHandle(ref, ()=>{
        scroll: listRef.current.scroll 
        setScroll: (top)=>{
            listRef.current.scroll = top;
        }
    })
}
export default forwardRef(myComponent)
  1. 状态管理useReducer: react 内置的

用于管理复杂的状态逻辑,接受一个reducer函数 和一个初始状态,并返回一个状态值 和dispatch函数;适合在组件内部有复杂状态更新逻辑时使用


import { useReducer } from "react";

// 定义 action 类型
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
 
// 定义 reducer 函数
function counterReducer(state, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 };
    case DECREMENT:
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}


const Count = () =>{
    
    const [state,dispatch]  = useReducer(counterReducer,{count:0})
    return (
        <div>
          <p>Count: {state.count}</p>
          <button onClick={() => dispatch({ type: INCREMENT })}>+</button>
          <button onClick={() => dispatch({ type: DECREMENT })}>-</button>
        </div>
      );
}

export default Count;

Redux hooks:

  1. useDispatch():分发action
  2. useSelector():读取数据
  3. useStore():获取整个redux store 实例

React router hooks:

  1. useHistory
  2. useNavigate: 在reactRouterV6 中取代了 useHistory
  3. useLocation: 返回当前location对象
  4. useParams: 动态参数获取




区别:

1. useMemo和useCallback的区别 ?

useMemo 缓存计算结果,useCallback缓存函数

2. useEffect 和 useLayoutEffect 的区别?

useEffect 异步,useLayoutEffect 同步

  • 执行时机不同
    • useEffect是浏览器绘制后异步执行;
    • useLayoutEffect是在dom更新后 同步执行,在浏览器绘制前;
  • 性能上
    • 前者不会阻塞渲染;
    • 后者阻塞渲染;
  • 使用场景:
    • 前者是用户数据获取、订阅、异步操作、非关键的 DOM 修改等
    • 直接的 DOM 操作,如测量布局、同步更新布局样式等,需要在布局完成后立即进行操作,以保证布局的一致性。

3. useDispatch 和useReducer()区别?

useReducer内置的hooks,组件内状态更新; useDispatch() redux提供的,全局更新

  • 来源:一个是redux提供的 ;一个是react内置的
  • 用途:前者用于在react组件中派发actions到redux store; 后者用于管理复杂状态的逻辑
  • 工作原理:
    • useReducer当你调用 dispatch 函数并传入一个 action 时,reducer 函数会根据 action 的类型来更新状态。
    • useDispatch 当你调用 dispatch 函数并传入一个 action 时,Redux store 会根据当前的 state 和传入的 action 来计算下一个 state
  • 使用场景:
    • 适合在全局状态管理(如应用设置、用户信息等)时使用,特别是当状态需要在多个组件之间共享时。
    • 适合在组件内部有复杂状态更新逻辑时使用。
  • 状态管理范围:
    • useReducer 主要用于组件内部的状态管理;
    • 而 useDispatch 用于全局状态管理,通过 Redux store 来共享状态。
  • 状态更新机制:
    • useReducer 通过 reducer 函数在组件内部更新状态;
    • useDispatch 派发 actions 到 Redux store,由store根据 reducers 来更新全局状态。

react中的一些性能优化手段

  1. 类组件使用shouldComponentUpdate或pureComponent进行精准控制,减少不必要的更新;
  2. 函数组件使用React.Memo

pureComponent || React.Memo()内部实现了浅比较 props 和state 的shouldComponentUpdate逻辑

  1. 使用useMemo 和useCallback 进行缓存
  2. 使用 useEffect 的依赖数组优化
  3. 对于长列表或大量数据的渲染,使用虚拟化列表可以避免渲染不必要的元素,提高性能。
  4. 使用 React 的 React.lazy 和 Suspense 实现代码拆分和懒加载,减少初始加载时间。
  5. 减少组件的嵌套层数,避免不必要的组件更新,可以使用 React.memo 或 shouldComponentUpdate 来控制组件的重新渲染。
  6. 生产环境构建:使用生产环境构建提高性能。
  7. 性能分析:使用 Profiler 分析性能,找出性能瓶颈。