乐闻世界logo
搜索文章和话题

React面试题手册

React Fiber 架构是什么?有什么优势?

React Fiber 是 React 框架的一个核心算法重写版本,它是 React 16 版本中引入的。Fiber 架构的主要目标是增强 React 在处理动画、布局、手势等方面的能力,并且让这些任务的执行变得更加平滑,不会引起应用程序的卡顿。这种架构的引入是为了优化渲染过程,使之能够利用浏览器的空闲时间执行,从而提高应用程序的性能并使用户界面更加流畅。React Fiber 架构的主要优势有:增量渲染:Fiber 架构的主要功能之一是能够将渲染工作拆分成多个小任务,并将这些任务分散到多个帧中。这个特点允许 React 暂停和恢复渲染任务,这种“可中断”的渲染过程意味着主线程可以更响应用户操作,从而提高了应用的性能。任务优先级:Fiber 架构可以为更新分配优先级。一些任务(如动画)比其他任务(如数据的后台同步)更为紧急。React Fiber 可以区分这些任务,并且先执行更高优先级的任务,再在空闲时处理低优先级的任务。更好的错误处理:Fiber 引入了新的错误边界概念,使得组件能够更好地捕获子组件的错误,并且定义备用 UI,从而提供更好的用户体验。更平滑的动画和过渡:由于 React Fiber 可以利用浏览器的空闲时间执行渲染任务,因此可以更平滑地执行动画和过渡,降低了卡顿的可能性。更好的适配未来的变化:Fiber 架构为将来 React 框架的可能更新和改变打下了基础,比如并发模式(Concurrent Mode)和 Suspense 等新特性。示例:优先级调度:想象一下一个用 React Fiber 构建的聊天应用。用户正在输入消息,同时应用正在后台同步接收新消息。使用 Fiber 架构,React 可以给用户输入的响应分配更高的优先级,从而保证输入的流畅,而消息同步的任务可以在浏览器空闲时进行,用户体验因此得到提升。通过这些改进,React Fiber 架构使得开发者可以构建出更加响应快速、用户体验更好的应用程序。
阅读 26·2024年6月24日 16:43

React 项目中如何统一监听组件报错,并且处理报错情况?

在React项目中,统一监听组件报错并处理错误可以通过几种方式来实现,其中最常见和有效的方式是使用错误边界(Error Boundaries)。错误边界(Error Boundaries)错误边界是React 16引入的一种新的概念,它允许我们捕获后代组件树中JavaScript错误,并记录这些错误,并显示一个备用UI,而不是使整个组件树崩溃。实现方式创建一个错误边界组件:我们可以创建一个类组件,并在其中定义static getDerivedStateFromError()和componentDidCatch()这两个生命周期方法。class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 更新状态使下一次渲染能够显示降级后的UI return { hasError: true }; } componentDidCatch(error, errorInfo) { // 你同样可以将错误记录到一个错误报告服务 logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // 你可以自定义降级后的UI并渲染 return <h1>Something went wrong.</h1>; } return this.props.children; }}在应用中使用错误边界组件:错误边界组件可以包裹在任何你希望捕获其内部错误的组件外部。如果一个class组件内部发生了错误,但是它并没有被任何错误边界包裹,那么整个React组件树将会卸载。<ErrorBoundary> <MyComponent /></ErrorBoundary>错误报告服务在componentDidCatch方法中,我们不仅可以将错误渲染为备用UI,还可以将错误信息发送到服务器或错误监控服务,比如Sentry、LogRocket或者自己的错误收集系统中。实现方式componentDidCatch(error, errorInfo) { // Example: Sentry integration Sentry.captureException(error, { extra: errorInfo });}总结通过使用错误边界组件(Error Boundaries),我们可以在React应用中统一监听组件报错。每当组件树中的某部分发生JavaScript错误时,错误边界将会捕获这些错误,并能够显示备用UI来避免整个应用崩溃。同时,利用componentDidCatch可以处理错误,例如记录到日志服务中。这种方式不仅能够提高用户体验,还能帮助开发者及时发现并解决问题。不过需要注意,错误边界无法捕获以下场景中的错误:事件处理(了解更多请使用try/catch)异步代码(例如setTimeout或requestAnimationFrame回调函数)服务器端渲染它自身(错误边界组件自己的错误)抛出的错误在实践中,我们通常建议在高层组件(如路由层级)使用错误边界,以便能够捕获更多未预期的错误情况。
阅读 55·2024年6月24日 16:43

React 中 setState 是如何工作的?

setState 是 React 类组件中的一个方法,用于更新组件的状态,并触发组件的重新渲染。每当状态改变时,React 会重新执行 render 方法来确定是否需要更新 DOM。当你调用 setState 方法时,你实际上是在对 React 发起一个“请求”来更新组件状态。然而,这个更新并不是立即执行的。React 会将 setState 调用放入一个队列中,稍后异步地批量处理这些更新。这种更新策略有助于优化性能,因为它避免了不必要的重复渲染和DOM操作。下面是一个 setState 的工作流程的简单描述:调用 setState: 当组件的状态需要更新时,你会调用 setState,传入一个新的状态对象或者一个函数。如果传入的是函数,该函数会接收前一个状态作为参数,并返回一个新状态。合并状态: React 会将你传入的状态对象合并到当前状态中。这个合并是浅合并,意味着只合并第一层的属性,更深层次的对象则会被整个替换。组件重新渲染: 一旦状态被更新,React 会将新的状态和当前的属性作为输入,计算出新的组件树。虚拟 DOM 比较: React 使用虚拟 DOM 来优化性能,它会比较旧的组件树和新的组件树,来确定实际DOM需要哪些更新。更新 DOM: 最后,React 会根据需要更新的部分来更新实际的 DOM,这使得渲染过程更加高效。让我们来看一个具体的例子:class MyComponent extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } handleClick = () => { this.setState(prevState => { return { count: prevState.count + 1 }; }); } render() { return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={this.handleClick}> Click me </button> </div> ); }}在这个例子中,每当按钮被点击时,handleClick 方法会被调用,它又调用了 setState 来更新 count 状态。对 setState 的调用会导致 MyComponent 重新渲染,渲染方法中的 {this.state.count} 会显示新的计数值。
阅读 26·2024年6月24日 16:43

