1:React中的类组件和函数组件之间有什么区别?
类组件(Class components)
- 无论是使用函数或是类来声明一个组件,它决不能修改它自己的 props。
- 所有 React 组件都必须是纯函数,并禁止修改其自身 props。
- React是单项数据流,父组件改变了属性,那么子组件视图会更新。
- 属性 props是外界传递过来的,状态 state是组件本身的,状态可以在组件中任意修改
- 组件的属性和状态改变都会更新视图。
-
1class Welcome extends React.Component { 2 render() { 3 return ( 4 <h1>Welcome { this.props.name }</h1> 5 ); 6 } 7} 8ReactDOM.render(<Welcome name='react' />, document.getElementById('root'));
函数组件(functional component)
函数组件接收一个单一的 props 对象并返回了一个React元素
1function Welcome (props) {
2 return <h1>Welcome {props.name}</h1>
3}
4ReactDOM.render(<Welcome name='react' />, document.getElementById('root'));
区别
- 语法上
两者最明显的不同就是在语法上,函数组件是一个纯函数,它接收一个props对象返回一个react元素。而类组件需要去继承React.Component并且创建render函数返回react元素,这将会要更多的代码,虽然它们实现的效果相同。
- 状态管理
因为函数组件是一个纯函数,你不能在组件中使用setState(),这也是为什么把函数组件称作为无状态组件。
如果你需要在你的组件中使用state,你可以选择创建一个类组件或者将state提升到你的父组件中,然后通过props对象传递到子组件。
- 生命周期钩子
你不能在函数组件中使用生命周期钩子,原因和不能使用state一样,所有的生命周期钩子都来自于继承的React.Component中。
因此,如果你想使用生命周期钩子,那么需要使用类组件。
注意:**在react16.8版本中添加了hooks,**使得我们可以在函数组件中使用useState钩子去管理state,使用useEffect钩子去使用生命周期函数。因此,2、3两点就不是它们的区别点。从这个改版中我们可以看出作者更加看重函数组件,而且react团队曾提及到在react之后的版本将会对函数组件的性能方面进行提升。
- 调用方式
如果SayHi是一个函数,React需要调用它:
1// 你的代码
2function SayHi() {
3 return <p>Hello, React</p>
4}
5// React内部
6const result = SayHi(props) // » <p>Hello, React</p>
如果SayHi是一个类,React需要先用new操作符将其实例化,然后调用刚才生成实例的render方法:
1// 你的代码
2class SayHi extends React.Component {
3 render() {
4 return <p>Hello, React</p>
5 }
6}
7// React内部
8const instance = new SayHi(props) // » SayHi {}
9const result = instance.render() // » <p>Hello, React</p>
可想而知,函数组件重新渲染将重新调用组件方法返回新的react元素,类组件重新渲染将new一个新的组件实例,然后调用render类方法返回react元素,这也说明为什么类组件中this是可变的。
react函数 组件为什么没有this
React函数组件没有this,因为是**纯粹的JavaScript函数,而不是类的实例。**因此,在函数组件中无法使用this来引用组件实例本身。相反,函数组件通过参数来接收props,并使用钩子函数来处理状态和副作用。这种设计使得函数组件更加简洁和灵活,同时也更符合函数式编程的思想。
2.react性能优化
优化 React 应用的性能是一个重要的任务,以下是一些优化 React 应用性能的常见方法:
-
代码优化:确保你的代码逻辑合理且高效。避免不必要的渲染和重复的计算。尽量使用 PureComponent 或者 React.memo 来避免不必要的组件重新渲染。
-
使用虚拟化列表:如果你的应用中有大量的列表数据,可以考虑使用虚拟化技术,例如 react-virtualized 或 react-window,来只渲染可见区域的列表项,减少 DOM 元素的数量。
-
懒加载组件:使用 React.lazy 和 Suspense 来实现组件的懒加载,只在需要的时候加载组件,减少初始渲染所需的代码量。
-
使用 memoization:通过使用 memoization 技术,例如使用 useMemo 或者 useCallback,可以缓存函数的计算结果,避免重复计算,提高性能。
-
使用 shouldComponentUpdate 或者 React.memo 进行组件更新控制:通过手动实现 shouldComponentUpdate 或者使用 React.memo,可以避免不必要的组件重新渲染。
-
使用生产环境优化工具:在生产环境中,使用构建工具(如Webpack)进行代码压缩、合并和分割,以减少文件大小和资源加载时间。
-
使用性能分析工具:使用性能分析工具,例如 React Profiler 或者 Chrome DevTools,来识别和解决应用中的性能问题。
-
避免在渲染期间进行昂贵的操作:在组件的 render 方法中避免执行昂贵的操作,如大量计算或者网络请求。可以将这些操作放到 componentDidMount 或 useEffect 中进行。
记住,优化性能是一个持续的过程,需要不断地检查和改善代码。根据你的具体应用场景和需求,还可能需要采取其他优化措施。
3.react PureComponent 和普通的Component 区别
简单来说,React 的 PureComponent 和普通的 Component 在更新机制上有以下区别:
-
PureComponent 继承自 React.Component,内部默认实现了 shouldComponentUpdate 方法。每次组件更新时,PureComponent 会自动进行浅层比较(shallow comparison)来判断 props 和 state 是否发生变化。只有当引用发生变化时,PureComponent 才会触发重新渲染。
-
普通的 Component 需要手动实现 shouldComponentUpdate 方法来控制组件的更新。在该方法中,开发者需要自行编写逻辑来比较新旧的 props 和 state 是否相等,以决定是否进行重新渲染。
简而言之,PureComponent 提供了一种方便的方式来自动进行浅层比较,以避免不必要的组件重新渲染。普通的 Component 则需要手动实现 shouldComponentUpdate 方法来控制组件更新。在使用 PureComponent 时,需要注意保证 props 和 state 的属性变化时都返回新的引用,以确保变化被正确检测到。
4.简单说下什么是纯函数
纯函数是指在相同的输入下,总是返回相同的输出,并且没有产生任何副作用的函数。它具有以下特点:
-
相同的输入始终产生相同的输出:无论何时调用纯函数,只要给定相同的输入,它们都会返回相同的输出值。这种可预测性使得纯函数更容易进行测试和推理。
-
没有副作用:纯函数不会对除函数内部作用域以外的状态进行修改,包括全局变量、传入的参数以及其他外部资源。纯函数不会改变传入的参数,也不会直接修改其引用。
-
只依赖于输入参数:纯函数的结果只取决于输入参数,不受外部环境的影响。同样的输入会得到同样的输出,不受程序的执行顺序或其他因素的影响。
纯函数的设计原则有助于代码的可维护性和可测试性。由于纯函数不依赖外部状态,因此更容易理解和推理函数的行为。纯函数还支持函数组合,可以通过将多个纯函数组合在一起来构建更复杂的功能。
在实际开发中,使用纯函数可以避免意外的副作用和不可预测的行为,提高代码的可靠性和可读性。它们常用于函数式编程范式中,也在许多框架和库中得到广泛应用。
在日常开发中,常用的纯函数库包括以下几个:
-
Lodash:Lodash 是一个 JavaScript 实用工具库,提供了许多常用的纯函数,包括对数组、对象、字符串、函数等的操作和处理。
-
Ramda:Ramda 是另一个 JavaScript 实用工具库,也提供了一系列常用的纯函数,其中强调了函数式编程的思想和概念,支持函数柯里化、自动柯里化以及函数组合等特性。
-
Immutable.js:Immutable.js 是一个持久化数据结构的库,它提供了不可变对象、集合、列表等数据结构,并提供了一些纯函数来对这些数据结构进行处理。通过使用 Immutable.js,可以减少因为引用类型数据修改带来的副作用,提高代码的健壮性和可维护性。
-
Ramdajs.com:RamdaJS提供很多可以链式调用的数据处理函数。这些函数都是不会改变原本数据的纯函数,方便我们做一些数据整理和后期计算的相关处理。
这些纯函数库提供了丰富的纯函数集合,可以大大简化我们的开发流程,使得代码更加易于维护和测试。
5.简单说下setsate什么情况下是同步 什么情况下是异步
在 React 中,通过 setState() 方法来更新组件的状态,setState() 有时会表现为同步更新,有时则是异步更新。
当 setState() 在 React 生命周期、合成事件和 **React.useEffect()** 时,它通常是异步的。这意味着即使多次调用 setState(),React 也会将它们合并成一次更新,从而提高性能。当然,React 还会根据需要执行其他优化,例如批量更新、延迟更新等。
但是,如果在 **setTimeout()**、**setInterval()** 或原生事件处理程序中调用 setState(),它将会是同步的,因为 React 无法控制这些代码的执行顺序。当 setState() 被调用时,React 会立即执行更新,而不是等待更新进行批处理。
react18之前。
setState在不同情况下可以表现为异步或同步。
在Promise的状态更新、js原生事件、setTimeout、setInterval..中是同步的。
在react的 React 生命周期、合成事件和 **React.useEffect()** 时,是异步的。
react18之后。
setState都会表现为异步(即批处理)。
需要注意的是,无论 setState() 是同步还是异步,它都会触发组件的重新渲染。当状态更新时,React 将自动重新渲染组件,并在必要时更新 DOM。但是,由于 setState() 可能是异步的,因此在更新状态后,不能立即假设组件的状态已经被更新,可以使用 setState() 的回调函数来获取更新后的状态。
综上所述,setState() 的同步和异步更新取决于更新的时间和位置,需要根据具体情况进行使用。尽可能避免在定时器或原生事件处理程序中调用 setState(),以避免不必要的副作用和错误。
当 setState() 在 React 生命周期或合成事件中被调用时,它通常是异步的。这意味着即使多次调用 setState(),React 也会将它们合并成一次更新,从而提高性能。当然,React 还会根据需要执行其他优化,例如批量更新、延迟更新等。
但是,如果在 setTimeout()、setInterval() 或原生事件处理程序中调用 setState(),它将会是同步的,因为 React 无法控制这些代码的执行顺序。当 setState() 被调用时,React 会立即执行更新,而不是等待更新进行批处理。
需要注意的是,无论 setState() 是同步还是异步,它都会触发组件的重新渲染。当状态更新时,React 将自动重新渲染组件,并在必要时更新 DOM。但是,由于 setState() 可能是异步的,因此在更新状态后,不能立即假设组件的状态已经被更新,可以使用 **setState()** 的回调函数来获取更新后的状态。
综上所述,setState() 的同步和异步更新取决于更新的时间和位置,需要根据具体情况进行使用。尽可能避免在定时器或原生事件处理程序中调用 setState(),以避免不必要的副作用和错误。
6.React Router有几种模式,以及实现原理?
React Router 提供了三种路由模式:BrowserRouter、HashRouter、MemoryRouter。
-
**BrowserRouter**使用 HTML5 的新特性 History API 来实现路由,它在浏览器环境中表现得像一个标准的 React 应用程序。通过使用 HTML5 History API,BrowserRouter可以让我们在不进行页面刷新的情况下动态更新 URL。 -
**HashRouter**为旧版本的浏览器提供了一种实现路由的方式,它使用 URL 的哈希值(#)来实现路由。每当 URL 的哈希值发生变化时,HashRouter就会根据哈希值来匹配相应的路由。 -
**MemoryRouter**不会将 URL 存储在地址栏中,而是将 URL 存储在内存中。它主要用于测试和非浏览器环境(如 React Native)中使用。
React Router 的实现原理是基于 React 的组件化架构和上下文(Context)提供的能力。在 React Router 中,我们可以使用路由器组件(例如 BrowserRouter、HashRouter、MemoryRouter)来包装我们的应用程序,并使用路由组件(例如 Route)来定义应用程序的路由规则。
当我们的应用程序的 URL 发生变化时,React Router 会通过上下文(Context)来传递路由信息给相应的组件,然后渲染对应的组件。
React Router 还提供了一些辅助组件和钩子函数,例如 Link、Switch、Redirect、useParams、useLocation 等,来帮助我们实现更高级的路由功能。这些组件和钩子函数都是基于 React 组件和上下文(Context),通过向组件传递相应的路由信息来实现的。
总的来说,React Router 的实现原理是基于 React 的组件化架构和上下文(Context)提供的能力,通过路由器组件和路由组件来定义应用程序的路由规则,使用辅助组件和钩子函数来实现更高级的路由功能。
7.简单说下react 中怎么捕获异常
在 React 中捕获异常可以通过使用错误边界(Error Boundary)来实现。
错误边界是一种特殊的 React 组件,用于捕获并处理其子组件中抛出的 JavaScript 错误。通过将错误边界组件包装在需要进行错误捕获的组件周围,可以在组件树中的指定位置捕获错误。
要创建一个错误边界组件,需要定义一个类组件,并实现 **componentDidCatch**(error, errorInfo) 方法。这个方法会在其子组件抛出错误时被调用,接受两个参数:error 表示抛出的错误对象,errorInfo 包含有关错误的组件栈信息。
jsxCopy Codeclass ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, errorInfo) {
// 处理错误信息
console.log(error, errorInfo);
this.setState({ hasError: true });
}
render() {
if (this.state.hasError) {
// 当发生错误时渲染自定义的错误界面
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
要使用错误边界组件,只需将其嵌套在希望捕获错误的组件周围即可:
jsxCopy Code<ErrorBoundary>
<ComponentThatMayThrowError />
</ErrorBoundary>
当 ComponentThatMayThrowError 抛出错误时,错误边界组件会捕获到该错误,并触发 componentDidCatch 方法。在该方法中,你可以根据需要处理错误,例如显示自定义的错误信息或记录错误日志。
需要注意的是,错误边界仅能捕获其子组件中的错误,而不能捕获其自身或其他父级组件的错误。每个错误边界组件只能处理其子组件的错误。
另外,错误边界只能捕获在渲染期间抛出的错误,无法捕获以下情况的错误:
- 事件处理器中的错误(例如 onClick、onChange)
- 异步代码(例如定时器、Promise)
- 服务端渲染期间的错误
- 错误边界本身抛出的错误
总结来说,在 React 中捕获异常可以通过使用错误边界(Error Boundary)来实现。通过创建一个错误边界组件并使用它包装需要进行错误捕获的组件,可以在组件树中的指定位置捕获并处理 JavaScript 错误。
7.简述下 React 的事件代理机制?
React 并不会把所有的处理函数直接绑定在真实的节点上。而是把所有的事件绑定到结构的最外层,使用一个统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。
当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象。
当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。
这样做的优点是解决了兼容性问题,并且简化了事件处理和回收机制(不需要手动的解绑事件,React 已经在内部处理了)。但是有些事件 React 并没有实现,比如 window 的 resize 事件。
React 事件代理机制是指 React 通过在组件树的根节点上注册事件监听器,从而实现了一种事件委托(Event Delegation)的机制。这个机制可以帮助我们避免在每个子组件上都注册事件监听器,提高了性能和代码的可维护性。
在 React 中,当我们为子组件添加事件处理函数时,事件处理函数会被添加到祖先元素中,而不是直接添加到子组件上。当事件触发时,React 会在组件树中找到最近的公共祖先元素,并调用该元素上对应的事件处理函数。然后,React 会根据事件的类型和目标元素来判断需要向哪个子组件派发事件。
例如,如果我们在一个父组件中添加了一个点击事件处理函数,并将这个父组件渲染为以下结构:
jsxCopy Code<div onClick={handleClick}>
<ChildComponent />
</div>
那么当用户点击 ChildComponent 中的某个元素时,React 会根据事件类型和目标元素来判断需要向哪个子组件派发事件。如果目标元素是 ChildComponent 中的某个元素,那么 React 会向该元素派发事件;否则,React 会向 ChildComponent 组件本身派发事件。
React 的事件代理机制还可以支持事件的冒泡(Bubbling)和捕获(Capturing)。在事件捕获阶段,React 会从祖先元素向下遍历组件树,直到找到触发事件的目标元素。在事件冒泡阶段,React 会从目标元素向上遍历组件树,直到找到最近的公共祖先元素。
总的来说,React 的事件代理机制可以帮助我们避免在每个子组件上都注册事件监听器,提高了性能和代码的可维护性。通过在组件树的根节点上注册事件监听器,并根据事件类型和目标元素来判断需要向哪个子组件派发事件,React 实现了一种高效的事件委托机制。
8.说说React服务端渲染怎么做?原理是什么?
React 服务端渲染是指在服务器端将组件渲染为 HTML 字符串,再将其发送给客户端。这种做法可以提高首屏加载速度、SEO 友好等优点。
- 手动搭建一个 SSR 框架
- 使用成熟的SSR 框架,如 Next.JS
React 服务端渲染的实现原理可以概括为以下几个步骤:
-
接收请求:当服务器接收到客户端的请求时,会根据请求的 URL 和路由信息来确定需要渲染哪个组件。
-
渲染组件:服务器端使用 React 的 renderToString 方法将组件渲染为 HTML 字符串。
-
加载数据:如果组件需要异步加载数据,服务器端需要在渲染之前先加载所需的数据,并将数据传递给组件。
-
发送响应:将渲染好的 HTML 字符串作为响应发送给客户端。
-
客户端挂载:客户端通过将服务器端渲染好的 HTML 字符串插入到页面中,使得页面能够正常显示。
React 服务端渲染的优点在于可以提高网站的性能和用户体验。由于服务端可以在渲染之前预先加载组件所需的数据,因此可以避免在客户端渲染时出现闪烁或者空白的情况。另外,由于服务端渲染可以将组件直接渲染为 HTML 字符串,因此可以提高网站的 SEO 友好度,使得搜索引擎更容易抓取和索引页面。
需要注意的是,React 服务端渲染也会带来一些额外的复杂性。由于服务端和客户端需要共享组件的代码和状态,因此需要进行额外的配置和处理。另外,由于服务端渲染会增加服务器的负担,因此需要对服务器进行优化和调整,以便提供更好的性能和用户体验。
总结来说,React 服务端渲染可以提高网站的性能和用户体验,同时也可以提高网站的 SEO 友好度。在实现上,React 服务端渲染需要通过将组件渲染为 HTML 字符串,并在客户端挂载时插入到页面中的方式来实现。但需要注意的是,服务端渲染也会带来一些额外的复杂性和需要进行额外的处理。
10. redux 三大原则?
Redux 是一种用于管理应用程序状态的 JavaScript 库,它基于以下三个原则:
-
单一数据源(Single Source of Truth): Redux 将应用程序状态存储在一个单一的 JavaScript 对象中,称为 Store。这个对象包含了整个应用程序的状态,并且所有组件都从这个对象中获取数据。
-
状态是只读的(State is read-only): 不能直接修改状态,而是需要通过 dispatch 一个 action 来触发状态的更新。这个 action 是一个描述状态变化的简单对象,包含一个 type 属性和一些附加数据。
-
状态修改通过纯函数完成(Changes are made with pure functions): 为了描述状态的变化,需要编写 reducers,reducers 是纯函数,接收旧的状态和一个 action,并返回新的状态。由于 reducers 是纯函数,因此它们不会修改原始状态,而是返回一个新的状态对象。
这三个原则共同确保了 Redux 应用程序的可预测性和可维护性。通过将整个应用程序的状态存储在一个单一的对象中,Redux 可以提供强大的开发者工具来追踪状态的变化和调试应用程序。同时,通过将状态修改限制到纯函数中,Redux 可以避免意外的副作用和数据变异,使得代码更加易于理解和维护。
总之,Redux 的设计思想是简单且可预测的,它可以帮助开发者更好地管理应用程序状态,提高开发效率和代码质量。
11.简单介绍下redux
Redux 是一个用于 JavaScript 应用程序状态管理的可预测性状态容器。它可以帮助开发者有效地管理应用程序中的数据和状态,使得状态的变化更加可控和可维护。
Redux 的核心概念包括:
-
Store:存储应用程序的整个状态树。在 Redux 中,唯一能改变状态的方法是触发一个 action,并通过 reducers 更新状态。开发者可以通过
createStore方法来创建一个 Redux 的 store。 -
Action:描述状态变化的普通 JavaScript 对象。它必须包含一个
type属性来表示动作的类型,以及一些额外的数据作为动作的载荷。通过dispatch方法将 action 分发给 reducers。 -
Reducer:纯函数,接收旧的状态和一个 action,并返回新的状态。它定义了如何根据不同的 action 类型来更新应用程序的状态。Redux 中的 reducer 遵循“状态不可变”的原则,即永远不要直接修改状态,而是通过返回新的状态对象来实现。
-
Middleware:位于 action 发起之后、reducer 处理之前的扩展点。它可以用于处理副作用、异步操作、日志记录等。Redux 提供了一些中间件,如 redux-thunk 和 redux-saga,也支持自定义中间件。
Redux 的工作流程可以概括为以下几个步骤:
-
创建一个 Redux 的 store,将根 reducer 传入 createStore 方法中。
-
定义 action,描述状态的变化。
-
创建 reducers,根据不同的 action 类型来更新状态。
-
将 reducers 注册到 store 中,使其能够响应 action。
-
在组件中使用
connect方法连接 store 和组件,通过mapStateToProps和mapDispatchToProps将状态和动作映射到组件的 props 上。 -
在组件中发起 action,调用
dispatch方法将 action 分发给 reducers。 -
reducers 根据 action 的类型更新状态。
-
组件接收到新的状态,并根据新的状态重新渲染。
Redux 的优点在于它提供了一种可预测性的状态管理方案,使得状态的流动变得清晰可见。它适用于中大型的应用程序,尤其是涉及到复杂的数据流和状态共享的场景。同时,Redux 也具备强大的开发者工具支持,可以帮助开发者更好地理解和调试应用程序的状态变化。
需要注意的是,Redux 并不是必须的,对于简单的应用程序或者组件之间状态的传递,可以使用 React 的内部状态管理即可。Redux 更适合于需要全局状态管理,或者多个组件之间需要共享状态的场景。
12.说说对Redux中间件(redux通常同步,用于异步需要中间件)的理解?常用的中间件有哪些?实现原理?
Redux整个工作流程,当action发出之后,reducer立即算出state,整个过程是一个同步的操作
那么如果需要支持异步操作,或者支持错误处理、日志监控,这个过程就可以用上中间件Redux中,中间件就是放在就是在**dispatch**过程,在分发**action**进行拦截处理,如下图:
预览
其本质上一个函数,对store.dispatch方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能
二、常用的中间件
有很多优秀的redux中间件,如:
-
redux-thunk:用于异步操作
-
redux-logger:用于日志记录
怎么取react-redux store的数据
在 React 应用中使用 Redux 管理状态时,通常会搭配使用 react-redux 库来连接 Redux store 和 React 组件。要从 react-redux 的 store 中获取数据,可以使用 useSelector 钩子函数或者 connect 函数来实现。
使用 useSelector 钩子函数(适用于函数组件):
import { useSelector } from 'react-redux';
// 在函数组件中使用 useSelector
const MyComponent = () => {
const data = useSelector(state => state.myReducer.data);
// 通过 useSelector 获取 store 中的数据
return (
<div>
{/* 在组件中使用获取到的数据 */}
<p>{data}</p>
</div>
);
};
在上面的代码中,我们使用 useSelector 钩子函数从 Redux store 中选择所需的数据,并将其存储在变量 data 中。然后在组件中可以直接使用这个数据。
使用 connect 函数(适用于类组件):
如果你使用的是类组件,可以通过 connect 函数来连接 Redux store 和组件:
import { connect } from 'react-redux';
import React, { Component } from 'react';
class MyComponent extends Component {
render() {
return (
<div>
{/* 在组件中使用 this.props 中的获取到的数据 */}
<p>{this.props.data}</p>
</div>
);
}
// 使用 connect 连接 Redux store 和组件
const mapStateToProps = state => {
return {
data: state.myReducer.data // 从 store 中获取所需的数据
};
};
export default connect(mapStateToProps)(MyComponent);
在上面的示例中,我们使用 connect 函数来将 Redux store 中的数据映射到组件的 props 上,然后在组件中可以通过 this.props.data 来访问获取到的数据。
无论是使用 useSelector 还是 connect,都可以让你方便地从 Redux store 中获取所需的数据,并在组件中进行使用。根据你的项目使用的是函数组件还是类组件,选择合适的方式来获取 Redux store 中的数据。
13.使用 React hooks 怎么实现类里面的所有生命周期?
具体来说,可以使用以下 React hooks 实现类里面的所有生命周期:
-
constructor:使用
useState方法或useReducer方法来初始化组件状态,或者通过useRef方法来创建 ref 对象。 -
componentDidMount:使用
useEffect方法,第二个参数传空数组([]),表示只在挂载时执行一次。在其中可以执行一些副作用操作,比如发送请求、订阅事件等。 -
componentDidUpdate:同样使用
useEffect方法,第二个参数传入一个数组,指定需要监听的变量。当数组中的变量发生变化时,该 hook 函数会被调用,可以进行一些更新操作。 -
componentWillUnmount:使用
useEffect方法,返回一个清除函数,在组件被卸载时执行。 -
shouldComponentUpdate:使用
React.memo方法对组件进行包裹,它会根据 props 和 state 是否发生变化来判断是否重新渲染组件。如果没有变化,则不会重新渲染组件,从而避免不必要的性能开销。 -
getDerivedStateFromProps:使用
useState方法或useReducer方法来初始化组件状态,或者使用useEffect方法监听 props 的变化,并在其中更新状态。 -
componentDidCatch:使用
useErrorBoundary方法或者自定义 error boundary,用于处理组件中可能出现的错误。
需要注意的是,使用 React hooks 实现生命周期函数时,并不需要一一对应。相比于类组件中的生命周期函数,React hooks 的钩子函数更为灵活,可以根据实际需要选择使用。同时,React hooks 也具有更为简洁和易于维护的优势。
注意事项
- useState 只在初始化时执行一次,后面不再执行;
- useEffect 相当于是 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合,可以通过传参及其他逻辑,分别模拟这三个生命周期函数;
- useEffect 第二个参数是一个数组,如果数组为空时,则只执行一次(相当于componentDidMount);如果数组中有值时,则该值更新时,useEffect 中的函数才会执行;如果没有第二个参数,则每次render时,useEffect 中的函数都会执行;
- React 保证了每次运行 effect 的同时,DOM 都已经更新完毕,也就是说 effect 中的获取的 state 是最新的,但是需要注意的是,effect 中返回的函数(其清除函数)中,获取到的 state 是更新前的。
- 传递给 useEffect 的函数在每次渲染中都会有所不同,这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。
- effect 的清除阶段(返回函数)在每次重新渲染时都会执行,而不是只在卸载组件的时候执行一次。它会在调用一个新的 effect 之前对前一个 effect 进行清理,从而避免了我们手动去处理一些逻辑 。为了说明这一点,下面按时间列出一个可能会产生的订阅和取消订阅操作调用序列:
14.React.memo() 和 useMemo() 的用法是什么,有哪些区别.
React.memo() 和 useMemo() 都是 React 中的钩子函数,用于优化组件性能和避免不必要的重新渲染。它们的用法和作用可以简单概括如下:
- React.memo():
- 用法:React.memo() 是一个高阶组件(Higher-Order Component),用于对函数组件进行记忆化(memoization)。通过将组件包裹在 React.memo() 函数中,可以避免在组件的 props 没有变化时重新渲染组件。
- 作用:React.memo() 会比较前后两次渲染时 props 是否发生变化。如果所有的 props 都没有变化,那么组件就不会重新渲染,直接使用之前的渲染结果。这样可以减少不必要的重复渲染,提升组件性能。
- useMemo():
- 用法:useMemo() 是一个 hook 函数,用于在函数组件中进行记忆化计算。通过传入一个回调函数和一个依赖项数组,可以缓存计算的结果,并在依赖项发生变化时重新计算。
- 作用:useMemo() 的回调函数会在组件重新渲染时执行,并返回计算的结果。如果依赖项数组中的任一元素发生变化,那么回调函数会重新执行,否则会使用上一次缓存的结果。这样可以避免耗时的重复计算,优化性能。
区别:
- React.memo() 适用于优化函数组件的渲染,只能控制组件是否重新渲染。
- useMemo() 适用于在函数组件中进行记忆化计算,可以精确控制何时重新计算和使用缓存的结果。
- React.memo() 是一个高阶组件,需要将组件包裹在 React.memo() 中。
- useMemo() 是一个 hook 函数,直接在函数组件中使用,并传入依赖项数组。
- React.memo() 主要通过比较 props 是否变化来决定是否重新渲染组件。
- useMemo() 主要通过比较依赖项数组的变化来决定是否重新计算和使用缓存结果。
综上所述,React.memo() 和 useMemo() 都是优化性能的工具,但用途和作用略有不同。React.memo() 用于控制组件的重新渲染,而 useMemo() 用于缓存计算结果并避免重复计算。根据具体场景选择合适的工具可以提升 React 应用的性能。
15.React Hooks 在使用上有哪些限制
React Hooks 的限制主要有两条:
- 不要在循环、条件或嵌套函数中调用 Hook;
- 在 React 的函数组件中调用 Hook。
具体在使用 React Hooks 过程中,有一些限制和注意事项需要遵守。以下是 React Hooks 使用上的一些主要限制:
-
**只能在函数组件中使用:**React Hooks 只能在函数组件(Function Component)中使用,不能在类组件中使用。这是因为 Hooks 是基于函数组件的新特性。
-
只能在顶层调用:Hooks 的调用必须在函数组件的顶层作用域中进行,不能出现在条件语句、循环或嵌套函数中。这是为了确保 Hooks 的调用顺序在每次渲染时保持一致。
-
Hooks 的调用顺序必须保持一致:在同一个组件内,Hooks 的调用顺序必须在每次渲染时保持一致。换句话说,不能根据条件动态调用 Hooks,而应该按照固定的顺序调用。
-
只能在 React 函数组件中使用特定的 Hooks:不是所有的 Hooks 都可以在任意函数组件中使用。某些 Hooks 有特定的使用条件和限制,比如 useState() 只能在 React 函数组件中调用,useEffect() 只能在 React 函数组件中调用等等。
-
自定义 Hooks 的命名规范:自定义 Hooks 的命名必须以 "use" 开头,这是为了明确标识它们是 Hooks。同时,自定义 Hooks 应该是可重用的,不应该依赖于特定组件的状态。
-
不能在循环和嵌套函数中直接使用 Hooks:由于 Hooks 需要按照一定顺序执行,不能在循环和嵌套函数中直接使用 Hooks。如果需要在循环或嵌套函数中使用 Hooks,可以通过自定义 Hooks 来实现。
-
Hooks 和闭包的注意事项:由于函数组件每次渲染都会创建新的函数实例,因此在使用 Hooks 时需要特别注意闭包的问题。确保正确地捕获和更新 Hooks 的依赖。
需要注意的是,以上仅是对 React Hooks 使用上的一些主要限制进行了概述,并不是所有的限制都在其中。在实际使用时,建议参考 React 官方文档和相关资源,了解更多关于 React Hooks 的规范和最佳实践。
16.简单说说说说对受控组件和非受控组件的理解,以及应用场景?
受控组件和非受控组件是 React 中用于处理表单元素的两种常见方式。
- 受控组件(Controlled Components): 受控组件是指由 React 组件完全控制的表单元素。在受控组件中,表单元素的值(如输入框的内容、复选框的选中状态等)由 React 组件的状态(state)来管理,并通过事件处理函数进行更新。每当用户与受控组件交互时,React 组件会更新相应的状态,并重新渲染组件,从而保持表单元素的值与组件状态同步。
应用场景:
- 当需要对表单元素的值进行验证或者处理时,可以使用受控组件。例如,在保存表单数据之前验证用户输入的有效性。
- 当需要动态地根据状态来控制表单元素的可用性、显示状态等属性时,受控组件也非常有用。
- 非受控组件(Uncontrolled Components): 非受控组件是指直接从 DOM 获取表单元素的值,而不是通过 React 组件的状态来管理。在非受控组件中,表单元素的值可以由 DOM 自身维护和修改,而不需要额外的事件处理函数和状态更新。
应用场景:
- 当表单较为简单且不需要进行额外的验证或处理时,可以使用非受控组件来简化代码。
- 当需要操作大量表单元素时,非受控组件可以提供更好的性能,因为不需要为每个表单元素都创建事件处理函数和状态。
需要注意的是,受控组件和非受控组件并没有绝对的优劣之分,选择哪种方式取决于具体的需求和场景。在实际开发中,根据表单复杂度、验证逻辑、性能要求等因素进行选择,或者根据不同的表单元素使用不同的方式也是可行的。
17.简单说下为什么 useState 返回的是数组而不是对象
React 中,useState 是 React Hooks 的一部分,它是用于在函数组件中存储和更新状态的方法。useState 函数返回一个由当前状态值和一个函数来更新该状态值的函数组成的数组。这个数组的结构可以通过数组解构来获取,例如:
Copy Codeconst [count, setCount] = useState(0);
其中 count 是当前状态值,setCount 是更新状态值的函数。
那么为什么 useState 返回数组而不是对象呢?这是因为 React Hooks 的设计理念是尽可能保持简单和纯粹,而数组比对象更加轻量级和简单。另外,数组解构也比对象解构更加常见和易于理解。
此外,使用数组作为状态值和更新函数的容器还有以下优点:
-
数组解构语法更加简洁易懂,可以直接获取状态值和更新函数。
-
数组可以保证状态值和更新函数之间始终存在对应关系,方便调试和维护。
-
数组可以保证顺序一致性,确保每次渲染时状态值和更新函数的顺序都是固定的。
需要注意的是,虽然 useState 返回的是数组,但是数组的顺序是固定的,即第一个元素为状态值,第二个元素为更新函数。在实际使用时,建议使用数组解构来获取状态值和更新函数,以保证代码的可读性和可维护性。
18.简单概括下React 组件间怎么进行通信?
在 React 中,组件间的通信可以通过以下几种方式进行:
-
Props(属性): 父组件可以通过给子组件传递 props(属性)的方式与子组件进行通信。父组件可以将数据或函数作为 props 传递给子组件,子组件可以通过 props 来访问传递过来的数据或调用传递过来的函数。
-
Callbacks(回调函数): 父组件可以定义回调函数,并将其作为 props 传递给子组件。子组件可以通过调用该回调函数来向父组件传递数据或触发特定的行为。
-
Context(上下文): Context 是一种在组件之间共享数据的方式,适用于跨层级的组件通信。通过创建一个 Context 对象,并在组件树中使用 Provider 和 Consumer 来提供和消费共享的数据。
-
Redux 或其他状态管理库: Redux 是一种常用的状态管理库,它可以帮助在组件间共享和管理状态。通过在 Redux 存储中定义和更新状态,组件可以从中获取所需的状态数据。
-
Pub/Sub 模式(发布-订阅模式): 使用 Pub/Sub 模式,可以通过事件的发布和订阅来实现组件间的通信。一个组件可以发布一个事件,其他组件可以订阅该事件并对其作出响应。
需要根据具体的场景和需求选择适当的通信方式。对于简单的父子组件通信,使用 props 和回调函数是常见的做法。而对于跨层级的组件通信或复杂的状态管理,可以考虑使用 Context 或状态管理库。
19.setState 之后发生了什么
简单版本: React 利用状态队列机制实现了 setState 的“异步”更新,避免频繁的重复更新 state。
首先将新的 state 合并到状态更新队列中,然后根据更新队列和 shouldComponentUpdate 的状态来判断是否需要更新组件。
调用 setState 后,React 将会进行以下步骤:
-
触发状态更新: 调用
setState后,React 将标记组件为“脏”(dirty),表示需要进行更新。 -
合并状态对象: 如果
setState的参数是一个对象,React 将会将这个对象合并到组件的当前状态中。 -
触发重新渲染: React 会触发组件的重新渲染过程。
-
调用
render方法: 重新渲染时,React 将调用组件的render方法,得到新的虚拟DOM。 -
比较与更新: React 将新生成的虚拟DOM与上一次的虚拟DOM进行比较,找出差异。
-
更新实际DOM: 根据差异,React 将只更新实际DOM中发生变化的部分,而不是整个组件。
-
触发生命周期方法: 如果定义了生命周期方法,例如
componentDidUpdate,这些方法将被调用。
复杂版本:
- enqueueSetState 将 state 放入队列中,并调用 enqueueUpdate 处理要更新的 Component
- 如果组件当前正处于 update 事务中,则先将 Component 存入 dirtyComponent 中。否则调用batchedUpdates 处理。
- batchedUpdates 发起一次 transaction.perform() 事务
- 开始执行事务初始化,运行,结束三个阶段
- 初始化:事务初始化阶段没有注册方法,故无方法要执行
- 运行:执行 setSate 时传入的 callback 方法
- 结束:更新 isBatchingUpdates 为 false,并执行 FLUSH_BATCHED_UPDATES 这个 wrapper 中的close方法,FLUSH_BATCHED_UPDATES在close阶段,会循环遍历所有的 dirtyComponents,调用updateComponent 刷新组件,并执行它的 pendingCallbacks, 也就是 setState 中设置的 callback。
20.简单介绍下React中的 diff 算法
在 React 中,Virtual DOM 与实际 DOM 的差异是通过 diff 算法来计算和处理的。diff 算法用于比较前后两个 Virtual DOM 树的差异,并最小化对实际 DOM 的操作,从而提高性能。
React 中的 diff 算法有以下几个特点:
-
优化更新: 为了提高性能,React 使用了一种称为 "Reconciliation" 的优化策略。在进行 diff 比较时,React 首先会比较新旧 Virtual DOM 树的根节点,如果它们不同,React 将完全替换整个子树。但是,如果根节点相同,React 将比较子节点并进行进一步细化的比较。
-
Diff 策略: React 采用了三个主要的 diff 策略,即同级比较、唯一 key 和列表遍历。
-
同级比较:React 在比较同级组件时,会通过比较组件类型和属性来决定是否需要更新该组件。如果组件类型或属性发生变化,React 将卸载旧组件,创建新组件,并将其插入到正确的位置。
-
唯一 key:在列表渲染时,React 需要每个列表项都有一个唯一的 key 属性。这样 React 可以通过比较新旧列表中的 key 来确定哪些列表项需要被更新、删除或添加,而不是重新渲染整个列表。
-
列表遍历:React 在比较列表时,会尽可能地复用节点。它会通过比较新旧列表项的 key 和顺序来决定是否需要重新渲染该项,而不是直接替换整个列表。
- Diff 算法的复杂度: React 的 diff 算法具有线性时间复杂度,即 O(n)。这是因为 React 在进行 diff 比较时,会使用一些启发式算法和优化策略,以减少比较的次数和提升性能。
总结起来,React 的 diff 算法通过比较前后两个 Virtual DOM 树的差异,并最小化对实际 DOM 的操作,从而实现高效的更新和渲染。它采用了同级比较、唯一 key 和列表遍历等策略,以及一些优化措施,以提供快速且高效的组件更新过程。
21.为什么不能直接使用 this.state 改变数据?
在 React 中,不能直接使用 this.state 来改变组件的状态数据。这是因为 React 需要对组件的状态变化进行跟踪和管理,以便正确地触发组件的更新和重新渲染。
直接修改 this.state 的值可能会导致以下问题:
-
不会触发重新渲染: React 无法检测到状态的变化,因此组件不会触发重新渲染。React 的更新机制是基于 Virtual DOM 和 diff 算法的,它需要比较前后两个状态来确定是否需要重新渲染组件。如果直接修改
this.state,React 无法捕捉到状态的变化,从而无法正确地更新组件。 -
组件状态不可控: React 鼓励使用不可变数据(Immutable data)的概念来管理组件的状态。通过创建新的状态对象并将其传递给
setState方法,可以确保组件的状态是不可变的。这样可以避免出现副作用和意外的行为,同时也有助于优化性能。 -
异步更新问题:
setState方法是异步执行的,它会将状态更新请求添加到一个队列中,并在适当的时机批量处理更新,以提高性能。如果直接修改this.state,则可能会绕过setState的异步更新机制,导致不一致的状态。
为了正确地更新组件的状态,应该使用 setState 方法来改变数据。setState 接受一个新的状态对象或一个返回状态对象的函数作为参数,并在适当的时机触发组件的更新和重新渲染。通过使用 setState,React 可以跟踪状态的变化,并在必要时更新组件以保持界面的同步和一致性。
22.什么是高阶组件?
高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。基本上,这是从React的组成性质派生的一种模式,我们称它们为“纯”组件, 因为它们可以接受任何动态提供的子组件,但它们不会修改或复制其输入组件的任何行为。
1const EnhancedComponent = higherOrderComponent(WrappedComponent);
- 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧
- 高阶组件的参数为一个组件返回一个新的组件
- 组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件
以下是高阶组件常见的应用场景:
-
代码复用: 高阶组件可以帮助我们在多个组件之间共享相同的逻辑和功能。通过将这些重复的逻辑提取到一个高阶组件中,可以避免代码的重复编写,提高代码的可维护性和可重用性。
-
权限控制: 高阶组件可以用于实现对组件的权限控制。例如,可以创建一个验证用户身份的高阶组件,当用户没有登录或没有相应权限时,可以选择是否渲染原始组件或者显示一个提示页面。
-
数据获取与管理: 高阶组件可以用于处理数据获取和管理的逻辑。例如,可以创建一个用于获取和管理数据的高阶组件,将数据作为 prop 传递给包装的组件,以便在组件中使用这些数据进行渲染和操作。
-
渲染劫持: 高阶组件可以用于修改组件的渲染方式。通过包裹组件并在渲染过程中添加、修改或删除组件的 props,可以对组件进行自定义渲染。例如,可以创建一个高阶组件来实现懒加载、错误边界等功能。
-
性能优化: 高阶组件可以用于性能优化,例如通过 memoization(记忆化)技术缓存计算结果,避免不必要的重复计算。这可以帮助在某些情况下提高组件的渲染性能。
需要注意的是,高阶组件不是 React 的官方概念,而是一种常见的设计模式。在使用高阶组件时,应该注意保持代码的可读性和可维护性,避免过度使用和滥用高阶组件。
23.React 中,怎么实现父组件调用子组件中的方法()?
要实现父组件调用子组件中的方法,需要通过以下步骤进行操作:
-
在子组件中,创建一个公开的方法。这可以通过在子组件类中定义一个方法或者使用 React Hooks 中的
useImperativeHandle来实现。-
如果是类组件,可以在子组件类中定义一个方法,并将其挂载到实例上。例如:
1 class ChildComponent extends React.Component { 2 childMethod() { 3 // 子组件中需要执行的操作 4 } 5 6 render() { 7 // 子组件的渲染逻辑 8 } 9} -
如果是函数式组件,可以使用
**useImperativeHandle**Hook 将指定的方法暴露给父组件。例如:1import { forwardRef, useImperativeHandle } from 'react'; 2 3function ChildComponent(props, ref) { 4 useImperativeHandle(ref, () => ({ 5 childMethod() { 6 // 子组件中需要执行的操作 7 } 8 })); 9 10 // 子组件的渲染逻辑 11} 12 13export default forwardRef(ChildComponent);
-
-
在父组件中,首先引用或创建对子组件的引用。可以使用
**ref**对象来保存对子组件的引用。-
如果是类组件,可以使用
**createRef**创建一个ref对象,并将其传递给子组件的refprop。例如:1class ParentComponent extends React.Component { 2 constructor(props) { 3 super(props); 4 this.childRef = React.createRef(); 5 } 6 7 handleClick() { 8 // 调用子组件的方法 9 this.childRef.current.childMethod(); 10 } 11 12 render() { 13 return ( 14 <div> 15 <ChildComponent ref={this.childRef} /> 16 <button onClick={() => this.handleClick()}>调用子组件方法</button> 17 </div> 18 ); 19 } 20} -
如果是函数式组件,可以使用
**useRe**f创建一个ref对象,并将其传递给子组件的refprop。例如:1function ParentComponent() { 2 const childRef = useRef(null); 3 4 const handleClick = () => { 5 // 调用子组件的方法 6 childRef.current.childMethod(); 7 }; 8 9 return ( 10 <div> 11 <ChildComponent ref={childRef} /> 12 <button onClick={handleClick}>调用子组件方法</button> 13 </div> 14 ); 15}
-
通过以上步骤,父组件就能够成功调用子组件中暴露的方法了。请注意,在函数式组件中,需要使用 forwardRef 来包裹子组件,并通过 ref 参数来定义暴露的方法。
24.简单概括useMemo 和 useCallback 有什么区别?
useMemo 和 useCallback 都是 React 中用于性能优化的 Hook,它们的作用类似,都可以用于缓存计算结果。不过它们的使用场景和用法还是有所区别的。
useMemo 用于缓存计算结果,只有当依赖项发生变化时才会重新计算。useMemo 接受一个计算函数和一个依赖数组作为参数,返回计算结果。
javascriptCopy Codeconst memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
在上面的代码中,computeExpensiveValue 是一个计算函数,它接受参数 a 和 b,返回一个计算结果。useMemo 将根据依赖项 [a, b] 来判断是否需要重新计算结果。
useCallback 用于缓存回调函数,只有当依赖项发生变化时才会重新创建。useCallback 接受一个回调函数和一个依赖数组作为参数,返回一个缓存的回调函数。
javascriptCopy Codeconst memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
在上面的代码中,doSomething 是一个回调函数,它接受参数 a 和 b,执行一些操作。useCallback 将根据依赖项 [a, b] 来判断是否需要返回一个新的回调函数。
区别:
useMemo返回的是计算结果,而useCallback返回的是缓存的回调函数。useMemo会在组件渲染时执行计算函数,而useCallback只会在依赖项发生变化时才会返回新的回调函数。useMemo的作用是缓存计算结果,可以用于避免重复计算,提高性能。useCallback的作用是缓存回调函数,可以用于避免不必要的函数创建和传递,优化组件渲染性能。
需要注意的是,在大多数情况下,不需要过度使用 useMemo 和 useCallback 进行性能优化,因为它们本身也有一些性能开销。只有在需要处理复杂计算或回调函数传递等情况下,才需要考虑使用这两个 Hook。
25.react中事件绑定列举
在React中,你可以使用以下几种方式来绑定事件:
-
使用 JSX 中的内联事件处理程序:
function handleClick() { console.log('Button clicked'); } function MyComponent() { return ( <button onClick={handleClick}>Click me</button> ); } -
使用 ES6 箭头函数绑定事件:
class MyComponent extends React.Component { handleClick = () => { console.log('Button clicked'); } render() { return ( <button onClick={this.handleClick}>Click me</button> ); } } -
使用 bind 方法绑定事件处理程序:
class MyComponent extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { console.log('Button clicked'); } render() { return ( <button onClick={this.handleClick}>Click me</button> ); } }
这些是在React中常用的事件绑定方式,你可以根据自己的喜好和实际情况选择合适的方式进行事件绑定。