[React] 2023年最新面试题
1. class 组件与函数组件的区别
- 语法不同
类组件: 使用 ES6 的类语法定义组件。具有 render 方法,并在类组件中使用 this 来引用组件实例。
函数组件: 使用函数定义组件,通常是无状态的。在 React 16.8 之后,可以使用 Hook 来在函数组件中使用状态和其他 React 特性。
- 状态的管理不同
类组件: 使用 this.state 和 this.setState 来管理组件的状态。
函数组件: 使用 useState Hook 来在函数组件中添加状态。
- 声明周期方法不同
类组件: 有一系列生命周期方法,如 componentDidMount、componentDidUpdate、componentWillUnmount 等,用于处理组件的生命周期事件。
函数组件: 使用 useEffect Hook 来处理函数组件的生命周期事件
const MyFunctionComponent = () => {
useEffect(() => {
// 在组件挂载后和更新后执行
return () => {
// 在组件卸载前执行
};
},
[] // 第二个参数来控制何时运行副作用函数
);
- 复杂性:
class 组件语法相对冗长,可能导致较多样板代码。
生命周期方法分散在不同生命周期阶段。
函数组件语法更简洁,减少样板代码。
生命周期相关逻辑集中在 useEffect 中,提高可读性。
- 可读性和维护性:
函数组件使代码更易读、易写。
更好地符合 React 社区的最新实践和推荐。
总的来说,函数组件和 Hook 的方式更加推荐,特别是在新项目中。它们使得 React 组件更易于理解、编写和维护,同时提供了更灵活的状态管理和生命周期处理机制。类组件仍然存在,可能在老项目中或特殊场景中使用。
2. react 18 新特性有那些?
新增 createRoot API
自动批处理
React 18 通过默认做更多批量处理来增加开箱即用性能提升。批量处理指的是 React 为了提高性能将多个 state 更新分组到一个单独的重渲染。
如果仍然希望setState之后立即重新渲染,只需要使用flushSync
包裹
function handleClick() {
// React 18 之前的版本
(/*...*/).then(() => {
setCount(c => c + 1); // 立刻重新渲染
setShow(show => !show); // 立刻重新渲染
});
}
function handleClick() {
// React 18
fecth(/*...*/).then(() => {
ReactDOM.flushSync(() => {
setCount(c => c + 1); // 立刻重新渲染
setFlag(f => !f);
})
})
}
过渡更新
用于并发渲染的并发新特性,开发者希望能够在Web Platform引入并发渲染,来实现多个渲染任务的并行渲染,其中Suspense就是基于此诞生的。
React18支持并发特性的三个API
- startTransition() 在屏幕内容之间进行导航,而不会阻塞用户输入
- useDeferredValue() 节流处理开销巨大的重新渲染
- useTransition() startTransition的 hooks钩子
import { startTransition } from 'react';
// 紧急更新
setInputValue(input)
// 标记回调函数内的更新为 非紧急更新
startTransition(() => {
setSearchQuery(input)
})
这个hook适用于设置延迟值
function Page() {
const [filters, mergeFilter] = useMergeState(defaultFilters);
const deferedFilters = React.useDeferredValue(filters);
return (
<>
<Filters filters={filters} />
<List filters={deferedFilters}>
</>
)
}
useDeferredValue() 会将List组件的渲染变得更加平滑,深层次看来是 defered value 引起的渲染会被标记为不紧急渲染,会被filters引起的渲染进行抢占,进而达到用户快速输入搜索等场景下页面抖动或卡顿问题。
新的Hook
- useId 用于生成在客户端和服务端两侧都独一无二的 id
- useTransition 将一些状态更新标记为过渡更新
- useDeferredValue 允许推迟渲染树的非紧急更新,类似防抖
- useSyncExternalStore 允许使用第三方状态管理来支持并发模式,并且能通过对 store 进行强制更新实现数据同步。
- useInsertionEffect 允许 CSS-in-JS 库解决在渲染中注入样式的性能问题。
3. redux 和 react-redux 的区别
react-redux 是一个用于在 React 应用中管理状态(state)的库,它是基于 Redux 的 React 绑定库。Redux 是一种用于管理 JavaScript 应用程序状态的可预测状态容器,而 react-redux 则简化了在 React 应用中使用 Redux 的过程。
4. redux 中间件的原理
Redux 中间件是一种在 action 被发起和 reducer 处理这个 action 之间进行操作的机制。中间件允许你在 Redux 应用中的 dispatch 过程中拦截、检查、或者修改 action 和 state。Redux 中间件的原理涉及到 Redux 的 dispatch 过程、函数式编程的概念,以及中间件的链式调用。
Redux 的基本 dispatch 过程如下:
-
应用中的组件通过调用 store.dispatch(action) 来分发 action。
-
dispatch 被传递给中间件链。中间件链是在创建 store 时通过 applyMiddleware 函数添加的。
-
中间件对 action 进行处理,可以取消、修改、或者发起其他 action。
-
action 最终到达 reducer,reducer 根据 action 的类型进行相应的 state 更新。
-
state 更新后,通知所有订阅的组件进行重新渲染。
Redux 中间件的原理主要体现在中间件链的设计和执行过程上:
-
中间件链的设计: 中间件被组织成一个链表形式,每个中间件都有机会处理 action。这个链表是通过 applyMiddleware 函数创建的。
-
dispatch 的增强: 中间件提供了一种方式,使得每个 action 发起的时候都可以执行一些额外的逻辑。中间件通过封装 dispatch 函数,并将其传递给链中的下一个中间件。
-
next 函数的传递: 在中间件链中,每个中间件都接收到一个 next 函数,它代表链中的下一个中间件。中间件可以选择调用 next(action) 来将 action 传递给下一个中间件,也可以选择不调用,中止链的执行。
-
异步处理: 由于中间件可以获取 dispatch 函数和 getState 函数,因此它们可以进行异步操作。例如,Redux Thunk 中间件就允许 action 创建函数返回一个函数而不仅仅是一个普通的 action 对象,这个函数可以在异步操作完成后再 dispatch。
整体来说,Redux 中间件的原理是通过改变 dispatch 函数的行为,使其能够在 action 到达 reducer 之前经过一系列的处理。这种机制使得 Redux 的功能能够被灵活地扩展和定制,例如通过中间件实现异步操作、日志记录、时间旅行调试等功能。
5. setState 发生了什么 ,render 函数做了什么
- 接受新的状态: setState可以接受一个新的状态对象或一个函数,该函数返回新的状态。这个状态可以包含要更新的部分或全部组件状态。
- 将新的状态合并到当前状态: React会将新状态与当前状态进行合并。这是因为setState可能是异步的,React可能会在后台批量处理多个setState调用。
- 触发组件更新流程: 一旦新状态被合并,React会触发组件的更新流程。
- 构建虚拟DOM: 当组件被更新时,React首先调用render函数。render函数负责返回一个虚拟DOM(Virtual DOM)树,描述了组件在更新后应该如何呈现。
- 对比虚拟DOM和上一次渲染的虚拟DOM: React使用虚拟DOM来提高性能。在render函数返回新的虚拟DOM后,React会将它与上一次渲染的虚拟DOM进行比较,找出实际发生变化的部分。
- 计算更新的部分: React会计算出需要进行实际DOM更新的部分,以最小化对实际DOM的操作。
- 应用更新: 最后,React将计算出的更新应用到实际DOM上,只更新发生变化的部分,以提高性能。
setState触发了React的更新流程,而render函数则负责描述组件在更新后的状态应该如何呈现,并最终将这个描述应用到实际的DOM上。 React通过使用虚拟DOM和差异算法,尽可能高效地更新实际DOM,以提高性能和用户体验。
6. 虚拟DOM, Fiber
虚拟DOM
虚拟DOM是React的一种概念,它是在内存中维护的一份虚拟的DOM树。当组件的状态发生变化时,React首先在虚拟DOM上进行操作,而不是直接操作实际的DOM。这个虚拟DOM是一个轻量级的 JavaScript 对象树,它映射了真实DOM的结构。
虚拟DOM的工作流程如下:
组件状态更新: 当组件的状态发生变化时,React会创建一个新的虚拟DOM树。
虚拟DOM对比: 新的虚拟DOM树与先前的虚拟DOM树进行比较,找出发生变化的部分。
差异计算: React使用差异算法来计算需要进行更新的最小操作集。
实际DOM更新: 最终,React将计算出的变化应用到实际的DOM上,实现页面的更新。
通过使用虚拟DOM,React可以最小化直接操作实际DOM的次数,从而提高性能。这是因为直接DOM操作通常是昂贵的,而虚拟DOM的比较和计算过程是在内存中进行的,相对来说更快。
Fiber
Fiber是React 16 中引入的一种新的协调引擎,用于更灵活地控制React的渲染过程。在React 16之前,React的渲染过程是递归的,当组件层次很深时,可能导致浏览器阻塞。
Fiber的目标是实现增量式渲染,使React能够在多个帧中分割渲染工作,防止阻塞主线程。Fiber架构引入了一种新的数据结构,可以更灵活地中断、终止或恢复渲染过程,以适应不同的优先级和交互场景。
要点包括:
可中断的渲染: Fiber允许React在执行过程中中断渲染,执行其他高优先级的任务,然后返回并继续渲染。这有助于提高应用的响应性。
优先级调度: Fiber引入了任务调度器,可以根据任务的优先级动态调整渲染的顺序,确保高优先级任务优先完成。
总的来说,虚拟DOM和Fiber是React中两个关键的技术,它们共同为React提供了高效的渲染和更新机制,提高了应用的性能和用户体验。
7. useEffect 与 useLayoutEffect 的区别? 同步异步
useEffect:
- 异步执行: useEffect 中的回调函数是异步执行的。这意味着它不会阻塞浏览器渲染,而是会在浏览器完成渲染后执行。因此,它对页面的布局和绘制不会产生直接影响。
- 执行时机: useEffect 的效果函数会在浏览器完成渲染后才会被调用。
useEffect(() => {
// 异步执行的副作用
console.log('useEffect callback');
}, [/* dependencies */]);
useLayoutEffect
- 同步执行: useLayoutEffect 中的回调函数是同步执行的。它会在浏览器布局完成、但在浏览器绘制之前立即执行。这可能会导致一些阻塞,因此应该谨慎使用,以避免对性能产生不利影响。
- 执行时机: useLayoutEffect 的效果函数会在浏览器完成布局、但在浏览器绘制之前立即被调用。
如何选择
如果你的副作用不需要同步执行,并且不影响布局,那么使用 useEffect
是更安全的选择,因为它不会导致阻塞。
如果你的副作用依赖于 DOM 的布局信息,并且需要在浏览器绘制之前立即执行,那么 useLayoutEffect
可能更适合。
大多数情况下,优先选择使用 useEffect,因为它更不容易导致性能问题。 useLayoutEffect 应该在确实需要在布局阶段同步执行的情况下使用。
8. useCallback 和 useMemo的区别
useCallback
和 useMemo
都是 React 中用于性能优化的 Hook,它们的目标是避免不必要的计算和渲染。尽管它们在使用上有相似之处,但它们的主要区别在于它们的应用场景和返回值的类型。
useCallback 用于缓存回调函数,以避免在每次渲染时创建新的回调。它的语法如下:
const memoizedCallback = useCallback(
() => {
// 回调函数
},
[/* 依赖项数组 */]
);
应用场景: 主要用于防止回调函数在每次渲染时都被重新创建,特别是在传递给子组件时,可以避免不必要的子组件重新渲染。
返回值: 返回一个 memoized(记忆的)版本的回调函数
。
useMemo
用于缓存计算结果,以避免在每次渲染时重新计算。它的语法如下:
const memoizedValue = useMemo(
() => {
// 计算结果
return computeExpensiveValue(a, b);
},
[a, b]
);
应用场景: 主要用于优化计算昂贵的值
,确保只在相关依赖项变化时进行重新计算。
返回值: 返回一个 memoized(记忆的)版本的计算结果
。
区别
返回类型: useCallback 返回一个 memoized 的回调函数,而 useMemo 返回一个 memoized 的计算结果。
主要应用场景: useCallback 主要用于缓存回调函数,而 useMemo 主要用于缓存计算结果。
总的来说,useCallback 用于缓存回调函数,而 useMemo 用于缓存计算结果。选择使用哪个取决于你的具体需求。如果你需要缓存一个回调函数以避免不必要的重新渲染,使用 useCallback。如果你需要缓存计算结果以提高性能,使用 useMemo。在一些情况下,你可能需要同时使用这两个 Hook。
9. Webpack 打包原理
Webpack 是一个现代的 JavaScript 应用程序静态模块打包工具。它的打包原理涉及以下关键概念:
入口(Entry): Webpack 从一个或多个入口点开始,根据入口点的依赖关系,递归地构建一个依赖图。
依赖图(Dependency Graph): Webpack 分析入口文件及其所有直接或间接依赖的文件,形成一个依赖图。每个模块都可以是一个文件,也可以是一个通过加载器(Loaders)处理的文件。
加载器(Loaders): 加载器是用于对模块进行转换的工具,它允许你在 import 模块时预处理文件。例如,可以使用 Babel 加载器将 ES6 代码转换为 ES5。
插件(Plugins): 插件用于执行各种任务,例如优化、压缩、拷贝文件等。插件可以在整个构建过程中执行各种任务,包括对生成的 bundle 进行优化和处理。
输出(Output): Webpack 根据依赖图生成一个或多个输出文件,这些文件包含了打包后的代码。输出文件的生成位置和文件名可以通过配置进行指定。
模块(Modules): 在 Webpack 中,一切皆模块。每个文件都被视为一个模块,并且可以包含 JavaScript、CSS、图片等各种类型的资源。
Webpack 的打包过程可以概括为以下步骤:
解析(Resolving): Webpack 从入口开始解析模块的依赖关系,找到每个模块的位置。
加载(Loading): 解析完成后,Webpack 将加载模块,这可能涉及到对文件的读取、转换和编译。
转换(Transformation): 加载器对模块进行转换,例如将 ES6 语法转换为 ES5、将 Sass 转换为 CSS 等。
打包(Bundling): 经过加载和转换后的模块被放置到一个或多个 bundle 中,这是由输出配置指定的。
输出(Output): Webpack 将 bundle 写入文件系统,生成最终的打包结果。
整个过程中,Webpack 通过使用各种配置选项、加载器和插件来灵活地处理不同类型的文件和任务,实现高度可定制的打包过程。Webpack 的灵活性和强大性使得它成为现代前端开发中的主流打包工具。