← Frontend / Interview

React

React 框架相关面试问题

1. 为什么组件中没有使用 React 变量还需要引入

  • 因为 react 中的 JSX 语法本质上是 React.createElement 方法的语法糖
  • 在组件中使用 JSX 语法时,JSX 会被转换为 React.createElement 函数调用,因此需要在组件中引入 React 变量
  • 注意: React 17 起引入了新的 JSX 转换(react/jsx-runtime),不再需要手动引入 React

2. react 组件传参

  • 父 -> 子: props
  • 子 -> 父: 子组件调用父组件传过来函数,通过函数的参数传递
  • 嵌套传参:context(Provider 组件把数据传给所有的后代组件, 后代组件使用 Consumer 或者useContext 接收 Provider 传过来的数据)
  • 全局数据使用状态管理工具:redux、zustand

3. 受控组件和非受控组件

3.1. 受控组件

受控组件是指组件的状态由 state 控制 。例如表单元素的 value 受组件的state控制,需要通过 onChange 事件修改 state。

3.2. 非受控组件

非受控组件第一次渲染时可以通过 defaultValue 设置默认值,后续表单的操作和组件的状态无关联。

4. react 中 key 的作用

  • key 的主要作用是为每一个列表元素提供一个稳定的身份标识。在数据变更时(如元素的添加、移除或排序),React 通过比较新旧两个列表中的 key 来判断哪些元素是相同的,从而决定是否需要实际更新 DOM。
  • 提高 diff 的同级比较的效率。

5. diff 算法和虚拟dom

5.1. 虚拟dom

作用和概念:用来描述真实 DOM 的对象,虚拟 DOM 可以在组件状态更新时进行快速比较,查找出页面中真正需要变化的部分,从而避免了直接操作真实DOM带来的性能问题。

5.2. diff 算法

diff算法的作用:计算出虚拟 DOM 中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面。

  1. 把树形结构按照层级分解,只比较同级元素,新旧节点不同直接用新节点替换旧节点,只有两个新旧节点是相同节点的时候,才会去比较他们各自的子节点。
  2. 给列表结构的每个单元添加唯一的 key 属性,方便比较。
  3. 根据key先找到不需要移动的相同节点。
  4. 再找相同但是需要移动的节点。
  5. 最后找不到,才会去新建、删除节点。

注意:diff是按照深度优先遍历,React会递归地遍历旧虚拟DOM树和新虚拟DOM树,从上到下,从左到右。

6. state 和 props 的区别

  • state: 组件内定义的数据,可以通过 setState 修改,触发组件更新
  • props: 父组件传过来的数据,不可以直接修改,必须调用父组件的方法修改

7. 生命周期

![[React 生命钩子.png]]1. 挂载阶段

  1. constructor 初始化state和props

  2. static getDerivedStateFromProps(静态方法,不常用)返回一个对象来更新 state,如果返回 null 则不更新任何内容,当 state 的数据始终来源于 props 时使用

  3. render 渲染虚拟dom

  4. componentDidMount 挂载成功

  5. 更新阶段

  6. static getDerivedStateFromProps(静态方法,不常用)返回一个对象来更新 state,如果返回 null 则不更新任何内容,当 state 的数据始终来源于 props 时使用

  7. shouldComponentUpdate 判断组件是否需要更新,用来做性能优化,返回true更新,返回false不更新

  8. render 渲染虚拟dom

  9. getSnapshotBeforeUpdate(prevProps, prevState) (不常用)在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。

  10. componentDidUpdate 组件更新完成

  11. 销毁阶段

  12. componentWillUnmount 组件销毁之前调用,可以用来清除异步操作

8. 合成事件

  1. React 自己实现了一套事件注册、分发的机制,统一了不同浏览器之间事件系统的差异。
  2. React 组件上声明的事件最终绑定到了根节点上,而不是 React 组件对应的 DOM 节点。只有document这个节点上面才绑定了DOM原生事件,其他节点没有绑定事件。这样简化了DOM原生事件,减少了内存开销。

9. 高阶组件

定义:高阶组件是参数为组件,返回值为新组件的函数。

高阶组件的作用:

  • 复用组件逻辑

常用的高阶组件:

  • withRouter 把路由信息添加到组件的 props 中
  • connect 链接redux仓库和组件
  • React.memo 优化函数组件性能,减少不必要的更新

10. react 性能优化

