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操作,而非重新渲染整个页面。
- 把树形结构按照层级分解,只比较同级元素,新旧节点不同直接用新节点替换旧节点,只有两个新旧节点是相同节点的时候,才会去比较他们各自的子节点。
- 给列表结构的每个单元添加唯一的 key 属性,方便比较。
- 根据key先找到不需要移动的相同节点。
- 再找相同但是需要移动的节点。
- 最后找不到,才会去新建、删除节点。
注意:diff是按照深度优先遍历,React会递归地遍历旧虚拟DOM树和新虚拟DOM树,从上到下,从左到右。
6. state 和 props 的区别
- state: 组件内定义的数据,可以通过 setState 修改,触发组件更新
- props: 父组件传过来的数据,不可以直接修改,必须调用父组件的方法修改
7. 生命周期
![[React 生命钩子.png]]1. 挂载阶段
-
constructor 初始化state和props
-
static getDerivedStateFromProps(静态方法,不常用)返回一个对象来更新 state,如果返回 null 则不更新任何内容,当 state 的数据始终来源于 props 时使用
-
render 渲染虚拟dom
-
componentDidMount 挂载成功
-
更新阶段
-
static getDerivedStateFromProps(静态方法,不常用)返回一个对象来更新 state,如果返回 null 则不更新任何内容,当 state 的数据始终来源于 props 时使用
-
shouldComponentUpdate 判断组件是否需要更新,用来做性能优化,返回true更新,返回false不更新
-
render 渲染虚拟dom
-
getSnapshotBeforeUpdate(prevProps, prevState) (不常用)在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。
-
componentDidUpdate 组件更新完成
-
销毁阶段
-
componentWillUnmount 组件销毁之前调用,可以用来清除异步操作
8. 合成事件
- React 自己实现了一套事件注册、分发的机制,统一了不同浏览器之间事件系统的差异。
- React 组件上声明的事件最终绑定到了根节点上,而不是 React 组件对应的 DOM 节点。只有document这个节点上面才绑定了DOM原生事件,其他节点没有绑定事件。这样简化了DOM原生事件,减少了内存开销。
9. 高阶组件
定义:高阶组件是参数为组件,返回值为新组件的函数。
高阶组件的作用:
- 复用组件逻辑
常用的高阶组件:
- withRouter 把路由信息添加到组件的 props 中
- connect 链接redux仓库和组件
- React.memo 优化函数组件性能,减少不必要的更新
10. react 性能优化
10.1. 类组件性能优化: pureComponent、shouldComponentUpdate
-
PureComponent: 通过浅比较 props 和 state 来减少不必要的渲染。如果组件的 props 和 state 没有变化,那么组件不会重新渲染
-
shouldComponentUpdate:手动比较当前和新的 props 或 state 来决定组件是否应该更新
-
return false 组件不更新
-
return true 组件更新
10.2. 函数组件性能优化
-
useMemo 缓存数据减少不必要的计算
-
useCallback 缓存函数
-
React.memo 高阶组件
-
浅比较组件上一次的 props 和当前组件的 props 的内容是否一致,如果一致就不更新组件,不一致再渲染组件。
-
如果要做深层次的比较,在 memo 的第二个参数传入一个函数做比较
-
return true // 不更新
-
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
}))
}
}
- 如果对同一个值进行多次 setState , setState 的批量更新,只更新一次组件
- setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的
13. 路由两种模式的区别
- hash模式 —— 地址栏 URL 中的有 # 号
原理:使用 hashchange 事件监听地址栏 # 后的内容变化展示对应的组件 - history 模式:地址栏 URL 中的没有 # 号,使用时需要后端配置支持,否则页面刷新会 404
原理:利用了 HTML5 History 中新增的 pushState() 方法实现路由
history使用注意事项:需要后台配置支持,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,直接返回 index.html 页面,否则页面刷新会 404
14. class组件和函数组件的区别
- class 组件有生命周期,函数组件中没有,函数组件中可以使用 useEffect 实现类似生命周期的功能,但是和生命周期的思想是不同的
- class 组件中 this 指向组件实例对象,函数组件中没有 this
- class 组件中抽取公共逻辑需要使用高阶组件,函数组件中抽取公共逻辑使用自定义 hook,自定义hook比高阶组件更简洁和容易理解
15. hooks诞生的背景,为什么有class组件还要创造hooks
React Hooks 的诞生背景是为了解决类组件在代码共享、逻辑复用以及生命周期管理上的一些局限性。以下是Hooks诞生的几个关键背景和原因:
-
代码复用困难:在使用类组件时,共享逻辑通常需要使用高阶组件,但是高阶组件会增加组件嵌套的复杂性。函数组件中使用自定义hook复用逻辑比较方便理解。
-
生命周期函数的局限性:
-
类组件的生命周期方法中常常混合了不相关的逻辑,这使得代码难以维护和理解。
-
例如进入页面需要立即执行 调用接口、创建定时器、绑定原生事件这些操作时都需要在componentDidMount中写,导致此函数中的逻辑比较混乱,不方便维护,hooks可以定义多个useEffect分开处理不同的副作用逻辑。
-
避免this的复杂性:在类组件中使用 this 时需要注意函数中的 this 指向,增加了代码复杂度。函数组件中不需要使用 this,通过 hook 来管理组件的状态。
16. hook 规则
- 只能在函数最顶层使用 hook,不能在 if,循环,子函数中使用
- 只能在函数组件中和自定义 hook 中使用 hook
17. hook 为什么必须在组件顶层使用
- 在函数组件中使用 hook 时,React 会根据 hook 的调用顺序来管理状态
- 如果在 if 语句中使用 hook 可能会导致 hook 的状态丢失或执行顺序不正确
18. hook 的原理(为什么hook不能在if中使用)
- React Hooks 的实现是基于 Fiber 架构的,在 Fiber 节点的 memorizedState 上使用一个链表来存储每个 Hook 的状态。
- 访问和更新 Hooks 状态时:在组件渲染时,React 会遍历这个链表来访问和更新每个 Hook 的状态。为了确保每次渲染时 Hooks 的调用顺序一致,React 依赖于一个 hookIndex 的索引,这个索引在每次渲染时都会重置,并在每次调用 Hook 时递增。
19. Fiber 架构
19.1. Fiber 的核心概念:
- Fiber 节点:每个 React 组件对应一个 Fiber 节点,包含了组件的类型、状态、props、子节点等信息。
- 工作单元:Fiber 将渲染工作拆分成多个小的工作单元,每个工作单元对应一个 Fiber 节点,React 可以在执行这些工作单元时,根据需要进行调度和打断。
- 双缓存技术:React 维护两棵 Fiber 树,一棵是当前树(Current Tree),另一棵是工作树。当前树表示屏幕上显示的内容,工作树是正在构建的新 Fiber 树。这允许 React 进行更新时,可以在后台构建一个新的树,而不会影响当前显示的 UI。
19.2. Fiber 的作用:
- 可中断的渲染:在 fiber架构之前的 React 渲染过程中,组件树的更新是同步进行的,复杂的更新会阻塞主线程,导致用户界面卡顿。Fiber 通过将渲染工作分解为多个小的工作单元,允许在需要时打断渲染并恢复,这样就可以在高优先级任务(如用户输入)和低优先级任务(如动画)之间进行调度。
- 错误恢复: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 的工作流程可以分为两个主要阶段:
-
构建阶段:在这个阶段,React 构建 Fiber 树并计算需要进行的更新。这个过程是可中断的,React 可以根据任务的优先级来调度工作。
-
遍历组件树:从根节点开始,递归遍历组件树。
-
创建或更新 Fiber 节点:为每个组件创建新的 Fiber 节点,或更新现有的 Fiber 节点。
-
计算差异(diff):比较新旧 Fiber 树,计算出需要更新的部分。
-
生成工作单元:每个 Fiber 节点作为一个工作单元,React 可以在每个工作单元之间暂停,以便处理其他高优先级任务。
-
提交阶段:在这个阶段,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** 不会触发组件重新渲染。
作用:
-
存和组件渲染无关的数据,数据改变时组件不会更新(适合存计时器 id、上一次的值等)
-
和useState的区别:useState 的数据改变会触发组件更新
-
获取dom元素
-
可以调用函数子组件中的方法和数据(配合 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, [依赖项数组])
- 不传第二个参数时:只要组件中有数据更新就会执行函数,类似于 componentDidUpdate
- 依赖数组为空数组时: 函数只在组件初次渲染完成时执行一次,相当于 class 组件的 componentDidMount
- 数组中传入具体依赖项时:监听到传入的依赖项改变时执行函数
- 第一个参数中 return 的函数: 该函数会在组件销毁时执行,相当于 componentWillUnmount
注意:第一个参数不能传入 async 函数,因为 useEffect 的第一个参数需要返回一个函数,在组件销毁时执行,async 函数返回的是一个 promise 组件销毁时把 promise 当成函数调用会报错
useEffect(() => {
// 组件逻辑
return () => {
// 组件销毁时执行此函数
}
}, [函数里依赖的变量])
4. useCallback(缓存函数)
作用:
- useCallback 返回一个缓存的函数
- 如果依赖项改变了重新创建函数,没改变从缓存中读函数
使用场景:可以配合 React.memo 高阶组件使用优化子组件的渲染性能,避免子组件不必要的更新。例如子组件使用 React.memo 优化组件,父组件传入一个普通函数时,React.memo 会失效,因为父组件每次更新都会创建新函数,可以使用 useCallback 缓存此函数。
备注:可以使用 useMemo 实现 useCallback
// useCallback (函数,[依赖项数组]), 返回缓存后的函数
const memoizedCallback = useCallback(() => {
// 函数逻辑
},[a, b],);
// 使用 useMemo 实现 useCallback的功能
const memoizedCallback = useMemo(() => {
return () => {
// 函数逻辑
}
}, [依赖项])
5. useMemo
作用:对复杂的计算操作进行缓存,依赖项没有更新时避免重复计算
- useMemo返回一个缓存的值
- 如果依赖项改变了重新执行函数计算结果,没改变从缓存中读取值
// 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 的区别:
- useReducer不可以拆分模块,存储的数据比较多时难以维护
- useReducer没有中间件,无法扩展,无法处理异步
8. useLayoutEffect:在dom更新后,浏览器绘制之前执行
与useEffect的区别:useEffect是在浏览器绘制完成后执行,如果在 useEffect 内修改数据有可能会造成页面闪烁,如果要避免页面闪烁的话,可以使用useLayoutEffect。
22. 自定义hook
- 通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。
- 自定义 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
- Redux 是什么?解决什么问题?
- Redux 是 JavaScript 状态管理库,解决了组件间状态共享困难、数据流混乱的问题。
- 核心思想:单一数据源、状态只读、纯函数修改。
- Redux 三大原则
- 单一数据源:整个应用的 state 存储在唯一的 store 中
- State 只读:唯一改变 state 的方式是 dispatch action
- 使用纯函数执行修改:reducer 必须是纯函数,相同输入永远得到相同输出
纯函数需要满足两个条件:
-
相同输入,永远返回相同输出(确定性)
-
没有副作用(不影响外部代码,例如修改外部变量 /修改传入的参数)
-
Reducer 为什么必须是纯函数?
- 保证行为可预测,相同输入,永远返回相同输出
- redux 通过比较修改前后的 state 是否相同来决定是否重新渲染
- 支持时间旅行调试(Redux DevTools),Redux DevTools 能"回放"每一步操作,依赖的前提是每个历史 state 快照都被完整保留。
25. redux 配合 react-redux 使用流程
- 通过 createStore 创建仓库,把 reducer 函数传入 createStore
- reducer 函数的作用是返回和修改 state ,reducer 有两个参数,修改前的 state 和 action
- 组件中通过 useSelector 获取 store 数据
- 组件中通过 dispatch(action) 发送 action 触发 reducer 函数执行,修改 state 数据
- action 是一个对象,对象中必须有一个 type 属性,用来描述本次修改内容
- reducer 函数中根据传入的 action.type 判断本次修改的内容,返回新的 state
25.1. react-redux 作用
- 连接 redux 仓库和 react 组件
- 在根组件中通过 react-redux 提供的 Provider 组件把仓库数据传给应用的所有后代组件
- 函数组件中可以使用 useSelector 获取仓库的数据
26. Redux Toolkit(RTK)
特点:Redux 官方推荐的工具集,简化 Redux 开发,主要是为了解决原始 Redux 样板代码太多的问题。
使用:
createSlice:自动生成 action creators 和 reducerscreateAsyncThunk:简化异步操作- 内置
immer,支持直接"修改" state(实际是不可变更新) - 内置
redux-thunk
27. 状态管理:Zustand
概念:一个轻量级的 React 状态管理库,基于发布订阅模式,API 极简,不需要 Provider 包裹,直接用 hook 消费状态。
28. Zustand vs Redux 对比
28.1. 设计思想的根本差异
- Redux 的核心思想是严格约束,它认为状态变更必须有迹可循,所有修改都必须经过 action 描述"发生了什么",再由 reducer 纯函数处理,整个流程是强制的单向数据流。这种设计让状态变化极其可预测,但也带来了较多的概念和样板代码。
- Zustand 的思想则是够用就好,它认为状态管理不需要那么多仪式感,store 就是一个普通对象,修改状态就是调用函数,没有 action、没有 reducer、没有 dispatch,怎么直觉怎么来。
28.2. 各自的适用场景
- Redux 更适合大型团队协作的项目,因为它的强约束让每个人写出来的代码风格一致,出了问题也容易追踪。同时它成熟的生态和强大的 DevTools 对复杂业务场景更有保障。
- Zustand 更适合中小型项目、个人项目或者追求开发效率的团队,学习成本低、代码少、上手快,不需要为了简单的状态共享引入大量概念。