React面试题
1.对虚拟dom理解
在React.js中,虚拟DOM(Virtual DOM)是一种优化策略,用于解决传统Web应用直接操作DOM带来的性能问题。以下是关于React虚拟DOM的核心理解点:
-
虚拟DOM的概念:
-
虚拟DOM是实际DOM(Document Object Model)在内存中的抽象表现,用JavaScript对象模拟DOM节点及其属性。当React组件渲染时,它并不会立即操作浏览器的真实DOM,而是创建对应的虚拟DOM树。
-
-
工作原理:
-
当React组件的状态或属性发生变化时,React会重新运行
render()
方法生成新的虚拟DOM树。 -
新旧两棵虚拟DOM树之间会通过高效的Diff算法进行比较(也称为reconciliation过程)。
-
Diff算法找出最小差异集,即哪些子节点需要更新、插入、删除等操作。
-
只有那些经过Diff算法识别出的需要更新的部分,React才会去操作真实DOM,进行必要的增删改查操作。
-
-
优势:
-
性能优化:避免频繁操作真实DOM,每次变更都全量更新DOM的成本极高,而虚拟DOM通过Diff算法能有效减少对真实DOM的操作次数,显著提升性能。
-
跨平台性:虚拟DOM本身是一种抽象概念,这使得React不仅适用于Web环境,还能应用于其他非DOM环境,例如React Native就是利用虚拟DOM思想构建原生移动应用UI。
-
-
简化开发:
-
开发者只需要关心组件的状态变化,React负责根据状态变化后的虚拟DOM去更新视图,实现了声明式编程模式,极大地简化了UI开发和维护工作。
-
综上所述,React虚拟DOM是React框架下高效管理UI更新的重要技术手段,它通过在内存中构建和操作抽象的DOM模型,有效地提升了大规模Web应用的性能表现。
2.谈谈你对React的理解
React 是一个由 Facebook 开发并开源的 JavaScript 库,主要用于构建用户界面(UI)。它是目前Web前端开发领域最流行的库之一,特别是在构建单页应用(SPA)、移动应用和桌面应用中占有重要地位。React以其独特的设计理念和强大的功能著称:
组件化开发:React的核心思想是组件化编程,它鼓励开发者将UI切分成一系列可复用的独立部件——组件(Components)。组件可以嵌套,每个组件负责自己的渲染和状态管理,这有助于复杂界面的模块化和维护。
虚拟DOM:React使用虚拟DOM(Virtual DOM)技术,这是一种内存中的轻量级JavaScript对象树,用来描述真实的DOM结构。每当组件的状态改变时,React会重新计算虚拟DOM树,通过高效的Diff算法找出实际DOM中需要更新的部分,然后仅针对这些变化进行实际DOM操作,极大提高了网页的更新效率。
单向数据流与状态管理:React推崇单向数据绑定和父子组件之间通过props向下传递数据的方式。对于复杂的组件间通信和状态管理,React配合Redux、Context API或React Hooks(如useState、useReducer、useContext等)提供了解决方案。
JSX:React引入了JSX语法扩展,它允许在JavaScript中书写类似HTML的语法来定义组件结构。JSX让开发者可以直接在JavaScript代码中编写UI模板,提高了代码的可读性和直观性。
响应式编程:React通过对组件内state或props的变化实时响应,能够保证界面状态始终与数据同步。
可维护性与测试性:React组件易于理解和测试,因为它们是纯粹的函数输入输出,而且React提供了丰富的生命周期方法,帮助开发者在组件的不同阶段进行初始化、更新和清理工作。
生态丰富:围绕React建立了一个庞大的生态系统,包括但不限于Router(路由库)、State Management Libraries(如Redux、MobX)、Testing Tools(如Jest、Enzyme)、UI Component Libraries(如Material-UI、Ant Design)等。
总之,React不仅仅是一个视图层库,它提供了一种现代化的、声明式的编程方式来构建交互性强、性能优越的动态用户界面,并且在大型项目的开发和维护过程中展现出了极高的价值。随着React Hooks的推出以及React Native对原生移动应用的支持,React的应用范围得到了进一步拓展。
3.如何避免React生命周期中的坑
-
避免生命周期中的坑需要做好两件事:不在恰当的时候调用了不该调用的代码;在需要调用时,不要忘了调用。
-
那么主要有这么 7 种情况容易造成生命周期的坑
-
getDerivedStateFromProps
容易编写反模式代码,使受控组件与非受控组件区分模糊 -
componentWillMount
在 React 中已被标记弃用,不推荐使用,主要原因是新的异步渲染架构会导致它被多次调用
。所以网络请求及事件绑定代码应移至componentDidMount
中。 -
componentWillReceiveProps
同样被标记弃用,被getDerivedStateFromProps
所取代,主要原因是性能问题 -
shouldComponentUpdate
通过返回true
或者false
来确定是否需要触发新的渲染。主要用于性能优化 -
componentWillUpdate
同样是由于新的异步渲染机制,而被标记废弃,不推荐使用,原先的逻辑可结合getSnapshotBeforeUpdate
与componentDidUpdate
改造使用。 -
如果在
componentWillUnmount
函数中忘记解除事件绑定,取消定时器等清理操作,容易引发 bug -
如果没有添加错误边界处理,当渲染发生异常时,用户将会看到一个无法操作的白屏,所以一定要添加
-
4.React 的请求应该放在哪里,为什么
对于异步请求,应该放在 componentDidMount
中去操作。从时间顺序来看,除了 componentDidMount
还可以有以下选择:
-
constructor:可以放,但从设计上而言不推荐。constructor 主要用于初始化 state 与函数绑定,并不承载业务逻辑。而且随着类属性的流行,constructor 已经很少使用了
-
componentWillMount:已被标记废弃,在新的异步渲染架构下会触发多次渲染,容易引发 Bug,不利于未来 React 升级后的代码维护。
-
所以React 的请求放在
componentDidMount 里是最好的选择
。
5.React Fiber架构
React Fiber 架构是React团队对React核心算法的重大革新,旨在改善React应用程序的性能并提供更好的用户体验。以下是对React Fiber架构关键理解和特性概述:
-
背景与问题:
-
在React 15及之前的版本中,渲染过程是一个递归且不可中断的过程,对于大型组件树或者高频率的更新场景,可能导致主线程长时间阻塞,进而影响应用的响应性和流畅度。
-
-
Fiber核心概念:
-
Fiber节点:Fiber架构引入了一种新的内部数据结构——Fiber节点,每个React组件都会有一个对应的Fiber节点,它包含了组件的类型、props、状态等信息,形成了一个新的“Fiber树”结构,取代了原有的单一递归调用链。
-
可中断与恢复渲染:Fiber架构允许渲染过程被打断并在之后恢复,这是通过将渲染任务拆分成一系列小的工作单元(microtasks),每个工作单元都可以被单独调度和优先级排序,这样就可以在渲染过程中灵活地切换上下文,比如响应高优先级的浏览器事件。
-
-
增量渲染与优先级调度:
-
Fiber架构支持增量渲染,这意味着React不再一次性计算整个组件树的差异,而是分阶段逐步进行,可以根据优先级决定何时暂停渲染并处理其他任务,比如高优先级的动画帧或者用户交互。
-
-
任务调度器:
-
React Fiber引入了一个新的任务调度器,可以根据组件的不同特性(如是否可见、是否有动画、是否悬停等)分配优先级,确保重要的更新能够及时得到处理。
-
-
性能优化:
-
通过Fiber架构,React能够更好地管理复杂的渲染逻辑,降低不必要的DOM操作,从而提升性能,特别是在大规模的应用场景下,有助于减小UI更新带来的卡顿现象,增强应用的实时性和流畅性。
-
-
未来扩展性:
-
Fiber架构也为React后续功能的拓展提供了基础,如异步渲染、并发模式等功能,使得React能够适应更多复杂和高性能的需求。
-
总之,React Fiber架构通过对React核心算法的重构,从根本上改变了React处理组件更新和渲染的方式,提高了其在复杂场景下的性能和灵活性,从而带来了更好的用户体验。
6.react中有哪些优化?
在React应用中进行性能优化可以从多个角度入手,以下是一些常见的React性能优化手段:
-
减少渲染
-
shouldComponentUpdate
: 实现shouldComponentUpdate
生命周期方法,根据props
和state
的变化来决定组件是否需要重新渲染。若新的props
或state
与现有值相等,则返回false
以阻止渲染。 -
React.PureComponent
/React.memo
: 使用React.PureComponent
替代React.Component
,它会浅比较props
和state
的变化。而对于函数组件,可以使用React.memo
来进行优化,它会对组件进行 memoization,只有当props
改变时才重新渲染。
-
-
懒加载和代码分割
-
懒加载(Dynamic Import): 使用
React.lazy
和Suspense
组件实现组件级别的懒加载,降低首屏加载时间,按需加载组件。 -
Webpack代码分割(Code Splitting): 结合构建工具如Webpack,将应用拆分为多个bundle,延迟加载未到达视窗的组件所依赖的代码。
-
-
状态管理优化
-
精简
setState
调用:合并多个setState
调用,避免短时间内频繁更新状态引发过多渲染。 -
选择合适的状态容器:使用Redux、MobX等状态管理库可以更有效地管理全局状态,减少不必要的组件层级更新。
-
-
渲染优化
-
使用
React.Fragment
:避免多余的DOM节点,用<>...</>
或者<React.Fragment>
包裹多个子元素以减少DOM开销。 -
避免不必要的计算和变量创建:在
render
方法中,尽量减少每次渲染时创建新对象或执行耗时计算的操作。
-
-
事件处理
-
避免在
render
方法中直接创建匿名函数:事件处理函数应作为实例方法或使用箭头函数绑定到实例上,防止每次渲染都产生新的函数实例。
-
-
React Fiber架构利用
-
React 16 引入的Fiber架构允许任务中断和恢复,可以更好地进行优先级调度,尤其对于高优先级的交互和动画有更好的支持。
-
-
优化上下文(Context)使用
-
对于大型应用,谨慎使用Context,因为它可能导致大量组件重新渲染。可使用
useContextSelector
(如果有的话)来挑选需要的Context值,避免不必要的重渲染。
-
-
服务端渲染
-
首次加载时进行服务器端渲染(SSR)可以提高搜索引擎优化(SEO)和页面加载速度,提供更好的初始用户体验。
-
-
组件优化
-
组件拆分:合理划分组件层次结构,使组件尽可能地小型化和可复用。
-
使用Hooks:React Hooks如
useMemo
和useCallback
可以帮助避免在每次渲染时都重新创建昂贵的计算结果或函数引用。
-
-
CSS优化
-
使用CSS Modules、styled-components等工具,减少样式冲突和全局CSS的影响。
-
通过上述多种方法的组合运用,可以有效地优化React应用程序的性能和用户体验。同时,持续监控和分析应用性能也是至关重要的,以便找到针对性的优化点。
7.浏览器一帧都会干些什么以及requestIdleCallback的启示
浏览器一帧会经过下面这几个过程:
-
接受输入事件
-
执行事件回调
-
开始一帧
-
执行 RAF (RequestAnimationFrame)
-
页面布局,样式计算
-
绘制渲染
-
执行 RIC (RequestIdelCallback)
requestIdleCallback 的启示
:我们以浏览器是否有剩余时间作微任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。
8.createElement过程
在React中,React.createElement
函数用于创建虚拟DOM元素。这个过程是React的核心部分,它负责将JavaScript对象形式的描述转化为可在内存中进行高效比较和操作的虚拟节点(Virtual Node,简称为VNode)。以下是React.createElement
的基本使用方式及其背后的原理:
// 示例用法 const element = React.createElement( 'div', // 类型,可以是HTML标签名或React组件类/函数 {id: 'myDiv', className: 'container'}, // 属性对象 'Hello, World!', // 子元素,可以是字符串、React元素或其他可迭代的子元素集合 ); // 等同于JSX写法 const elementInJSX = <div id="myDiv" className="container">Hello, World!</div>;
React.createElement
函数的大致处理流程如下:
-
解析参数:
-
第一个参数是类型(type),它可以是原生HTML标签名(如
'div'
)、React组件类或函数。 -
第二个参数是属性对象(props),它是传递给React元素的所有属性和回调函数。
-
第三个及以后的参数是子元素列表,可以是字符串、数字、其他React元素,或者是这些元素组成的数组。
-
-
创建虚拟节点:
-
函数根据传入的参数创建一个虚拟DOM对象(即VNode),VNode包含类型、属性和子元素信息,并不直接对应真实的DOM节点,而是在内存中表示DOM结构的一种抽象数据结构。
-
-
构建虚拟DOM树:
-
调用
React.createElement
会递归地构建出整个组件树的虚拟表示,形成一颗完整的虚拟DOM树。
-
-
更新与渲染:
-
当组件状态或属性变化时,React重新执行
createElement
创建新的虚拟DOM树,并通过高效的差异算法(diffing algorithm)对比新旧两棵树,找出最小化的变更集。 -
随后,React根据这个变更集,对实际的DOM进行必要的更新,保证视图与状态同步。
-
这种虚拟化的过程大大提高了React应用在大量DOM操作中的性能表现,因为它减少了对真实DOM的直接操作,仅在需要的时候更新实际DOM。同时,React.createElement
也是React配合JSX语法工作的底层机制,JSX会被编译成调用React.createElement
的形式。
9.调和阶段 setState内部干了什么
在React中,调和阶段(Reconciliation Phase)是React更新UI流程中的一个重要环节,当组件调用setState
方法时,内部发生的操作主要包括以下几个步骤:
-
状态合并:
-
当调用
setState
时,React首先会将传入的对象与当前组件的状态合并。如果是函数形式的setState
,则会结合当前状态计算出新的状态。
-
-
调度更新:
-
React不会立即更新DOM,而是将状态变更放入一个队列中,并安排在未来的某个时间点执行更新。这个过程是异步的,除非在合成事件处理器或者生命周期方法中明确指定了
setState
应该同步执行。
-
-
调和过程开始:
-
React进入调和阶段,该阶段的主要目的是确定如何根据新的状态更改来更新用户界面。
-
-
创建新的虚拟DOM树:
-
React会基于合并后的最新状态重新运行
render
方法,生成新的虚拟DOM树。
-
-
差异比较(Virtual DOM Diffing):
-
React会将新生成的虚拟DOM树与之前保存的旧树进行差异比较(也称为
reconciliation
或diff
算法)。 -
这个算法能够识别出哪些DOM节点需要被添加、删除或更新,从而最小化对实际DOM的操作。
-
-
更新DOM:
-
根据差异比较的结果,React仅对实际DOM进行必要的更改,这一过程被称为“提交更新”(commit phase)。
-
-
生命周期方法触发:
-
在此过程中,相关的生命周期方法如
shouldComponentUpdate
、getDerivedStateFromProps
(已弃用)、componentWillUpdate
(已弃用但在某些类组件中仍可用)、getSnapshotBeforeUpdate
、componentDidUpdate
等会被相应地调用。
-
总之,在调和阶段中,React通过状态合并、调度更新、构建新虚拟DOM、执行差异算法等一系列操作,确保了UI能够在状态改变后高效、准确地反映出最新的状态变化。
10.setState
在React中,setState()
是一个用于更新组件状态的关键方法,它存在于React类组件中。当你调用 setState()
方法时,实际上是告诉React组件的状态(state)发生了一次变更,并且React会基于新的状态重新计算虚拟DOM树,然后通过对比前后虚拟DOM差异来决定是否以及如何更新实际的DOM树,从而引起界面的重新渲染。
以下是对 setState()
函数的一些关键理解点:
-
异步性质:
-
setState()
在React中并不总是立即更新组件状态。React可能会批量处理多个setState()
调用,以此提升性能。这意味着你不能依赖setState()
后紧跟的代码来立即看到状态更新的结果。
-
-
合并状态:
-
当传递给
setState()
的是一个对象时,React会将这个对象浅合并到当前状态中。也就是说,新状态会与现有状态的部分内容结合在一起,而不是完全替换当前状态。
-
-
回调函数:
-
setState()
接收可选的第二个参数,这是一个回调函数。这个回调将在组件完成重新渲染并且新的状态已经合并进组件实例后被执行。在这个回调函数里,你可以安全地访问到最新的状态值。
-
-
不保证同步:
-
即使在回调函数中访问状态,也不能假设状态更新一定是同步的,尤其是在异步渲染模式下。若需依赖更新后的状态进行后续操作,推荐使用
componentDidUpdate
生命周期方法或React Hooks(如useEffect
)。
-
-
队列化处理:
-
React会把连续的
setState()
调用放入一个队列中,然后一次性的去处理这个状态更新队列,避免过于频繁的渲染。
-
-
函数形式:
-
除了接受对象作为参数,
setState()
还可以接受一个返回新状态的函数作为参数。这种情况下,React会把这个函数应用于当前状态以生成新的状态,这对于处理依赖于前一个状态来计算下一个状态的情况非常有用。
-
示例用法:
// 对象形式 this.setState({ count: this.state.count + 1 }); // 函数形式 this.setState((prevState) => { return { count: prevState.count + 1 }; }); // 使用回调函数确认状态已更新 this.setState({ message: 'Hello' }, () => { // 此处可以安全地使用更新后的message状态 });
总之,setState()
是React中核心的API之一,它帮助开发者管理组件内部的状态变化,并驱动视图的自动更新,实现了React声明式编程的核心理念。
11.Hooks
-
状态钩子 (useState): 用于定义组件的 State,其到类定义中this.state的功能;
// useState 只接受一个参数: 初始状态 // 返回的是组件名和更改该组件对应的函数 const [flag, setFlag] = useState(true); // 修改状态 setFlag(false) // 上面的代码映射到类定义中: this.state = { flag: true } const flag = this.state.flag const setFlag = (bool) => { this.setState({ flag: bool, }) }
-
生命周期钩子 (useEffect):
类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做componentDidMount、componentDidUpdate和componentWillUnmount的结合。
useEffect(callback, [source])接受两个参数
-
callback: 钩子回调函数;
-
source: 设置触发条件,仅当 source 发生改变时才会触发;
-
useEffect钩子在没有传入[source]参数时,默认在每次 render 时都会优先调用上次保存的回调中返回的函数,后再重新调用回调;
useEffect(() => { // 组件挂载后执行事件绑定 console.log('on') addEventListener() // 组件 update 时会执行事件解绑 return () => { console.log('off') removeEventListener() } }, [source]); // 每次 source 发生改变时,执行结果(以类定义的生命周期,便于大家理解): // --- DidMount --- // 'on' // --- DidUpdate --- // 'off' // 'on' // --- DidUpdate --- // 'off' // 'on' // --- WillUnmount --- // 'off' @程序员poetry: 代码已经复制到剪贴板
通过第二个参数,我们便可模拟出几个常用的生命周期:
-
componentDidMount: 传入[]时,就只会在初始化时调用一次
const useMount = (fn) => useEffect(fn, []) @程序员poetry: 代码已经复制到剪贴板
-
componentWillUnmount: 传入[],回调中的返回的函数也只会被最终执行一次
const useUnmount = (fn) => useEffect(() => fn, []) @程序员poetry: 代码已经复制到剪贴板
-
mounted: 可以使用 useState 封装成一个高度可复用的 mounted 状态;
const useMounted = () => { const [mounted, setMounted] = useState(false); useEffect(() => { !mounted && setMounted(true); return () => setMounted(false); }, []); return mounted; } @程序员poetry: 代码已经复制到剪贴板
-
componentDidUpdate: useEffect每次均会执行,其实就是排除了 DidMount 后即可;
const mounted = useMounted() useEffect(() => { mounted && fn() }) @程序员poetry: 代码已经复制到剪贴板
-
其它内置钩子:
-
useContext
: 获取 context 对象 -
useReducer
: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux:
-
并不是持久化存储,会随着组件被销毁而销毁;
-
属于组件内部,各个组件是相互隔离的,单纯用它并无法共享数据;
-
配合useContext`的全局性,可以完成一个轻量级的 Redux;(easy-peasy)
-
-
useCallback
: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果; -
useMemo
: 用于缓存传入的 props,避免依赖的组件每次都重新渲染; -
useRef
: 获取组件的真实节点; -
useLayoutEffect
-
DOM更新同步钩子。用法与useEffect类似,只是区别于执行时间点的不同
-
useEffect属于异步执行,并不会等待 DOM 真正渲染后执行,而useLayoutEffect则会真正渲染后才触发;
-
可以获取更新后的 state;
-
-
自定义钩子(useXxxxx): 基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子,如上面的useMounted。又例如,我们需要每个页面自定义标题:
function useTitle(title) { useEffect( () => { document.title = title; }); } // 使用: function Home() { const title = '我是首页' useTitle(title) return ( <div>{title}</div> ) }
react hooks的好处:
-
跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
-
类定义更为复杂
-
不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
-
时刻需要关注this的指向问题;
-
代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
-
状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。
注意:
-
避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
-
只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
-
不能在useEffect中使用useState,React 会报错提示;
-
类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;
12.useEffect和useLayoutEffect的区别
-
它们的共同点很简单,底层的函数签名是完全一致的,都是调用的
mountEffectImpl
,在使用上也没什么差异,基本可以直接替换,也都是用于处理副作用。 -
那不同点就很大了,
useEffect
在 React 的渲染过程中是被异步调用的,用于绝大多数场景,而LayoutEffect
会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在LayoutEffect
做计算量较大的耗时任务从而造成阻塞。 -
在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用
useEffect
,一般问题不大;如果页面有异常,再直接替换为useLayoutEffect
即可。
13.受控组件和非受控组件
在React框架中,受控组件和非受控组件是处理表单输入元素(如<input>
、<textarea>
、<select>
等)的不同策略。
受控组件:
-
定义:受控组件的值是由React组件自身的state所控制的。这意味着表单元素的值不是直接由DOM维护,而是通过React组件的state来决定。每次用户输入时,都会触发一个事件处理器(如
onChange
),这个处理器会更新组件的state,进而重新渲染组件,使得表单字段的值总是与React state中的值保持同步。 -
特点:
-
表单元素必须有对应的onChange事件处理器,用来更新state。
-
表单元素的值通过其value属性(对于checkbox和radio则是checked属性)绑定到state变量上。
-
开发者拥有对表单数据变化的完全控制权,确保任何时候组件的state都反映了当前的表单值。
-
非受控组件:
-
定义:非受控组件的值没有直接关联到React组件的state上,它们允许DOM自身维持输入元素的值,因此其值是由DOM节点内在的HTML属性(如defaultValue)初始化,或者直接由用户输入产生。
-
特点:
-
没有强制要求使用onChange事件来更新state以反映输入值的变化。
-
若要获取非受控组件的当前值,可以使用React的ref特性来访问DOM节点的值。
-
用户可以直接修改输入值,而不需要经过React的state机制,这增加了灵活性,但在某些情况下可能使数据管理和验证变得复杂。
-
应用场景:
-
受控组件适用于需要严格控制表单输入、实时反映输入变化以及进行即时校验的场景。
-
非受控组件适用于简单的、一次性读取输入值或者无需实时追踪输入状态的情况,例如某些只读或纯展示用途的输入框,或者复杂的富文本编辑器组件,这类组件通常自带内部状态管理机制。
总结来说,受控组件提供了更清晰的数据流和更好的可控性,但需要更多代码来处理每一步状态变化;而非受控组件则简化了编码工作量,但对于输入值的跟踪和管理较为间接。根据项目需求和具体场景选择合适的组件类型是很重要的实践。
14.Redux实现原理解析
在 Redux 的整个工作过程中,数据流是严格单向的。
14-1为什么要用redux
在React
中,数据在组件中是单向流动的,数据从一个方向父组件流向子组件(通过props
),所以,两个非父子组件之间通信就相对麻烦,redux
的出现就是为了解决state
里面的数据问题
14-2Redux设计理念
Redux
是将整个应用状态存储到一个地方上称为store
,里面保存着一个状态树store tree
,组件可以派发(dispatch
)行为(action
)给store
,而不是直接通知其他组件,组件内部通过订阅store
中的状态state
来刷新自己的视图
如果你想对数据进行修改,只有一种途径:派发 action
。action 会被 reducer 读取,进而根据 action 内容的不同对数据进行修改、生成新的 state(状态),这个新的 state 会更新到 store 对象里,进而驱动视图层面做出对应的改变。
14-3Redux三大原则
-
唯一数据源
整个应用的state都被存储到一个状态树里面,并且这个状态树,只存在于唯一的store中
-
保持只读状态
state
是只读的,唯一改变state
的方法就是触发action
,action
是一个用于描述以发生时间的普通对象
-
数据改变只能通过纯函数来执行
使用纯函数来执行修改,为了描述
action
如何改变state
的,你需要编写reducers
14-4从编码的角度理解Redux工作流
-
使用
createStore 来完成 store 对象的创建
// 引入 redux import { createStore } from 'redux' // 创建 store const store = createStore( reducer, initial_state, applyMiddleware(middleware1, middleware2, ...) ); @程序员poetry: 代码已经复制到剪贴板
createStore 方法是一切的开始,它接收三个入参:
-
reducer;
-
初始状态内容;
-
指定中间件
-
reducer 的作用是将新的 state 返回给 store
一个 reducer 一定是一个纯函数,它可以有各种各样的内在逻辑,但它最终一定要返回一个 state:
const reducer = (state, action) => { // 此处是各种样的 state处理逻辑 return new_state } @程序员poetry: 代码已经复制到剪贴板
当我们基于某个 reducer 去创建 store 的时候,其实就是给这个 store 指定了一套更新规则:
// 更新规则全都写在 reducer 里 const store = createStore(reducer) @程序员poetry: 代码已经复制到剪贴板
-
action 的作用是通知 reducer “让改变发生”
要想让 state 发生改变,就必须用正确的 action 来驱动这个改变。
const action = { type: "ADD_ITEM", payload: '<li>text</li>' } @程序员poetry: 代码已经复制到剪贴板
action 对象中允许传入的属性有多个,但只有 type 是必传的。type 是 action 的唯一标识,reducer 正是通过不同的 type 来识别出需要更新的不同的 state,由此才能够实现精准的“定向更新”。
-
派发 action,靠的是 dispatch
action 本身只是一个对象,要想让 reducer 感知到 action,还需要“派发 action”这个动作,这个动作是由 store.dispatch 完成的
。这里我简单地示范一下:
import { createStore } from 'redux' // 创建 reducer const reducer = (state, action) => { // 此处是各种样的 state处理逻辑 return new_state } // 基于 reducer 创建 state const store = createStore(reducer) // 创建一个 action,这个 action 用 “ADD_ITEM” 来标识 const action = { type: "ADD_ITEM", payload: '<li>text</li>' } // 使用 dispatch 派发 action,action 会进入到 reducer 里触发对应的更新 store.dispatch(action) @程序员poetry: 代码已经复制到剪贴板
15.react中有哪些优化性能的手段
React框架以其Virtual DOM(虚拟DOM)和高效的diff算法而著称,但为了进一步提升应用程序性能,开发者可以采取多种策略来优化React组件的性能。以下是几个关键的React性能优化手段:
-
shouldComponentUpdate / PureComponent / React.memo
-
使用
shouldComponentUpdate(nextProps, nextState)
方法定制组件的更新逻辑,仅当props或state发生变化时才触发渲染。 -
使用
React.PureComponent
替换React.Component
,它自带浅比较 props 和 state 的功能,如果两者都没有变化,则不会触发子组件的重新渲染。 -
对函数组件使用
React.memo
进行优化,它提供了类似 PureComponent 的优化,防止不必要的渲染。
-
-
useMemo 和 useCallback
-
useMemo
用于缓存计算结果,确保在依赖项没有变化时复用之前的计算结果,避免在每个渲染周期都执行昂贵的计算。 -
useCallback
类似地,用于缓存函数引用,防止因为函数实例变化导致不必要的子组件重新渲染。
-
-
useEffect 的优化
-
精细化地控制 useEffect 清理函数和依赖数组,避免不必要的副作用执行和渲染。
-
-
useTransition
-
通过
useTransition
Hook 可以延迟状态更新,将多个状态变更合并并在浏览器空闲时一次性更新,有助于保持流畅的用户体验。
-
-
懒加载与代码分割
-
利用
React.lazy
和Suspense
实现组件级别的懒加载,只有在需要时才加载模块,减小初始加载体积。 -
结合构建工具如Webpack实现代码分割,按需加载资源。
-
-
避免无必要的渲染
-
不要在render方法中创建新的引用类型(如对象、数组),除非必要,因为这会导致父组件即使其他props不变也重新渲染。
-
避免在循环中直接创建内联函数,尤其是在列表渲染场景,可以通过闭包或其他方式提前绑定函数。
-
-
优化事件处理器
-
将事件处理器绑定在构造函数或使用箭头函数的形式固定其上下文,避免每次渲染时产生新的函数实例。
-
-
组件层级扁平化
-
减少组件嵌套深度,避免不必要的渲染穿透,使用React.Fragment代替多余的DOM元素。
-
-
服务端渲染(SSR)
-
提升首屏加载速度和SEO效果,通过服务端渲染快速展示初始内容,然后客户端接管交互。
-
-
状态管理
-
合理组织和合并状态,避免频繁的小粒度状态变更,利用Redux、MobX等状态管理库可以更精细地控制何时何地更新状态。
-
-
静态标记
-
使用
React.memo
或者静态属性React.memoizeProps
(实验性特性) 优化不可变或者静态部分的渲染。
-
以上就是React中常见的性能优化手段,具体的实践会根据项目需求和环境灵活运用。随着React版本的迭代,还会有更多性能相关的API和最佳实践出现,例如上述提到的 useTransition
功能是在较新的React版本中引入的。
16.如何避免ajax数据请求重新获取
一般而言,ajax请求的数据都放在redux中存取。
17.类组件与函数组件有什么区别呢?
-
作为组件而言,类组件与函数组件在使用与呈现上没有任何不同,性能上在现代浏览器中也不会有明显差异
-
它们在开发时的心智模型上却存在巨大的差异。类组件是基于面向对象编程的,它主打的是继承、生命周期等核心概念;而函数组件内核是函数式编程,主打的是 immutable、没有副作用、引用透明等特点。
-
之前,在使用场景上,如果存在需要使用生命周期的组件,那么主推类组件;设计模式上,如果需要使用继承,那么主推类组件。
-
但现在由于 React Hooks 的推出,生命周期概念的淡出,函数组件可以完全取代类组件。
-
其次继承并不是组件最佳的设计模式,官方更推崇“组合优于继承”的设计概念,所以类组件在这方面的优势也在淡出。
-
性能优化上,类组件主要依靠
shouldComponentUpdate
阻断渲染来提升性能,而函数组件依靠React.memo
缓存渲染结果来提升性能。 -
从上手程度而言,类组件更容易上手,从未来趋势上看,由于React Hooks 的推出,函数组件成了社区未来主推的方案。
-
类组件在未来时间切片与并发模式中,由于生命周期带来的复杂度,并不易于优化。而函数组件本身轻量简单,且在 Hooks 的基础上提供了比原先更细粒度的逻辑组织与复用,更能适应 React 的未来发展。
18.vue和react在diff中有什么区别
Vue和React在实现Virtual DOM的diff算法时有一些策略上的区别,主要体现在以下几个方面:
-
元素类型的判断:
-
Vue:当遇到元素类型相同但属性(如
className
)不同的情况时,Vue会认为这是不同类型的元素,进而选择删除并重建该DOM节点。 -
React:React在确定元素类型时,如果标签名一致,则不会因为某些属性的变化就认定为不同类型的元素,而是仅针对发生变化的属性进行更新。
-
-
列表比对方式:
-
Vue:Vue的列表diff算法采用双向同步遍历(也称为双端比较),从两端开始向中间比较,这使得在一些特定情况下,例如列表尾部元素移动到头部时,Vue可以更快地定位变动并做出更少的DOM操作。
-
React:React的列表diff默认从头至尾遍历新旧两个列表,通过序列号(
key
)来识别和复用已存在的子元素,若没有正确设置key
,在大量元素顺序改变时可能会导致较多不必要的DOM操作。
-
-
Fiber架构影响:
-
React自从引入Fiber架构后,其diff算法有了显著的改进,它允许任务中断和恢复,能够更好地处理优先级调度和异步渲染,尽管这并不直接影响diff算法本身,但在整个渲染流程中增强了React对复杂更新场景的处理能力。
-
-
细节处理:
-
Vue在调用
patch
函数时,直接比较新旧虚拟节点(vnode和oldVnode)来决定如何更新DOM。 -
React在内部管理了一个复杂的 Fiber 树结构,它的diff过程是对Fiber节点进行比较,而非直接的虚拟DOM节点。
-
总结来说,Vue和React虽然都致力于减少真实DOM操作提升性能,但在具体实现diff算法时存在微妙且重要的策略差异,这些差异反映了各自框架在优化DOM更新策略上的不同考量和优化方向。随着版本迭代,两者的diff算法都在持续改进和完善。
19.vue和react在虚拟DOM上有什么区别
Vue和React虽然都采用了虚拟DOM技术来提高UI更新效率,但在具体实现和优化策略上存在一些不同之处:
-
实现方式:
-
React:React通过JSX语法构建虚拟DOM,并通过
ReactDOM.render()
或React.Component
生命周期方法中的render()
函数生成和更新虚拟DOM树。React采用了一套高效的Reconciliation算法(也称DIFF算法)来比较新的虚拟DOM与旧的虚拟DOM之间的差异,并最小化对实际DOM的操作。 -
Vue:Vue同样使用虚拟DOM,但它首先通过模板编译器将模板转化为可执行的渲染函数,然后利用这个函数生成虚拟DOM。Vue 2.x版本有一个观察者系统来跟踪每个组件的数据依赖关系,而在Vue 3.x版本中,通过Composition API和更先进的响应式系统实现了更细粒度的变更追踪,从而在某些情况下能够比React更精确地定位到需要更新的DOM部分。
-
-
更新策略:
-
React:默认情况下,当组件的状态或props发生变化时,React会重新渲染整个组件及其子组件树。为了优化这个过程,开发者可以通过
shouldComponentUpdate()
生命周期方法或者使用PureComponent
/React.memo
来避免不必要的渲染。 -
Vue:Vue利用依赖追踪系统,能够在组件内部自动识别哪些部分因数据变化而需要更新,而不必总是整体重新渲染。Vue能够检测到数据变化并只更新那些真正受到影响的部分,这种机制使得Vue在很多场景下显得更为高效。
-
-
优化细节:
-
Vue 2.x中,由于其依赖收集系统的特性,结合虚拟DOM的diff算法,能够在一定程度上减少无谓的DOM操作。
-
Vue 3.x引入了
Fragment
、Teleport
、Suspense
等功能,并且重构了虚拟DOM系统,使其更加灵活高效,尤其是在编译阶段做了更多静态分析,以进一步提升更新性能。
-
总结来说,Vue和React在虚拟DOM上的区别主要体现在如何构建和管理虚拟DOM以及如何根据数据变化做出精准高效的DOM更新。两者都在不断改进虚拟DOM相关的技术以提高性能,但Vue在自动化追踪依赖方面提供了更精细的控制,而React则提供了一套丰富的生命周期钩子和优化手段供开发者手动优化组件渲染。
20.合成事件理解
React中的合成事件(Synthetic Event)是··一种抽象层,它统一了浏览器原生事件在不同浏览器间的差异,为开发者提供了一致的API接口来处理事件。以下是合成事件的主要理解点:
-
封装原生事件:React并不直接使用浏览器提供的原生DOM事件,而是创建了自己的事件对象,即合成事件对象。这些对象是对原生事件对象的包装,无论在哪个浏览器环境下,它们都表现得如同一个标准化的对象。
-
一致性:由于各个浏览器对事件处理可能存在细微差别,如事件对象的属性名、事件触发顺序、事件传播模型等,React通过合成事件统一了这些差异,使得开发者无需担心浏览器兼容性问题。
-
API设计:合成事件对象与原生事件对象有着相似的方法和属性,如
stopPropagation()
用于阻止事件冒泡,preventDefault()
用于阻止默认行为,同时也包含了与事件相关的各种属性,如target
、type
、currentTarget
等。 -
事件池化:为了提高性能和内存利用率,React的合成事件对象在每次事件处理器执行完毕后都会被回收复用。这意味着如果你在事件处理器外部尝试访问这个事件对象,可能会得到一个无效的结果,除非显式调用了
event.persist()
方法,将事件从事件池中移除。 -
事件委托:React使用事件委托的方式来处理事件,即将事件处理器绑定到最顶层的容器元素(通常是
document
),而不是直接绑定到每个具体的子元素上。这有助于简化事件处理和提高性能。 -
多态化:React根据不同的事件类型,创建不同的合成事件子类,如
SyntheticKeyboardEvent
、SyntheticMouseEvent
、SyntheticFocusEvent
等,它们都继承自基础的SyntheticEvent
类,确保不同类型的事件都有对应的适配器来处理。
综上所述,React的合成事件机制不仅解决了浏览器兼容性问题,还优化了事件处理的性能,让React应用的事件处理变得更加简单、可靠和高效。
21.react中闭包陷阱是什么?如何处理
闭包陷阱是指在使用闭包时可能出现的一些意料之外的问题,这些问题源于闭包特有的性质,即闭包能够记住其所在外部函数的作用域,即使在其外部函数已经执行完毕之后。以下是几个闭包陷阱的示例以及处理方式:
-
变量捕获陷阱:
-
闭包陷阱之一在于闭包会捕获其定义时而非执行时外部作用域的变量。这意味着如果在闭包定义后外部变量的值发生变化,下次闭包执行时使用的仍然是原始捕获的值,而非最新的值。处理办法是明确区分哪些变量应该被捕获,哪些变量需要动态获取,必要时使用额外的变量或参数来传递最新值。
function outer() { let counter = 0; return function inner() { return ++counter; // 如果多次调用outer(),将会共享同一个counter变量 }; }
若要避免此陷阱,可以确保每次调用
outer
时创建独立的counter
:function outer() { return function inner(counter = 0) { return ++counter; }(0); // 这样每次调用outer()都会返回一个新的inner函数,每个inner函数有自己的counter变量 } 或者使用闭包的工厂模式: function createCounter() { let counter = 0; return { increment: function() { return ++counter; }, }; } const myCounter = createCounter();
-
-
资源泄露陷阱:
-
当闭包持有对大对象或资源(如文件句柄、数据库连接等)的引用时,如果不妥善处理,可能会导致这些资源无法被垃圾回收器回收,从而造成内存泄漏。处理方法是在闭包不再需要这些资源时,显式地解除引用或关闭资源。
function setupResource() { const resource = acquireLargeResource(); // 假设这是一个消耗大量内存的资源 return function useResource() { // 使用resource... }; } // 解决方法:在资源不再使用时显式释放 const releaseResource = setupResource(); // 使用资源... releaseResource();
-
-
闭包与循环变量问题:
-
在循环体中创建闭包时,可能会无意中引用到循环变量的最后一个值,而不是期望的循环迭代值。处理方式是通过IIFE或其他方式创建新的作用域来隔离每个循环迭代中的闭包。
for (let i = 0; i < 10; i++) { setTimeout(() => console.log(i), 0); // 所有回调都会打印出10,因为它们共享同一个i变量 } // 正确做法: for (let i = 0; i < 10; i++) { (function(iCopy) { setTimeout(() => console.log(iCopy), 0); // 使用闭包捕获每一次循环的i值 })(i); }
-
-
React Hooks闭包陷阱:
-
在React Hooks(如
useState
、useEffect
等)中,如果在回调函数里直接使用状态或props变量,可能会因为闭包的原因导致获取到的是过时的值。解决办法是利用useRef
储存变量的最新值,或者在useEffect
的依赖数组中列出相关状态或props变量以确保每次更新时都会执行正确的逻辑。
-
通过理解闭包的工作原理,根据具体场景采取适当的措施,可以有效地避免闭包陷阱并充分利用闭包的优点。
22.react框架安全吗?为什么?
React框架本身在设计和实现上是非常注重安全性的,尤其是在防范跨站脚本(XSS)和其他安全漏洞方面:
-
虚拟DOM与输出编码: React在渲染组件时,会自动对用户提供的数据进行适当的编码,防止未经处理的数据直接插入到DOM中而导致XSS攻击。React将所有内容视为JavaScript表达式并进行适当的转义,特别是对于危险属性如innerHTML。
-
防御式编程: React推荐使用JSX来构建UI,JSX天然地提供了一层防护,因为它的内容会被转译为React.createElement函数调用,React会正确处理和转义属性和文本内容。
-
组件化设计: React的组件化架构使得开发者可以更好地控制数据流向和渲染逻辑,通过限制组件内部状态和props的变化,可以减少不安全操作的机会。
-
生命周期方法: React的生命周期方法提供了控制组件何时更新和何时销毁的能力,开发者可以在适当的地方执行清理工作,如取消网络请求或清除定时器,从而避免内存泄漏等问题。
-
社区与最佳实践: React社区广泛提倡并遵循安全编码的最佳实践,包括使用
PropTypes
进行类型检查、避免直接操作DOM、谨慎处理用户输入等。
然而,React只是一个视图层库,它并不能保证应用的整体安全性,特别是涉及到HTTP请求、API调用、密码存储等后台逻辑时的安全性。开发者仍需遵循安全编码规范,并在实践中注意以下几点以增强安全性:
-
对于用户输入进行严格的验证和净化。
-
避免在组件中硬编码敏感信息,如有必要应使用环境变量或安全的存储机制。
-
严格控制第三方库的使用,确保它们也是安全可靠的。
-
使用HTTPS协议传输数据,确保数据传输过程中的安全。
综上所述,React框架在很大程度上有助于开发安全的应用程序,但仍需开发者遵循良好的编码习惯和安全实践,以确保应用在整体上的安全性。
23.react中数据更新而视图不更新问题?如何解决?
在React中,当数据更新但视图未随之更新时,通常是因为React没有检测到状态(state)或props的改变,或者改变未能触发组件的重新渲染。以下是一些可能的原因及解决方案:
-
直接修改状态(state):
-
React通过对比新旧状态(或props)来决定是否需要重新渲染组件。直接修改状态对象(如
this.state.xxx = somethingNew
)不会触发重新渲染。正确做法是使用setState
方法来更新状态:
this.setState((prevState) => ({ ...prevState, yourProperty: updatedValue }));
-
-
引用类型(如对象或数组)的浅比较:
-
React使用浅比较来检测props和state的改变。如果你直接修改了对象或数组的内部属性,但不改变引用,React无法检测到变化。在这种情况下,需要创建一个新的引用:
// 对于对象 this.setState((prevState) => ({ yourObject: { ...prevState.yourObject, propertyToUpdate: newValue } })); // 对于数组 this.setState((prevState) => ({ yourArray: [...prevState.yourArray, newItem] }));
-
-
React.memo或PureComponent:
-
使用
React.memo
或React.PureComponent
可能导致组件在props改变但浅比较结果相同时不重新渲染。如果你发现这种情况,确保你的areEqual
函数或shouldComponentUpdate
方法能够正确地比较props的变化。
-
-
Redux Store更新未触发组件更新:
-
使用Redux时,如果组件没有正确订阅到需要的store部分,或者Redux的selector函数没有正确地创建新引用,可能会导致组件不重新渲染。确保你的
mapStateToProps
函数能够正确地从store中提取数据,并且在数据更新时返回新的对象引用。
-
-
生命周期方法问题:
-
检查是否有生命周期方法(如
shouldComponentUpdate
、getDerivedStateFromProps
等)错误地阻止了组件的重新渲染。
-
-
React.StrictMode:
-
在
React.StrictMode
中,React可能会故意执行一些额外的渲染以辅助调试。如果你发现组件在StrictMode
外正常工作,而在StrictMode
内失效,请检查是否存在上述其他问题。
-
通过仔细排查并修复上述问题,通常可以解决React数据更新但视图不更新的问题。此外,还可以使用React DevTools等工具来帮助诊断和调试。
24.react中connect的作用
在React与Redux的结合使用中,react-redux
库提供的connect
函数起到了核心的桥梁作用。其主要作用是将React组件与Redux的store关联起来,实现以下几个核心功能:
-
状态注入(State Mapping):
-
connect
允许开发者通过mapStateToProps
函数指定从Redux store中选取哪些状态数据注入到被连接(connected)的React组件的props中。当store中的状态发生变化时,connect
会自动触发组件的重新渲染,从而使组件能实时响应store状态的变化。
const mapStateToProps = (state) => ({ user: state.auth.user, posts: state.posts.allPosts }); export default connect(mapStateToProps)(YourComponent);
-
-
动作派发(Dispatch Mapping):
-
connect
通过mapDispatchToProps
函数将Redux action creators包装进props中,可以直接在组件内部调用这些action creator,并自动通过store.dispatch
方法触发action。可以选择传入对象形式(自动绑定dispatch)或函数形式(手动处理dispatch)。
// 对象形式(自动绑定dispatch) const mapDispatchToProps = { fetchUser: fetchUserActionCreator, updatePost: updatePostActionCreator }; // 函数形式(手动处理dispatch) const mapDispatchToProps = (dispatch) => ({ fetchUser: () => dispatch(fetchUserActionCreator()), updatePost: (postId, updates) => dispatch(updatePostActionCreator(postId, updates)) }); export default connect(mapStateToProps, mapDispatchToProps)(YourComponent);
-
-
优化性能:
-
connect
还具备一定的性能优化能力,它通过浅比较props和state的变化来决定是否真正触发组件的重新渲染,避免不必要的渲染过程。
-
-
无需直接访问store:
-
使用
connect
后,React组件无需直接导入和访问Redux store,增强了组件的可复用性和解耦程度,使得组件关注点更集中于展示逻辑。
-
综上所述,connect
函数通过将Redux store的state映射到React组件的props,同时将action creators注入到props中,极大地简化了React组件与Redux store之间的交互,促进了代码组织和管理的模块化。
25.Redux 和 React-Redux区别
Redux 和 React-Redux 是两个紧密相关的库,但它们在用途和职责上有所不同:
-
Redux:
-
Redux 是一个独立的状态(Store)管理库,可用于任何JavaScript应用,不仅仅是React应用。它的核心概念是单一数据源(Single Source of Truth),所有的应用状态都被集中管理在一个全局的store中。
-
Redux 提供了一种管理应用状态的模式,其中包括创建store(包含应用的所有状态)、定义actions(表示状态的改变)和reducers(纯函数,接收旧的state和action,返回新的state)。
-
Redux 通过store.dispatch(action)的方式来触发状态变化,并通过store.subscribe(listener)来监听状态变化。
-
-
React-Redux:
-
React-Redux 是一个专门为React应用设计的官方绑定库,它提供了一种机制,使得React组件能够与Redux的store进行连接和交互。
-
主要提供了
Provider
组件和connect
函数:-
Provider
组件包裹整个React应用,它将Redux store注入到组件树的上下文中,使得子组件可以通过React的Context API访问到store。 -
connect
函数用于将Redux store的state映射到React组件的props上,同时也可以将React组件的dispatch方法包装成props,使得组件能够轻松发起action来改变状态。
-
-
总结一下,Redux本身是一种状态管理库,提供了全局状态管理的机制和API;而React-Redux是Redux与React集成的桥梁,它简化了React组件与Redux store之间的交互,使React组件能够便捷地读取和修改Redux store中的状态。