10.1. 类组件性能优化: pureComponent、shouldComponentUpdate

  1. PureComponent: 通过浅比较 props 和 state 来减少不必要的渲染。如果组件的 props 和 state 没有变化,那么组件不会重新渲染

  2. shouldComponentUpdate:手动比较当前和新的 props 或 state 来决定组件是否应该更新

  3. return false 组件不更新

  4. return true 组件更新

10.2. 函数组件性能优化

  1. useMemo 缓存数据减少不必要的计算

  2. useCallback 缓存函数

  3. React.memo 高阶组件

  4. 浅比较组件上一次的 props 和当前组件的 props 的内容是否一致,如果一致就不更新组件,不一致再渲染组件。

  5. 如果要做深层次的比较,在 memo 的第二个参数传入一个函数做比较

  6. return true // 不更新

  7. return false // 更新

const Test = props => {
  return (
    <div>
      <h3>Test组件</h3>
      <p>{props.num}</p>
    </div>
  )
}
// 性能优化的高阶组件
// 浅比较上一次的 props 的内容,和当前组件的 props 的内容是否一致,
// 如果一致就不更新组件,不一致再更新组件
export default React.memo(Test)

// 如果要做深层次的比较,在 memo 的第二个参数传入一个函数做比较
export default React.memo(Test, (prevProps, props) => {
	// 做深层比较
  if (prevProps.a.b.c === props.a.b.c) {
  	return true // 不更新
  }
  return false // 更新
})

10.3. 懒加载组件

使用React.lazy和 Suspense 可以实现组件的懒加载,这些组件只会在真正需要时才加载和渲染,可以减少应用的初始加载时间。

11. react 组件懒加载

定义:组件懒加载是指在组件被需要时才进行加载和渲染,而不是在页面加载时就全部加载。这样可以有效提高页面的加载速度和性能。

原理:利用 webpack 代码分割功能在打包时把异步引入的组件单独打包成一个js文件,然后调用组件时再动态加载改该组件的 js 文件

import { lazy, Suspense } from 'react'

// 异步加载组件,必须配合 Suspense 组件使用
const Detail = lazy(() => import(/* webpackChunkName: "detail" */ '组件路径'))

// fallback:Detail组件加载成功之前展示的内容
<Suspense fallback={<div style={{ fontSize: '50px' }}>loading...</div>}>
	<Detail />
</Suspense>

12. setState

class App extends React.Component {
	state = {
  	count: 0,
    title: '测试'
  }
	changeCount = () => {
    // 可以传入对象或者函数
    // 传入的对象和 state 的对象进行合并
  	this.setState({
    	count: 1
    })

    // 调用setState需要获取最新的数据时传入函数
    // 函数返回的对象和 state 的对象进行合并
    this.setState(state => ({
      count: state.count + 1
    }))
  }
}
  1. 如果对同一个值进行多次 setState , setState 的批量更新,只更新一次组件
  2. setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的

13. 路由两种模式的区别

  1. hash模式 —— 地址栏 URL 中的有 # 号
    原理:使用 hashchange 事件监听地址栏 # 后的内容变化展示对应的组件
  2. history 模式:地址栏 URL 中的没有 # 号,使用时需要后端配置支持,否则页面刷新会 404

原理:利用了 HTML5 History 中新增的 pushState() 方法实现路由

history使用注意事项:需要后台配置支持,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,直接返回 index.html 页面,否则页面刷新会 404

14. class组件和函数组件的区别

  1. class 组件有生命周期,函数组件中没有,函数组件中可以使用 useEffect 实现类似生命周期的功能,但是和生命周期的思想是不同的
  2. class 组件中 this 指向组件实例对象,函数组件中没有 this
  3. class 组件中抽取公共逻辑需要使用高阶组件,函数组件中抽取公共逻辑使用自定义 hook,自定义hook比高阶组件更简洁和容易理解

15. hooks诞生的背景,为什么有class组件还要创造hooks

React Hooks 的诞生背景是为了解决类组件在代码共享、逻辑复用以及生命周期管理上的一些局限性。以下是Hooks诞生的几个关键背景和原因:

  1. 代码复用困难:在使用类组件时,共享逻辑通常需要使用高阶组件,但是高阶组件会增加组件嵌套的复杂性。函数组件中使用自定义hook复用逻辑比较方便理解。

  2. 生命周期函数的局限性:

  3. 类组件的生命周期方法中常常混合了不相关的逻辑,这使得代码难以维护和理解。

  4. 例如进入页面需要立即执行 调用接口、创建定时器、绑定原生事件这些操作时都需要在componentDidMount中写,导致此函数中的逻辑比较混乱,不方便维护,hooks可以定义多个useEffect分开处理不同的副作用逻辑。

  5. 避免this的复杂性:在类组件中使用 this 时需要注意函数中的 this 指向,增加了代码复杂度。函数组件中不需要使用 this,通过 hook 来管理组件的状态。