React setState 执行过程是同步的还是异步的?

React 的 setState 方法通常被视为异步的,这是因为 React 可以批量延迟更新来优化性能。当你调用 setState 时,React 会将传递的对象或函数排入更新队列,而不是立即更新组件状态。之后,React 将在其生命周期方法中以批处理的方式来决定何时实际更新状态和重新渲染组件。这种行为通常在事件处理、生命周期方法或任何由 React 控制的异步代码中表现得最为明显。例如:handleClick = () => { this.setState({ count: this.state.count + 1 }); console.log(this.state.count); // 这里可能不会立即反映更新后的状态 // ...其他逻辑}在上述代码中,console.log 执行时可能会打印出更新前的状态值,因为 setState 的调用并没有立即更新 this.state。然而,当 setState 被用在某些异步的上下文中,比如 setTimeout 或者原生事件处理时,它的表现就可能是“同步”的,因为 React 的批处理机制没有在这些场景中介入:setTimeout(() => { this.setState({ count: this.state.count + 1 }); console.log(this.state.count); // 这里将会立即反映更新后的状态}, 0);这里因为 setTimeout 跳出了 React 的控制,所以更新不再是批处理的,setState 将会同步地更新状态并重新渲染组件。为了以一种可预测的方式处理 setState 可能的异步行为,最佳实践是使用它的回调函数:this.setState((prevState) => { return { count: prevState.count + 1 };}, () => { console.log(this.state.count); // 这里将会在状态更新后执行});使用回调函数作为 setState 的第二个参数,可以确保在状态更新和组件重渲染之后执行特定的逻辑。这也解释了为什么在很多场合我们需要考虑到 setState 的异步特性。
阅读 45·2024年6月24日 16:43

React 中 JSX 是什么?

JSX是React框架中使用的一种语法扩展,它允许我们在JavaScript代码中编写看起来像HTML的结构。JSX提供了一种更为直观和声明式的方式来创建React元素树,并且让代码的结构清晰易懂。这种语法对于开发者来说非常直观,因为它使得编写UI组件时可以像编写HTML标签一样自然。以下是一个简单的JSX示例:const myElement = <h1>Hello, world!</h1>;这行代码定义了一个React元素,它将会被渲染为一个<h1>标签,包含文本"Hello, world!"。使用JSX的好处包括:可读性: JSX由于类似于HTML结构,使得组件的结构更加直观和易于理解。表现力: 它能够很好地表达UI组件的层次结构和属性。工具支持: 现代前端工具链(如Babel)支持JSX,并且可以将其编译为浏览器可以理解的JavaScript代码。值得注意的是,JSX并不是必须的;React也可以不使用JSX来创建元素。但是,大多数React开发者都倾向于使用JSX,因为它提供了一种更加方便快捷的开发方式。下面是一个不使用JSX的React元素创建示例,与上面的JSX示例作用相同:const myElement = React.createElement('h1', null, 'Hello, world!');可以看到,不使用JSX时,代码会更加冗长和不易读,这也是为什么JSX在React开发中变得如此流行。
阅读 25·2024年6月24日 16:43

React 项目中有哪些内存泄露的场景?

在React项目中,内存泄露主要是指应用程序持续占用不再需要使用的内存。这会导致应用性能下降,甚至崩溃。以下是一些典型的内存泄露场景:1. 组件卸载后的挂载状态更新例子:class MyComponent extends React.Component { state = { data: null }; componentDidMount() { someAsyncCall().then(data => { if (!this.isUnmounted) { this.setState({ data }); } }); } componentWillUnmount() { this.isUnmounted = true; }}在上面的例子中,someAsyncCall是一个异步操作,如果在异步操作完成前组件被卸载,而异步操作的回调仍试图执行this.setState,这可能会导致内存泄露。通过设置一个标志isUnmounted并在componentWillUnmount中标记组件已卸载,可以避免这种情况。2. 没有清理的定时器和数据订阅例子:class MyComponent extends React.Component { componentDidMount() { this.intervalId = setInterval(this.fetchData, 1000); } componentWillUnmount() { clearInterval(this.intervalId); } fetchData = () => { // Fetch data from API }}如果在组件卸载后,没有清理定时器,定时器依然会运行,这会阻止JavaScript引擎回收相关内存,导致内存泄露。在componentWillUnmount中清除定时器可以解决这个问题。3. 外部库的事件监听器未正确移除例子:class MyComponent extends React.Component { componentDidMount() { ExternalLibrary.on('data', this.handleData); } componentWillUnmount() { ExternalLibrary.off('data', this.handleData); } handleData = (data) => { // Handle the event }}如果忘记移除事件监听器,那么即使组件卸载了,事件监听器仍然存在,导致内存泄露。应该在componentWillUnmount中移除监听器。4. 闭包引用导致的内存泄露例子:function setup() { let largeData = new Array(1000).fill(new Array(1000).fill('data')); document.getElementById('button').addEventListener('click', function handleClick() { console.log('Button clicked'); });}在上面的例子中,即使按钮点击事件的处理函数handleClick并不需要使用largeData,但由于闭包的作用,largeData会被保留在内存中,除非handleClick被移除或者largeData变量被显式设置为null。5. 长时间运行的WebSocket连接如果组件与一个WebSocket服务建立了连接,并且在组件卸载时没有关闭连接,那么这个连接可能会持续存在并保持对组件的引用,从而导致内存泄露。防止内存泄露的一般建议:当组件卸载时,确保取消所有订阅和异步操作。使用useEffect的清理函数来处理功能性组件的副作用和相关的清理操作。对于外部事件监听器和定时器,确保在组件卸载时移除或清理它们。考虑使用WeakMap或WeakSet来缓存大对象,它们不会阻止垃圾回收。避免在组件状态中保存大量的数据,特别是如果这些数据在组件的生命周期内不再需要。使用工具(如Chrome开发者工具的Memory选项卡)定期对应用的内存使用进行分析和调试。
阅读 37·2024年6月24日 16:43

React 中 useRef 的使用方式和使用场景有哪些?

在React中,useRef是一个非常有用的Hook,它主要被用来访问DOM元素和保存跨渲染周期持久的可变数据。useRef返回一个可变的ref对象,其 .current属性被初始化为传递给 useRef的参数(initialValue)。返回的对象在组件的整个生命周期内保持不变。 使用方式首先,我们需要在组件内部导入 useRef:import React, { useRef } from 'react';然后可以使用 useRef来创建一个ref对象:const myRef = useRef(initialValue);其中 initialValue是ref对象 .current属性的初始值。使用 myRef.current可以访问或者修改这个值。使用场景有几种常见的场景适合使用 useRef:1. 访问DOM元素当你需要直接操作DOM节点时,比如获取输入框的值或者设置焦点,可以使用 useRef。function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { // 直接访问DOM元素来设置焦点 inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Set focus</button> </> );}2. 保存任意可变值另一个使用场景是当你想要保存一个在组件的整个生命周期中都保持不变的值,这个值不会触发组件的重新渲染。function Timer() { const intervalRef = useRef(); useEffect(() => { const id = setInterval(() => { // 执行一些操作 }, 1000); intervalRef.current = id; return () => { clearInterval(intervalRef.current); }; }, []); // ...}在这个例子中,intervalRef被用来保存一个间隔定时器的ID,虽然这个ID会在不同的渲染周期间发生变化,但我们不希望这种变化触发组件的重新渲染。3. 跟踪上一个值useRef也可以被用来跟踪组件中的变量或状态的旧值。function Counter() { const [count, setCount] = useState(0); const prevCountRef = useRef(); useEffect(() => { prevCountRef.current = count; }); const prevCount = prevCountRef.current; // 在渲染时获取旧的值 // ...}这里 prevCountRef用于存储每次渲染后的 count值,可以在任何时候访问前一个状态的值,而不会触发组件的重新渲染。
阅读 53·2024年6月24日 16:43

React 中 useContext 的使用方式和使用场景有哪些?

React 中的 useContext 钩子是一个用于让组件能够访问 React 上下文(Context)的工具。这个上下文设计用于共享那些对于一个组件树而言是“全局”的数据,如当前认证的用户、主题或首选语言等。 使用方式:首先,你需要创建一个 Context 对象。这可以通过 React.createContext() 完成,并且通常会在组件树的较高层级上完成。import React from 'react';// 创建 Context 对象const MyContext = React.createContext(defaultValue);一旦你有了一个 Context 对象,就可以使用 Context.Provider 组件包裹你的组件树的一部分,以在其下所有的组件中提供上下文数据。<MyContext.Provider value={/* 某些值 */}> {/* 组件树 */}</MyContext.Provider>然后,在组件树中的任何层级上,你都可以使用 useContext 钩子来访问该上下文。import React, { useContext } from 'react';function MyComponent() { // 使用 useContext 钩子获取上下文值 const contextValue = useContext(MyContext); return <div>{/* 使用 contextValue 做些什么 */}</div>;}通过使用 useContext 钩子,你不需要通过组件的 props 手动传递数据,可以直接访问上层组件通过 Context.Provider 提供的数据。使用场景:主题切换:当你想要在应用程序中切换主题时,你可以使用上下文来保持当前主题的状态,并在整个应用程序中轻松访问它。用户认证:在需要知道当前用户是否已经登录的多个组件中,你可以使用上下文来共享用户的登录状态和用户信息。国际化:你可以使用上下文来存储当前的语言设置,并在组件树中的任何地方访问它,以便于国际化。状态管理:在某些简单的情况下,你可以使用上下文来代替其他状态管理库(如 Redux),来存储和管理全局状态。示例:假设我们有一个需要在多个组件之间共享的用户认证状态,可以这样使用 useContext:// AuthContext.jsimport React, { createContext, useState } from 'react';// 创建上下文对象,初始值为 nullexport const AuthContext = createContext(null);// 创建一个提供者组件export const AuthProvider = ({ children }) => { const [authUser, setAuthUser] = useState(null); // 登录逻辑 const signIn = (username, password) => { // 假设验证逻辑在这里 const user = { name: 'Mock User', username }; setAuthUser(user); }; // 登出逻辑 const signOut = () => { setAuthUser(null); }; return ( <AuthContext.Provider value={{ authUser, signIn, signOut }}> {children} </AuthContext.Provider> );};// App.jsimport React from 'react';import { AuthProvider } from './AuthContext';import MyComponent from './MyComponent';function App() { return ( <AuthProvider> <MyComponent /> </AuthProvider> );}// MyComponent.jsimport React, { useContext } from 'react';import { AuthContext } from './AuthContext';function MyComponent() { const { authUser, signIn, signOut } = useContext(AuthContext); return ( <div> {authUser ? ( <div> <p>Welcome, {authUser.name}!</p> <button onClick={signOut}>Sign out</button> </div> ) : ( <button onClick={() => signIn('user', 'password')}> Sign in </button> )} </div> );}
阅读 186·2024年6月24日 16:43

为什么 React 元素有一个 $$typeof 属性

React 元素有一个 $$typeof 属性的原因主要是出于安全考虑,它是用来防止一种名为 XSSI(跨站脚本包含)的攻击。$$typeof 属性可以帮助确保在 React 应用中引入的数据是有效的 React 元素,而非恶意对象。在过去,攻击者可能会尝试利用 JSONP 等技术将恶意脚本插入到网站中。JSONP 是一种通过 <script> 标签载入跨域的 JSON 数据的方法。在 JSONP 中,由于 <script> 标签会执行载入的内容,如果返回的数据不是纯粹的 JSON 而是可执行的 JavaScript 代码,那么它就可能被用于执行恶意脚本。为了防止这种攻击,React 开发团队引入了 $$typeof 属性。每个通过 React.createElement 创建的元素都会自动添加这个属性。这个属性的值是一个特殊的符号(在支持 Symbol 的 JavaScript 环境中)或者一个特定的数字(在不支持 Symbol 的环境中)。这样,在 React 对元素进行处理之前,它可以检查元素是否具有正确的 $$typeof 值,确保该元素是由 React 创建的,从而防止可能的 XSSI 攻击。例如,如果我们通过网络请求获取了一个对象,并且直接将其作为子元素传递给 React 来渲染,React 会检查这个对象是否具有正确的 $$typeof 属性。如果没有,React 将不会将其作为 React 元素进行处理,这样就能防止恶意对象被当作 React 元素来渲染,从而避免潜在的安全风险。总之,$$typeof 属性是 React 为了增强应用安全而引入的一个机制,用于验证被处理的元素确实是通过 React 的正确方式创建的,从而避免了某些类型的安全漏洞。
阅读 52·2024年6月24日 16:43

React 的调和阶段, setState内部做了哪些动作?

在React中,setState 函数用于更新组件的状态,并触发重新渲染流程。调和(Reconciliation)阶段是React用来对比新旧虚拟DOM树差异,并决定如何高效更新真实DOM的过程。当你调用 setState 时,内部会触发以下动作:排队状态更新(Enqueuing State Update):setState 调用并不会立即更新组件的状态,而是将状态更新排队。这意味着React可能会累积多个 setState 调用,然后批量更新状态以优化性能。标记组件需要更新(Marking Component for Update):一旦状态被置入队列,React会将当前组件标记为“脏”(dirty),意味着组件的状态与显示的输出不同步,需要进行更新。批处理和合并状态(Batching and Merging State):React会将所有排队的 setState 调用进行批处理。如果有多个状态更新,React会将它们合并以减少不必要的渲染和调和操作。调用生命周期方法(Lifecycle Methods Invoking):在实际更新之前,React会调用 componentWillUpdate(在旧版本的React中)或 getDerivedStateFromProps 和 shouldComponentUpdate(在新版本中),这些生命周期方法允许开发者在渲染发生前执行额外的操作。创建新的虚拟DOM树(Virtual DOM Tree Creation):有了新的状态,React会创建新的虚拟DOM树,这个树反映了状态更新后的组件结构。对比新旧虚拟DOM(Diffing Virtual DOM Trees):接下来,React会使用调和算法对比新旧虚拟DOM树,确定哪些部分需要更新。这个过程产生了所谓的“差异”(diffs)。生成更新操作(Generating Update Operations):根据差异,React会生成一系列更新操作,这些操作将被应用到真实的DOM上以实现UI的最终变化。执行更新操作(Executing Update Operations):React会按照效率最高的方式批量执行这些更新操作,这可能包括添加、移动、更新或删除DOM节点。调用生命周期方法(Lifecycle Methods Invoking):在更新操作完成之后,React会调用 componentDidUpdate 生命周期方法,使开发者有机会执行需要DOM更新后才能进行的操作。例如,假设我们有一个计数器组件,其中包含一个按钮,当点击按钮时,它会通过 setState 增加计数值。React将按照上述步骤进行操作,确保界面反映了最新的计数状态,并以最高效的方式更新DOM。请注意,从React 16开始,引入了Fiber架构,它改变了内部工作原理,特别是更新过程可以被中断和恢复,以便更好地管理UI渲染的性能。但是,以上所述的基本步骤仍然适用。
阅读 30·2024年6月24日 16:43