说说你对react hook的理解
React Hooks 是 React 16.8 版本引入的一种新的特性,它允许在不编写 class 的情况下使用 state 和其他 React 特性。我的理解主要集中在以下几个方面:
函数组件的状态管理:Hooks 提供了
useState
这样的钩子函数,使得我们可以在函数组件中使用状态(state),而不需要将组件转换为 class 组件。这简化了代码,并使组件的逻辑更易于理解和维护。副作用的处理:使用
useEffect
可以替代类组件中的生命周期方法(如componentDidMount
、componentDidUpdate
、componentWillUnmount
),用于处理组件副作用,如数据获取、订阅管理等。自定义 Hooks:Hooks 提供了编写自定义钩子函数的能力,使得我们可以将组件之间共享的逻辑提取到可复用的函数中。这样可以有效地提高代码的重用性和可维护性。
更加函数式和声明式:使用 Hooks 后,组件中的逻辑更加集中和声明式,而不是被分散在生命周期方法中。这使得 React 组件的编写更加自然和直观。
性能优化:Hooks 在内部实现上有助于 React 的性能优化,可以避免 class 组件中因为生命周期方法调用顺序的问题导致的 bug,并且更容易优化渲染性能。
总体来说,React Hooks 是一个强大且灵活的工具,使得 React 组件的编写变得更加简洁、清晰和可维护,同时也推动了函数式编程风格在 React 生态中的普及和应用。
单向数据绑定和双向数据绑定的优缺点
单向数据绑定
优点:
- 简单可控: 单向数据流通常更易于理解和调试,因为数据的流向是单一的,从数据源到UI展示,降低了复杂性。
- 性能优化: 由于数据流向明确,框架可以更容易地进行性能优化,例如实现虚拟DOM等技术。
- 预测性: 因为数据流向是单向的,使得状态变化更加可预测,减少了意外的副作用。
缺点:
- 繁琐的手动更新: 当数据发生变化时,UI无法自动更新,需要手动编写更新UI的逻辑。
- 输入交互复杂: 对于需要用户输入的表单等场景,需要手动处理用户输入并更新数据状态。
双向数据绑定
优点:
- 便捷的表单处理: 在处理表单等需要用户输入的场景中,双向绑定能够使得数据和UI之间的同步更加便捷。
- 简化代码: 可以减少手动编写的数据更新逻辑,减少重复代码。
缺点:
- 复杂性增加: 双向数据绑定引入了数据的双向流动,增加了系统的复杂性,使得数据流向不再那么明确,可能导致难以追踪的bug。
- 潜在的性能问题: 数据频繁变化时,双向绑定可能导致大量不必要的UI更新,影响性能。
为什么React并不推荐优先考虑使⽤Context?
- 组件耦合:使用 Context 可以使得很深的组件树中的任何组件都可以访问到 Context 中的数据,这可能会增加组件间的耦合。当你需要修改数据或者行为时,可能需要修改很多不相关的组件。
性能问题:当一个 Context 值改变时,所有消费这个 Context 的组件都会重新渲染。如果使用 Context 的组件很多,那么可能会导致性能问题。
复用性降低:如果一个组件依赖于 Context,那么这个组件就不能在没有这个 Context 的环境中运行,这降低了组件的复用性。
immutable的原理是什么?
旨在创建不可变(Immutable)的数据结构。其原理是通过在数据变化时不修改原始数据,而是创建一个新的数据副本来实现。这意味着一旦数据被创建,就无法再被修改。Immutable 数据结构保证了数据的不可变性,从而简化了数据处理和状态管理。
在 React 中判断点击元素属于哪个组件
可以通过给每个组件添加一个唯一的标识符,然后在事件处理函数中通过 event.target 获取点击的元素,进而判断点击的元素属于哪个组件。
什么是浅层渲染?
React 测试中一种只渲染组件本身而不渲染其子组件的技术。在浅层渲染中,React 将组件渲染成虚拟 DOM,但不会递归渲染其子组件,而是用占位符代替子组件。这使得测试更加专注于被测试组件本身的行为,而不必关心其子组件的状态和行为。
为什么建议setState的第一个参数是callback而不是一个对象呢?
使用回调函数的方式可以确保在 state 更新完成并且组件重新渲染后执行相应的逻辑,从而避免了因为获取到旧的 state 而导致的 bug。
解释下React中component和pureComponent两者的区别是什么?
在 React 中,
Component
和PureComponent
都是用来创建组件的基类,但它们在更新机制上有一些区别。
Component:
Component
是 React 中所有组件的基类。- 当组件的
state
或props
发生变化时,无论是否实际发生了变化,React 都会重新渲染组件及其所有子组件。- 这意味着,即使组件的
state
或props
没有变化,也会触发不必要的重新渲染,可能导致性能损失。PureComponent:
PureComponent
是Component
的一个子类,它在Component
的基础上实现了一种浅比较(shallow comparison)的机制。- 当组件的
props
或state
发生变化时,PureComponent
会先对新旧props
和state
进行浅比较。只有当新旧props
或state
不相等时,React 才会触发重新渲染;否则,不会重新渲染组件及其子组件。- 这种机制可以减少不必要的重新渲染,提高性能。
总的来说,
PureComponent
在更新机制上通过浅比较来避免不必要的重新渲染,从而提高性能。但需要注意的是,由于浅比较只能比较对象的引用值,如果props
或state
中包含复杂的数据结构(如数组或对象),可能会导致浅比较失效,从而影响性能。因此,在使用PureComponent
时,需要确保props
和state
中的数据结构是不可变的(immutable)或者通过深比较来确保比较的准确性。
React声明组件有哪几种方法,各有什么不同?
React声明组件的方法包括函数式组件、类组件、React Hooks、React.PureComponent和React.memo。
说说你对React的reconciliation(一致化算法)的理解
Reconciliation算法的核心思想是尽量复用已有的DOM节点,减少创建新节点和删除旧节点的操作。React会根据节点的类型、属性等信息来判断节点是否可以复用,从而实现高效的更新。这种机制使得React在处理大规模数据变化时仍能保持较高的性能表现。
React 的生命周期?每个生命周期都做了什么?
-
挂载阶段:
constructor()
:组件的构造函数,在组件被创建时调用,用于初始化状态和绑定事件处理函数。static getDerivedStateFromProps(props, state)
:在组件接收到新的 props 或者在组件初始化时调用,返回一个对象来更新状态,用于替代旧的componentWillReceiveProps
。render()
:根据组件的状态和属性渲染组件的内容。componentDidMount()
:组件挂载后调用,可以进行 DOM 操作或数据请求等副作用。
-
更新阶段:
static getDerivedStateFromProps(props, state)
:在组件接收到新的 props 时调用,返回一个对象来更新状态。shouldComponentUpdate(nextProps, nextState)
:在组件接收到新的 props 或 state 时调用,用于控制组件是否重新渲染。render()
:根据新的状态和属性重新渲染组件。getSnapshotBeforeUpdate(prevProps, prevState)
:在最近一次渲染输出(提交到 DOM 上)之前调用,可以在此保存当前 DOM 的一些信息。componentDidUpdate(prevProps, prevState, snapshot)
:组件更新完成后调用,可以进行 DOM 操作或处理更新前的信息。
-
卸载阶段:
componentWillUnmount()
:组件卸载前调用,用于清理定时器、取消订阅等操作。
Promis.all,并发几个请求,其中一个请求错误了会发生什么
Promise.all
是一个用于并行执行多个 Promise 的方法,它接收一个 Promise 数组作为参数,并返回一个新的 Promise,该 Promise 在数组中的所有 Promise 都成功解析(resolve)时才会解析,如果其中任何一个 Promise 被拒绝(reject)了,则整个Promise.all
也会被拒绝。
说说React render方法的原理?在什么时候会被触发?
render
函数里面可以编写JSX
,转化成createElement
这种形式,用于生成虚拟DOM
,最终转化成真实DOM
在 React
中,类组件只要执行了 setState
方法,就一定会触发 render
函数执行,函数组件使用useState
更改状态不一定导致重新render
组件的 props
改变了,不一定触发 render
函数的执行,但是如果 props
的值来自于父组件或者祖先组件的 state
在这种情况下,父组件或者祖先组件的 state
发生了改变,就会导致子组件的重新渲染
所以,一旦执行了setState
就会执行render
方法,useState
会判断当前值有无发生改变确定是否执行render
方法,一旦父组件发生渲染,子组件也会渲染
说说React事件和原生事件的执行顺序
为什么要有合成事件
- 在传统的事件里,不同的浏览器需要兼容不同的写法,在合成事件中
React
提供统一的事件对象,抹平了浏览器的兼容性差异 React
通过顶层监听的形式,通过事件委托的方式来统一管理所有的事件,可以在事件上区分事件优先级,优化用户体验
React
在合成事件上对于16
版本和17
版本的合成事件有很大不同,我们也会简单聊聊区别。
总结
16
版本先执行原生事件,当冒泡到document
时,统一执行合成事件,17
版本在原生事件执行前先执行合成事件捕获阶段,原生事件执行完毕执行冒泡阶段的合成事件,通过根节点来管理所有的事件
原生的阻止事件流会阻断合成事件的执行,合成事件阻止后也会影响到后续的原生执行
说说对受控组件和非受控组件的理解,以及应用场景?
-
受控组件:
- 受控组件是指组件的状态由 React 组件的 state 控制,并通过 props 传递给子组件,子组件通过回调函数来更新父组件的状态。
- 在受控组件中,数据流是单向的,父组件通过 props 将状态传递给子组件,子组件通过回调函数将事件传递回父组件。
- 应用场景:适用于需要实时控制和监控组件状态变化的情况,例如表单输入框、复选框等需要即时响应用户输入的组件。
-
非受控组件:
- 非受控组件是指组件的状态不受 React state 控制,而是由 DOM 元素本身维护状态,React 只负责渲染。
- 在非受控组件中,组件的状态由 DOM 元素自身管理,React 只负责初始化组件和处理生命周期方法。
- 应用场景:适用于简单的交互组件,不需要频繁地进行状态管理和更新的情况,可以减少一些状态管理的代码量,例如一些仅需初始化时设置数值的组件。
说说你对immutable的理解?如何应用在react项目中?
Immutable 是指数据不可变,一旦创建之后就不能被修改。在 JavaScript 中,原始类型(如字符串、数字)是不可变的,而对象和数组是可变的。
-
性能优化:使用 Immutable 数据结构可以帮助 React 进行浅比较,提高组件更新时的性能。通过比较引用而不是深层次比较对象内容,可以减少不必要的重新渲染。
-
简化状态管理:不可变数据结构可以减少状态管理的复杂性。在 Redux 或者其他状态管理工具中,配合 Immutable 数据结构可以更容易地追踪状态的变化,并且可以避免直接修改状态而导致的副作用。
-
防止意外修改:不可变数据结构可以减少由于在多处修改同一数据而引发的 bug。在 React 组件中,通过使用 Immutable 数据结构,可以更容易地追踪数据的变化,从而减少意外的状态修改。
在 React 项目中,可以通过以下方式应用 Immutable 数据结构:
- 使用 Immutable.js:Immutable.js 是一个专门为 JavaScript 提供不可变数据结构的库,可以使用其中的 Map、List 等数据结构来管理 React 组件的状态。
- 结合 Redux 或者 Context API:在使用 Redux 或者 React 的 Context API 进行状态管理时,可以结合 Immutable 数据结构来管理状态,以提高状态管理的效率和可维护性。
说说React Jsx转换成真实DOM过程?
-
JSX 编写阶段:
- 在编写 React 组件时,可以使用 JSX 语法来描述组件的结构和内容。
-
JSX 转换阶段:
- 在构建过程中,使用 Babel 或其他工具将 JSX 转换成普通的 JavaScript 代码。
- JSX 中的标签会被转换成对应的 React.createElement 函数调用。
- JSX 中的属性会被转换成一个对象,作为 createElement 函数的第二个参数传递。
- JSX 中的子元素会被转换成 createElement 函数的额外参数,作为子元素传递。
-
React.createElement 函数调用:
- 在转换后的代码中,每个 JSX 标签都会被转换成对应的 React.createElement 函数调用。
- React.createElement 接收三个参数:标签名(字符串或函数)、属性对象和子元素。
- React.createElement 函数会返回一个描述该元素的对象,称为 React 元素(React Element)。
-
虚拟 DOM 构建阶段:
- 在执行 React.createElement 函数时,会创建一个描述组件结构的虚拟 DOM 对象(Virtual DOM)。
-
虚拟 DOM 渲染阶段:
- 当组件准备好进行渲染时,React 会将虚拟 DOM 进行比对,并计算出需要进行更新的部分。
-
真实 DOM 更新阶段:
- 在虚拟 DOM 渲染阶段结束后,React 会将需要更新的部分转换为真实 DOM 操作。
说说你在React项目是如何捕获错误的?
在 React 项目中,采用错误边界、全局错误处理、组件生命周期方法、try...catch 等方式结合起来,可以有效地捕获和处理错误,提高应用的稳定性和用户体验。同时,及时记录和监控错误信息也是保障项目质量的重要手段。
-
错误边界(Error Boundaries):
- 使用 React 的错误边界是一种捕获组件树中 JavaScript 错误的方式。通过定义错误边界组件,在组件树中将错误限制在边界内,避免整个应用崩溃。
- 在类组件中,可以通过 componentDidCatch 钩子函数捕获组件生命周期中的错误。
- 在函数式组件中,可以使用 ErrorBoundary 组件包裹需要捕获错误的部分。
说说React服务端渲染怎么做?原理是什么?
-
服务器端渲染阶段:
- 当请求到达服务器时,服务器会根据请求的 URL 路径找到对应的 React 组件,并调用相应的生命周期方法或函数组件。
- 服务器使用 ReactDOMServer 提供的 renderToString 或 renderToNodeStream 方法,将组件渲染成一个字符串或流。
-
HTML 响应生成阶段:
- 服务器将渲染后的字符串或流包装在一个 HTML 模板中,并返回给客户端。
- 这个 HTML 模板通常包括渲染后的组件内容、样式和其他静态资源的链接。
-
客户端激活阶段:
- 客户端接收到 HTML 响应后,会解析 HTML 文档,并重新创建 React 组件树。
- 在客户端重新创建组件时,React 会尽量复用服务器端生成的组件实例,以节省计算和网络开销。
- 客户端会在组件挂载后,继续处理后续的交互和更新。
简单介绍下React中的 diff 算法(深度比较)
diff 算法的三级策略
dif算法可以总结为三个策略,分别从树、组件及元素三个层面进行复杂度的优化:
策略一:忽略节点跨层级操作场景,提升比对效率。(基于树进行对比)
这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法是非常"暴力”的,即两课树只对同一层次的节点进行比较,如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,这就提升了比对效率。
策略二:如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构。(基于组件进行对比)在组件比对的过程中:
如果组件是同一类型则进行树比对;
如果不是则直接放入补丁中。
只要父组件类型不同,就会被重新渲染,这也就是为什么 shoudcomponentupdate、Purecomponent及 Reactmemo 可以提高性能的原因。
策略三:同一层级的子节点,可以通过标记 key 的方式进行列表对比。(基于节点进行对比)
元素比对主要发生在同层级中,通过标记节点操作生成补丁,节点操作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要的作用。通过标记 key 的方式,React 可以直接移动 DOM 节点,降低内耗。
React Fiber 是如何实现更新过程可控?
-
可中断性:React Fiber 将整个组件更新过程分解为多个可中断的小任务单元,称为 fiber。这些 fiber 可以根据优先级和时间片来进行调度,使得 React 能够在更新过程中灵活地中断、暂停和恢复任务,从而提高用户界面的响应性。
-
优先级调度:每个 fiber 都有自己的优先级,React Fiber 根据优先级动态调整任务的执行顺序,确保高优先级任务能够优先执行,从而实现对更新过程的控制。
-
它通过将一个大的更新过程分割成多个小步骤,每个小步骤执行一部分工作,然后暂停并继续执行其他工作,从而实现了更新过程的可控。
react15和reaact16在diff区别
react15把页面渲染分为了两个阶段:1.协调/调和阶段(reconclie):在这个阶段 React 会更新数据生成新的 虚拟Dom树,然后通过Diff算法,从该节点/组件(使用useState声明该state的节点,而非调用setXXX的节点)开始往下递归(该节点&所有子节点)虚拟Dom树找出需要更新的节点,放到更新队列中去,得到新的更新队列。该过程不可中断。
2.渲染阶段(commit):这个阶段 React 会遍历更新队列,将其所有的变更一次性更新到Dom上。
简单来说,一个负责找,另一个负责改。
react16
react16引入了fiber的概念,此时的页面渲染可以分为三个阶段:1.调度阶段(schedule):调度任务的优先级,高优先级的先进入协调阶段。
2.协调阶段(reconclie):找出变化的节点,从不可中断的递归变成了可中断的循环过程,内部采用了fiber架构,react16就是把之前的stack reconlie(直到执行栈被执行空才会停止)重构成了fiber reconclie(可中断)。
3.渲染阶段(commit):将变更一次性更新到真实Dom上。
react15 diff不可中断原因
简单来说,我们的Vnode(虚拟节点)中只存有子节点(childrenNode)的信息,如果我们打断了,是找不到它的父亲和兄弟节点的,所以就又得重头开始找。
为什么要引入fiber
上面我们将react15和react16页面渲染的大致流程进行了讲解,我们会发现,最直观的差异就是:寻找变化节点的过程由不可中断变成了可以中断。
为什么要这样做呢?
浏览器js引擎和页面渲染引擎是在同一个渲染线程之内,两者是互斥关系。
当前有新的数据更新时候,我们需要递归虚拟Dom树,找出变动节点,如果其中dom节点过多,那么这个过程时间消耗的会很长,并且无法中断,所以会导致其他事件影响滞后,造成卡顿。
react15及之前版本:当有节点发生变动,会递归对比虚拟dom,找出变动节点,之后同步更新他们。这种遍历是递归调用,执行栈会越来越深,而且不能中断,中断后就不能恢复了。递归如果非常深,就会十分卡顿。
————————————————
fiber是什么
fiber:直译 纤维,意在指比线程Thread更细小的执行粒度。
从整体上看,fiber就是把原本不可中断的协调过程碎片化,化整为零,每次执行完一个小任务,都会让出线程,给其他任务执行的机会。
从本质上看,fiber实际上是一种数据结构:特殊的链表。每个节点都是一个 fiberNode。一个 fiberNode包括了 child(第一个子节点)、sibling(兄弟节点)、return(父节点)等属性(解决了react15Vnode中只有子节点信息,信息不足导致无法中断的问题)。
react 会先把 vdom 转换成 fiber,再去进行 reconcile,这样就是可打断的了。
fiber结构
{
type: any, // 对于类组件,它指向构造函数;对于DOM元素,它指定HTML tag
key: null | string, // 唯一标识符
stateNode: any, // 保存对组件的类实例,DOM节点或与fiber节点关联的其他React元素类型的引用
child: Fiber | null, // 大儿子
sibling: Fiber | null, // 下一个兄弟
return: Fiber | null, // 父节点
tag: WorkTag, // 定义fiber操作的类型, 详见https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
nextEffect: Fiber | null, // 指向下一个节点的指针
alternate: Fiber | null,
updateQueue: mixed, // 用于状态更新,回调函数,DOM更新的队列
memoizedState: any, // 用于创建输出的fiber状态,记录内部state对象的属性
pendingProps: any, // 已从React元素中的新数据更新,并且需要应用于子组件或DOM元素的props
memoizedProps: any, // 在前一次渲染期间用于创建输出的props
// ……}
setState 是同步,还是异步的?
setState
方法既可以是同步的,也可以是异步的,具体取决于使用 setState
的地方和上下文。
-
同步更新:当在生命周期方法(如
componentDidMount
、componentDidUpdate
)和原生事件处理函数中调用setState
时,通常会同步更新状态。这是因为 React 可以立即执行状态更新并重新渲染组件。 -
批量异步更新:在大多数情况下,
setState
是异步的,即在事件处理函数、异步回调或setTimeout
等代码块中调用setState
时,React 会将多个setState
调用合并成一个批量更新操作,以提高性能。这意味着连续的setState
调用不会立即触发组件的重新渲染,而是在当前代码块执行完毕后,React 根据一定策略进行更新。
简述下 React 的事件代理机制?
React 使用了一种称为事件代理)的机制来处理组件中的事件。事件代理是一种将事件处理函数绑定到父元素而不是每个子元素的技术,它利用事件冒泡机制将事件从子元素传递到父元素进行处理。
-
事件绑定:在 React 组件中,可以通过在 JSX 中添加事件监听器来绑定事件处理函数
-
事件冒泡:当用户在子元素上触发事件时,该事件会按照从子元素到父元素的顺序依次向上冒泡。也就是说,事件会首先在触发事件的子元素上被触发,然后沿着组件层级向上冒泡到更高层的父元素。
-
事件委派:React 在组件树的根节点上绑定事件监听器,这个根节点可以是最外层的 DOM 元素,也可以是某个组件的容器元素。当事件冒泡至根节点时,React 会根据事件类型找到对应的事件处理函数,并执行相应的处理逻辑。这种方式避免了为每个子元素都绑定事件监听器,提高了性能和内存利用率。
为什么不能在循环、条件或嵌套函数中调用 Hooks?
react用单链表来严格保证hooks的顺序。Hooks 依赖于 React 内部的调用顺序来确定每个 Hook 的对应关系,如果在循环、条件语句或嵌套函数中调用 Hooks,可能会导致 Hooks 调用顺序发生变化,从而破坏 React 内部的依赖关系。这种情况下,React 可能无法正确地管理状态,造成组件的不稳定行为。
-
违反规则一致性:
- React Hooks的一个关键原则是每次渲染时都按照相同顺序执行。如果在if语句或者for循环里使用Hook,那么其执行顺序将变得不确定,这会打破React内部对Hook执行顺序的依赖和管理。
-
状态管理混乱:
- 如果允许在条件分支内创建状态,会导致组件状态逻辑难以理解和维护。每个状态都应该与组件的生命周期直接相关,而不是根据运行时条件临时创建。
-
潜在的问题与错误:
- 在条件语句或循环中使用Hook可能导致意外的行为,例如某些状态可能不会被正确地初始化或清理,造成内存泄漏等问题。
-
代码可读性和可维护性下降:
- 将Hook置于非顶层会使代码结构变得复杂,降低可读性。遵循统一的规则可以提高代码的一致性和团队协作效率。
说说你对 React Hook的闭包陷阱的理解,有哪些解决方案?
常见的 React Hook 闭包陷阱包括:
- 在 useEffect 或 useCallback 中引用了过时的 state 或 props。
- 在自定义 Hook 中使用了闭包来保存状态或引用,可能导致多个实例之间状态共享。
- 在循环中使用 Hook,可能导致闭包中的变量无法正确捕获循环中的值。
解决 React Hook 的闭包陷阩可以采取以下方案:
- 使用 useEffect 的依赖项数组来确保正确捕获最新的 state 和 props 值,避免闭包中引用过时的值。
- 避免在自定义 Hook 中使用闭包来保存状态或引用,可以使用 useRef 来保存可变的引用,或者通过 useReducer 来管理状态。
- 在循环中使用 Hook 时,可以通过将循环变量作为 useEffect 的依赖项或传递给回调函数的方式来正确捕获循环中的值。
React18新特性
-
Concurrent Mode(并发模式):React 18 引入了 Concurrent Mode,这是一个可选的特性,可以帮助优化应用程序的性能和用户体验。通过 Concurrent Mode,React 可以更好地处理大型应用程序中的渲染优先级,并在不阻塞用户界面的情况下提高性能。
-
新的渲染器 Renderer:React 18 引入了新的渲染器,允许开发人员更好地控制渲染过程,并进行更细粒度的优化。
-
Automatic Batching(自动批处理):React 18 改进了更新批处理机制,使得在某些情况下不再需要手动进行批处理操作,从而提高了性能。
-
新的树形结构 Reconciler:React 18 中引入了新的树形结构 Reconciler,可以更好地处理组件树的更新和渲染,提高了整体的性能和效率。
-
新的事件系统:React 18 带来了全新的事件系统,使得事件处理更加灵活和高效。开发人员可以更容易地管理和优化事件处理逻辑。
React19新特性
新的Hooks:React 19引入了多个新的Hooks,如
use
、useOptimistic
、useFormState
和useFormStatus
。这些Hooks简化了数据获取和表单处理的复杂性。例如,useHook
允许开发者从Promise或Context中读取值,提供了一种新的“挂起”API,使得在等待异步操作完成时,UI可以更加友好地展示加载状态。而useOptimisticHook
使得开发者可以在等待服务器响应时,对UI进行乐观更新,提高了用户体验。编译器优化:React编译器的引入标志着React在性能优化方面迈出了重要一步。编译器能够通过对JS规则和React规则的建模,安全地编译代码,从而减少不必要的渲染。
数据获取和表单处理的改进:React 19通过新的Hooks和API,如
use
和form action
,提供了更直观和高效的方式来处理数据获取和表单提交。服务器组件(RSC):经过多年的开发,React引入了服务器组件,这是一个重大改进,使得开发者不再需要借助Next.js等其他工具来实现服务器端渲染。
动作(Action):动作也将彻底改变我们与DOM元素的交互方式。
文档元数据:这是一个备受期待的改进,让我们能够用更少的代码实现更多功能。
资源加载:这将使资源能够在后台加载,从而改善应用程序的加载时间和用户体验。React 19中,图像和其他文件将在用户浏览当前页面时在后台加载,这个改进应该有助于改善页面加载时间并减少等待时间。
Web Components:React代码现在可以让我们集成Web Components,这是一个特别令人着迷的特性。
useRef / ref / forwardsRef 的区别是什么?
ref
是 React 提供的一个属性,用于在组件中访问子组件或 DOM 元素。每次渲染都会被重新创建。useRef
是一个 Hook 函数,用于在函数组件中创建可变的 ref 对象。会因为重新渲染而改变。forwardRef
是一个高阶组件,用于在函数组件中向子组件传递 ref。
React 和 Vue 在技术层面有哪些区别?
-
语法和模板:
- React 使用 JSX(JavaScript XML)作为组件的声明语法,将 HTML 结构和 JavaScript 代码混合在一起。
- Vue 使用单文件组件(SFC)的方式,将模板、脚本和样式放在同一个文件中,使得代码更加模块化和清晰。
-
数据绑定:
- React 使用单向数据流,父组件通过 props 将数据传递给子组件,子组件需要通过回调函数来改变父组件的数据。
- Vue 使用双向数据绑定,通过
v-model
指令可以实现父子组件之间的数据双向绑定,简化了数据传递和状态管理。
-
状态管理:
- React 可以使用 Context API 或者第三方库(如 Redux、MobX)来进行状态管理,提供了更灵活的状态管理方式。
- Vue 提供了 Vuex 状态管理库,用于集中式存储管理应用的所有组件的状态。
-
组件通信:
- React 中组件之间的通信可以通过 props、Context API、回调函数等方式实现。
- Vue 中组件之间的通信可以通过 props、自定义事件、Vuex 等方式实现。
-
生命周期:
- React 组件的生命周期包括挂载、更新和卸载阶段,提供了一系列生命周期方法供开发者进行操作。
- Vue 组件也有生命周期钩子函数,但与 React 不同的是,Vue 的生命周期钩子函数更细粒度,例如
beforeCreate
、created
、beforeMount
、mounted
等。
-
虚拟 DOM:
- React 使用虚拟 DOM 来提高性能,通过比对虚拟 DOM 的差异来最小化页面重新渲染的开销。
- Vue 同样使用虚拟 DOM,但采用了模板编译的方式,将模板编译成渲染函数,提高了性能。
taro 的实现原理是怎么样的?
Taro 利用 Babel、React、Webpack 等技术,通过封装原生 API 和提供不同的 Polyfill 实现了多端适配,同时也支持复杂的样式表达和自动化导入组件等特性。Taro 的实现原理主要是通过源码转换、组件映射、API 封装和样式处理等方式,实现了多端统一开发的目标,
单页应用如何提高加载速度?
- 使用代码分割:将代码拆分成小块并按需加载(懒加载),以避免不必要的网络请求和减少加载时间。
- 缓存资源:利用浏览器缓存来存储重复使用的文件,例如 CSS 和 JS 文件、图片等。
- 预加载关键资源:在首次渲染之前,先提前加载关键资源,例如首页所需的 JS、CSS 或数据,以保证关键内容的快速呈现。
- 使用合适的图片格式:选择合适的图片格式(例如 JPEG、PNG、WebP 等),并根据需要进行压缩以减少文件大小。对于一些小图标,可以使用
iconfont
等字体文件来代替。 - 启用 Gzip 压缩:使用服务器端的 Gzip 压缩算法对文件进行压缩,以减少传输时间和带宽消耗。
- 使用 CDN:使用内容分发网络(CDN)来缓存和传递文件,以提高文件的下载速度和可靠性。
- 优化 API 请求:尽可能地减少 API 调用的数量,并使用缓存和延迟加载等技术来优化 API 请求的效率。
- 使用服务器端渲染:使用服务器端渲染(SSR)来生成 HTML,以减少客户端渲染所需的时间和资源。但需要注意,SSR 也可能增加了服务器的负担并使网站更复杂。
react-router 里的 <Link> 标签和 <a> 标签有什么区别?
-
性能优化:
<Link>
组件是 React-Router 提供的组件,它在用户点击时会使用 JavaScript 动态地改变 URL 而不会重新加载整个页面,从而实现单页应用的导航,避免了页面的完全刷新,提升了性能和用户体验。<a>
标签是 HTML 中的超链接标签,在用户点击时会导致整个页面重新加载或跳转到新的页面,这种方式会导致页面的重复加载和渲染,影响性能。
-
路由管理:
<Link>
组件与 React-Router 的路由管理机制结合,可以在单页应用中实现路由的切换、动态参数传递等功能,同时保持页面状态的稳定。<a>
标签通常用于传统的多页应用或简单的静态页面,点击后会直接加载新的页面或跳转到指定链接。
-
样式处理:
- 使用
<Link>
组件可以方便地进行样式控制,通过设置 activeClassName 等属性可以实现当前链接高亮显示等效果。 - 使用
<a>
标签需要自行处理样式,无法方便地根据路由状态来进行样式控制。
- 使用
说说你对React Router的理解?常用的Router组件有哪些?
react-router
等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面
路由的本质就是页面的URL
发生改变时,页面的显示结果可以根据URL
的变化而变化,但是页面不会刷新
主要是提供了一些组件:
- BrowserRouter、HashRouter
- Route
- Link、NavLink
- switch
- redirect
说说React Router有几种模式,以及实现原理?
-
Hash 模式:
- 在 Hash 模式下,URL 中会包含一个
#
符号,例如http://www.example.com/#/about
。 - 实现原理是基于浏览器对 URL 中哈希部分的变化不会引起页面刷新的特性。React Router 监听 URL 中哈希部分的变化,并根据哈希值匹配相应的路由进行页面更新。
- 这种模式兼容性好,可以在不支持 HTML5 History API 的浏览器上正常工作,但 URL 中包含
#
符号可能会被认为不够美观。
- 在 Hash 模式下,URL 中会包含一个
-
History 模式:
- 在 History 模式下,URL 不包含
#
符号,例如http://www.example.com/about
。 - 实现原理是利用 HTML5 History API 中的
pushState
和replaceState
方法,通过改变 URL 而不引起页面刷新来实现页面路由的切换。 - 这种模式生成的 URL 更加美观,但需要确保服务器端能够正确处理这些 URL,以避免在用户直接访问这些 URL 时出现 404 错误。
- 在 History 模式下,URL 不包含
使用 redux 有哪些原则?
- 单一数据源
- 状态是只读的
- 纯函数的 reducer
- 不要在 reducer 中执行异步操作
- 合理使用中间件
- 按照功能划分 reducer
- 避免过度使用 Redux
Redux 和 Vuex 有什么区别,它们有什么共同思想吗?
们的共同思想包括:
-
集中化的状态管理:Redux 和 Vuex 都提倡将应用程序的状态集中管理,以便于统一管理和跟踪状态的变化。
-
单向数据流:两者都采用了单向数据流的思想,即数据的流动是单向的,便于状态的追踪和调试。
-
纯函数:Redux 和 Vuex 都鼓励使用纯函数来处理状态的变化,使得状态的变化更加可预测和可控。
-
开发者工具:两者都提供了开发者工具,便于开发者监控状态变化、调试和时间旅行等功能。
它们的区别主要在于以下几点:
-
框架依赖:Redux 是一个独立的状态管理库,可以与任何框架结合使用,而 Vuex 是专门为 Vue.js 设计的状态管理库。
-
概念和API:Redux 使用了 action、reducer、store 等概念和 API,而 Vuex 使用了 mutation、action、state、getter 等不同的概念和 API。
-
语法差异:Redux 的语法比较简洁,但在某些情况下需要编写较多的模板代码;而 Vuex 在 Vue.js 中能够更好地利用框架的特性,提供了更加简洁的语法。
-
生态和社区:Redux 有着庞大的生态和社区支持,而 Vuex 则更加贴近 Vue.js 生态,能够更好地与 Vue.js 整合。
React 中怎么实现状态自动保存(KeepAlive)?
-
使用 Context API 或 Redux:
- 可以使用 React 的 Context API 或 Redux 来全局管理组件的状态。将需要缓存的组件状态存储在全局状态管理中,这样即使组件被卸载再重新挂载时,状态仍然可以被保留。
-
使用组件缓存技术:
- 可以自定义一个高阶组件(HOC)或 Hook,用于缓存组件的状态。当组件被卸载时,将状态保存到缓存中;当组件重新挂载时,从缓存中读取之前保存的状态。
-
使用 React Router 中的路由状态:
- 如果是基于 React Router 实现页面导航的应用,可以利用 React Router 提供的路由状态来实现状态自动保存。当路由切换时,可以在路由状态中存储和恢复组件状态。
-
利用 LocalStorage 或 SessionStorage:
- 可以使用浏览器提供的 LocalStorage 或 SessionStorage 来保存组件状态。在组件的生命周期方法中或者 useEffect 钩子中将状态保存到 LocalStorage 或 SessionStorage 中,在组件重新挂载时再从中读取状态。
-
利用第三方库:
- 也可以考虑使用像
react-keep-alive
这样的第三方库来实现状态自动保存的功能。这些库一般提供了简单易用的方式来实现组件状态的缓存和自动保存。
- 也可以考虑使用像
useEffect 与 useLayoutEffect 有什么区别?
-
useEffect:
useEffect
是 React 提供的标准 Hook,它会在浏览器渲染完成后异步执行副作用操作。这意味着它不会阻塞浏览器渲染,并且会在浏览器布局和绘制更新之后执行。- 通常用于处理数据获取、订阅或手动操作 DOM 等副作用操作,它不会阻塞浏览器的渲染过程。
-
useLayoutEffect:
useLayoutEffect
也是一个 React 提供的 Hook,与useEffect
类似,但它会在所有 DOM 变更之后同步执行副作用操作,但在浏览器布局和绘制之前执行。- 由于
useLayoutEffect
是在 DOM 更新之后、页面重新布局之前同步执行的,因此如果在其中执行大量计算或操作,可能会导致性能问题,甚至造成页面卡顿。
因此,主要区别在于执行时机和对页面布局的影响。一般情况下,推荐优先使用 useEffect
来处理副作用操作,只有在确实需要在布局更新之前立即执行代码时才考虑使用 useLayoutEffect
。
react中懒加载的实现原理是什么?
-
动态导入:
- 懒加载的关键在于使用动态导入来按需加载组件或其他资源。通过使用
import()
函数,可以在需要时动态加载模块,而不是在应用初始化时一次性加载所有模块。 - 例如,可以将组件的引入语句改为
const LazyComponent = React.lazy(() => import('./LazyComponent'))
,这样在需要时才会加载 LazyComponent 组件。
- 懒加载的关键在于使用动态导入来按需加载组件或其他资源。通过使用
-
React.lazy 和 Suspense:
- React 提供了
React.lazy
和Suspense
这两个懒加载相关的 API。React.lazy
函数接受一个函数,该函数应该返回一个动态 import 的 Promise,以实现组件的懒加载。 - 使用
Suspense
组件可以在等待懒加载组件加载完成时显示 loading 界面,以提升用户体验。在最外层包裹Suspense
组件,并设置fallback
属性为加载中时显示的组件,即可实现懒加载时的 loading 效果。
- React 提供了
-
代码分割:
- 除了懒加载组件外,还可以利用代码分割(Code Splitting)来拆分应用代码,减少初始加载时的体积。通过将应用代码分割成多个块,并在需要时动态加载这些块,可以进一步优化页面加载性能。
React有哪些性能优化的方法?
React 渲染性能优化的三个方向,其实也适用于其他软件开发领域,这三个方向分别是:
- 减少计算的量。 -> 对应到 React 中就是减少渲染的节点 或者 降低组件渲染的复杂度
- 利用缓存。-> 对应到 React 中就是如何避免重新渲染,利用函数式编程的 memo 方式来避免组件重新渲染
- 精确重新计算的范围。 对应到 React 中就是绑定组件和状态关系, 精确判断更新的'时机'和'范围'. 只重新渲染'脏'的组件,或者说降低渲染范围
- 使用Hooks,React.memo
- 避免不必要的重新渲染:避免在render函数中执行复杂的计算或操作,尽量将其移到组件的生命周期方法或其他地方执行。
- 使用组件的纯函数:将组件设计为纯函数,避免副作用和状态变化,可以更容易进行性能优化和测试。
- 使用虚拟化列表:对于长列表,可以使用虚拟化列表库如reacy-virtualized或react-window来减少DOM节点数量,提高性能。
- 使用key属性:在列表渲染时,为每个列表项添加唯一的key属性,可以帮助react更好地识别和更新组件。
react 的虚拟dom是怎么实现的?
通过 JavaScript 对象来模拟真实 DOM 的层次结构和状态,从而实现了对 DOM 的抽象和操作。虚拟 DOM 的实现可以简单概括为以下几个步骤:
-
创建虚拟 DOM 对象:
- 当使用 React 创建组件时,每个组件都会对应一个虚拟 DOM 对象。这个对象描述了组件在特定时间点的状态和结构。
-
渲染虚拟 DOM:
- 当组件状态发生变化或初始化时,React 会根据组件的 render 方法返回的 JSX 创建新的虚拟 DOM 对象。
-
对比更新:
- 当新的虚拟 DOM 对象产生后,React 会使用 diff 算法比较新旧虚拟 DOM 对象之间的差异,并找出最小的更新量。
-
应用更新:
- 经过对比后,React 会将变更部分应用到实际的 DOM 结构上,从而更新页面显示。
通过虚拟 DOM,React 实现了一种高效的页面更新机制。在组件状态发生变化时,React 首先更新虚拟 DOM,然后通过 diff 算法找出实际需要更新的部分,最终只更新实际 DOM 中发生变化的部分,避免了不必要的 DOM 操作,提高了页面渲染的效率。
为什么不能直接使用 this.state 改变数据?
setState通过一个队列机制来实现 state 更新。当执行 setState 的时候,会将需要更新的 state 合并后放入状态队列,而不会立刻更新 this.state。队列机制可以高效的批量更新 state,如果不通过 setState 而直接修改 this.state,那么该 state 将不会被放入状态队列中,当下次调用 setState 并对状态队列进行合并时,将会忽略之前被直接修改的 state,而造成无法预知的错误。