16. hook 规则

  1. 只能在函数最顶层使用 hook,不能在 if,循环,子函数中使用
  2. 只能在函数组件中和自定义 hook 中使用 hook

17. hook 为什么必须在组件顶层使用

  1. 在函数组件中使用 hook 时,React 会根据 hook 的调用顺序来管理状态
  2. 如果在 if 语句中使用 hook 可能会导致 hook 的状态丢失或执行顺序不正确

18. hook 的原理(为什么hook不能在if中使用)

  1. React Hooks 的实现是基于 Fiber 架构的,在 Fiber 节点的 memorizedState 上使用一个链表来存储每个 Hook 的状态。
  2. 访问和更新 Hooks 状态时:在组件渲染时,React 会遍历这个链表来访问和更新每个 Hook 的状态。为了确保每次渲染时 Hooks 的调用顺序一致,React 依赖于一个 hookIndex 的索引,这个索引在每次渲染时都会重置,并在每次调用 Hook 时递增。

19. Fiber 架构

19.1. Fiber 的核心概念:

  1. Fiber 节点:每个 React 组件对应一个 Fiber 节点,包含了组件的类型、状态、props、子节点等信息。
  2. 工作单元:Fiber 将渲染工作拆分成多个小的工作单元,每个工作单元对应一个 Fiber 节点,React 可以在执行这些工作单元时,根据需要进行调度和打断。
  3. 双缓存技术:React 维护两棵 Fiber 树,一棵是当前树(Current Tree),另一棵是工作树。当前树表示屏幕上显示的内容,工作树是正在构建的新 Fiber 树。这允许 React 进行更新时,可以在后台构建一个新的树,而不会影响当前显示的 UI。

19.2. Fiber 的作用:

  1. 可中断的渲染:在 fiber架构之前的 React 渲染过程中,组件树的更新是同步进行的,复杂的更新会阻塞主线程,导致用户界面卡顿。Fiber 通过将渲染工作分解为多个小的工作单元,允许在需要时打断渲染并恢复,这样就可以在高优先级任务(如用户输入)和低优先级任务(如动画)之间进行调度。
  2. 错误恢复:Fiber 架构改进了错误处理机制,使得 React 能够在渲染过程中捕获错误,并从错误中恢复,而不是导致整个渲染过程失败。

Fiber History

  • React Fiber 架构是在 React 16.0 版本(2017年9月发布)引入的,而不是 16.8.0。16.8.0 是 Hooks 推出的版本。
  • Fiber 主要解决了 React 15 及之前版本的同步渲染阻塞主线程问题。

20. Fiber 的工作流程(setState之后发生了什么)

React Fiber 的工作流程可以分为两个主要阶段:

  1. 构建阶段:在这个阶段,React 构建 Fiber 树并计算需要进行的更新。这个过程是可中断的,React 可以根据任务的优先级来调度工作。

  2. 遍历组件树:从根节点开始,递归遍历组件树。

  3. 创建或更新 Fiber 节点:为每个组件创建新的 Fiber 节点,或更新现有的 Fiber 节点。

  4. 计算差异(diff):比较新旧 Fiber 树,计算出需要更新的部分。

  5. 生成工作单元:每个 Fiber 节点作为一个工作单元,React 可以在每个工作单元之间暂停,以便处理其他高优先级任务。

  6. 提交阶段:在这个阶段,React 将所有的更改应用到实际的 DOM 上。这个过程是同步的、不可中断的。

21. 常用hook

1. useState

  • 作用:函数组件中定义数据
  • 用法:返回一个数组,数组中的第一个值是数据,第二个值是更新数据的函数
// 返回一个 state,以及更新 state 的函数。
const [state, setState] = useState(initialState);

//setState 函数用于更新 state。它接收一个新的 state 值或者一个函数
setState(newState);
setState(state => newState); // 函数的参数是最新的 state

2. useRef

描述:ref 对象在组件的整个生命周期内持续存在,且修改 **.current** 不会触发组件重新渲染。

作用:

  1. 存和组件渲染无关的数据,数据改变时组件不会更新(适合存计时器 id、上一次的值等)

  2. 和useState的区别:useState 的数据改变会触发组件更新

  3. 获取dom元素

  4. 可以调用函数子组件中的方法和数据(配合 forwardRef 和 useImperativeHandle )

注意:当 ref 对象内容发生变化时,useRef 并**不会**通知你。变更 .current 属性不会引发组件重新渲染。类似 class 组件中的实例属性

// 1. 父组件中定义 ref 对象传给子组件
// 2. 子组件通过 forwardRef 转发副组件入的 ref 属性
// 3. 子组件中使用 useImperativeHandle 给 ref 赋值,让父组件可以通过ref调用子组件的方法和数据

// 父组件
const App = () => {
  const inputRef = useRef()
  // 把ref对象传给子组件
  // inputRef.current === { focus, a }
  return (
    <FancyInput ref={inputRef} />
  )
}


// 子组件
import { useRef, useImperativeHandle, forwardRef } from 'react'
function FancyInput(props, ref) {
  // ref: 父组件传过来的ref对象,不使用 forwardRef 无法获取此对象

  const inputRef = useRef();
  // 给父组件传过来的 ref 添加值
  useImperativeHandle(ref, () => {
    // return 的对象会赋值给传入的ref对象的current属性
    return {
      focus: () => inputRef.current.focus(),
      a: 100
    }
  });

  return <input ref={inputRef} ... />;
}

// 转发 ref 属性
FancyInput = forwardRef(FancyInput);

3. useEffect

作用:处理组件中的副作用(例如请求接口,创建定时器,获取dom),可以实现类似生命周期的功能

用法:传入两个参数,第一个是函数,第二个是依赖项数组,useEffect(callback, [依赖项数组])

  1. 不传第二个参数时:只要组件中有数据更新就会执行函数,类似于 componentDidUpdate
  2. 依赖数组为空数组时: 函数只在组件初次渲染完成时执行一次,相当于 class 组件的 componentDidMount
  3. 数组中传入具体依赖项时:监听到传入的依赖项改变时执行函数
  4. 第一个参数中 return 的函数: 该函数会在组件销毁时执行,相当于 componentWillUnmount

注意:第一个参数不能传入 async 函数,因为 useEffect 的第一个参数需要返回一个函数,在组件销毁时执行,async 函数返回的是一个 promise 组件销毁时把 promise 当成函数调用会报错

useEffect(() => {
  // 组件逻辑
	return () => {
    // 组件销毁时执行此函数
  }
}, [函数里依赖的变量])

4. useCallback(缓存函数)

作用:

  1. useCallback 返回一个缓存的函数
  2. 如果依赖项改变了重新创建函数,没改变从缓存中读函数

使用场景:可以配合 React.memo 高阶组件使用优化子组件的渲染性能,避免子组件不必要的更新。例如子组件使用 React.memo 优化组件,父组件传入一个普通函数时,React.memo 会失效,因为父组件每次更新都会创建新函数,可以使用 useCallback 缓存此函数。

备注:可以使用 useMemo 实现 useCallback

// useCallback (函数,[依赖项数组]), 返回缓存后的函数
const memoizedCallback = useCallback(() => {
    // 函数逻辑
},[a, b],);

// 使用 useMemo 实现 useCallback的功能
const memoizedCallback = useMemo(() => {
	return () => {
    // 函数逻辑
  }
}, [依赖项])

5. useMemo

作用:对复杂的计算操作进行缓存,依赖项没有更新时避免重复计算

  1. useMemo返回一个缓存的值
  2. 如果依赖项改变了重新执行函数计算结果,没改变从缓存中读取值
// useMemo(有返回值的函数,[函数依赖的变量]),返回函数的返回值
const memoizedValue = useMemo(() => {
	return a + b
}, [a, b]);

注意:不是所有场景都需要 useCallback 和 useMemo,因为每次组件更新时都会进行依赖项比较,多余的 useMemo 和 useCallback 会增加依赖项的比较次数。

6. useContext

作用:接收 Provider 传给后代组件的数据

7. useReducer

  • useState 的替代方案。它接收一个 reducer函数,并返回当前的 state 以及与其配套的 dispatch 方法。state 逻辑较复杂时可以使用useReducer代替 useState
// 返回一个数组,第一个值是state,第二个值是dispatch函数
const [state, dispatch] = useReducer(reducer, initialArg);
  • useReducer 和 redux 的区别:
  1. useReducer不可以拆分模块,存储的数据比较多时难以维护
  2. useReducer没有中间件,无法扩展,无法处理异步

8. useLayoutEffect:在dom更新后,浏览器绘制之前执行

与useEffect的区别:useEffect是在浏览器绘制完成后执行,如果在 useEffect 内修改数据有可能会造成页面闪烁,如果要避免页面闪烁的话,可以使用useLayoutEffect。

22. 自定义hook

  1. 通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。
  2. 自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

23. react 和 vue 的区别

  • 相同点:

  • 都是数据驱动视图改变

  • 都有 virtual DOM (虚拟dom),组件化开发

  • 不同点:

  • vue 的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立Watcher来监听,当属性变化的时候,响应式的更新对应的虚拟dom。

  • react 主张函数式编程,数据不可变,单向数据流,需要通过 setSetate 更新页面

  • vue 提供指令方便dom操作(v-if、v-for),react 没有指令

  • react jsx 语法可扩展性比较强

24. 状态管理:redux

  1. Redux 是什么?解决什么问题?
  • Redux 是 JavaScript 状态管理库,解决了组件间状态共享困难、数据流混乱的问题。
  • 核心思想:单一数据源、状态只读、纯函数修改。
  1. Redux 三大原则
  • 单一数据源:整个应用的 state 存储在唯一的 store 中
  • State 只读:唯一改变 state 的方式是 dispatch action
  • 使用纯函数执行修改:reducer 必须是纯函数,相同输入永远得到相同输出

纯函数需要满足两个条件:

  1. 相同输入,永远返回相同输出(确定性)

  2. 没有副作用(不影响外部代码,例如修改外部变量 /修改传入的参数)

  3. Reducer 为什么必须是纯函数?

  • 保证行为可预测,相同输入,永远返回相同输出
  • redux 通过比较修改前后的 state 是否相同来决定是否重新渲染
  • 支持时间旅行调试(Redux DevTools),Redux DevTools 能"回放"每一步操作,依赖的前提是每个历史 state 快照都被完整保留。

25. redux 配合 react-redux 使用流程

  1. 通过 createStore 创建仓库,把 reducer 函数传入 createStore
  2. reducer 函数的作用是返回和修改 state ,reducer 有两个参数,修改前的 state 和 action
  3. 组件中通过 useSelector 获取 store 数据
  4. 组件中通过 dispatch(action) 发送 action 触发 reducer 函数执行,修改 state 数据
  5. action 是一个对象,对象中必须有一个 type 属性,用来描述本次修改内容
  6. reducer 函数中根据传入的 action.type 判断本次修改的内容,返回新的 state

25.1. react-redux 作用

  1. 连接 redux 仓库和 react 组件
  2. 在根组件中通过 react-redux 提供的 Provider 组件把仓库数据传给应用的所有后代组件
  3. 函数组件中可以使用 useSelector 获取仓库的数据

26. Redux Toolkit(RTK)

特点:Redux 官方推荐的工具集,简化 Redux 开发,主要是为了解决原始 Redux 样板代码太多的问题。

使用:

  • createSlice:自动生成 action creators 和 reducers
  • createAsyncThunk:简化异步操作
  • 内置 immer,支持直接"修改" state(实际是不可变更新)
  • 内置 redux-thunk

27. 状态管理:Zustand

概念:一个轻量级的 React 状态管理库,基于发布订阅模式,API 极简,不需要 Provider 包裹,直接用 hook 消费状态。

28. Zustand vs Redux 对比

28.1. 设计思想的根本差异

  1. Redux 的核心思想是严格约束,它认为状态变更必须有迹可循,所有修改都必须经过 action 描述"发生了什么",再由 reducer 纯函数处理,整个流程是强制的单向数据流。这种设计让状态变化极其可预测,但也带来了较多的概念和样板代码。
  2. Zustand 的思想则是够用就好,它认为状态管理不需要那么多仪式感,store 就是一个普通对象,修改状态就是调用函数,没有 action、没有 reducer、没有 dispatch,怎么直觉怎么来。

28.2. 各自的适用场景

  1. Redux 更适合大型团队协作的项目,因为它的强约束让每个人写出来的代码风格一致,出了问题也容易追踪。同时它成熟的生态和强大的 DevTools 对复杂业务场景更有保障。
  2. Zustand 更适合中小型项目、个人项目或者追求开发效率的团队,学习成本低、代码少、上手快,不需要为了简单的状态共享引入大量概念。