1.谈谈你对HOC的理解
定义: 高阶组件是一个接收组件作为参数并返回新组件的函数,用于复用组件逻辑,遵循纯函数特性(无副作用,输出仅依赖输入)。
- 组合性:可嵌套使用多个 HOC。
HOC(Higher-Order Component,高阶组件)是 React 中的一种设计模式,它本质上是一个函数,接受一个组件作为参数,返回一个新的组件。这个新组件通常会添加一些额外的功能或者修改原有组件的行为,而不直接修改原组件的代码。
属性代理props,state,反向继承(生命周期劫持,方法重写)
主要特点:
- 增强组件功能:HOC 允许你在不修改原组件的情况下,给它添加额外的逻辑或功能。比如:权限控制、数据获取、日志记录、条件渲染等。
- 纯函数:HOC 只是一个函数,它不改变原组件的实例,而是返回一个新的组件。传入的原组件将成为 HOC 的输入,返回的新组件是带有附加功能的组件。
- 组合性:多个 HOC 可以被组合在一起,形成一个强大的功能组合。这使得 React 的组件变得更加灵活和可复用。
常见应用场景:
- 状态共享:多个组件之间可以通过 HOC 共享相同的状态逻辑。
- 权限控制:HOC 可以用于根据用户权限来渲染不同的 UI。
- 生命周期管理:在 HOC 中添加钩子函数,可以封装组件的生命周期操作。
- 代码复用:例如,处理 API 请求的 HOC 可以应用于多个组件,而不需要每个组件都重复编写相同的请求逻辑。
优缺点:
优点:
- 增强可复用性:将常见的逻辑封装成 HOC,可以在多个地方复用。
- 逻辑与视图分离:HOC 使得 UI 和逻辑功能分离,提高代码的可维护性和可测试性。
- 组合性强:HOC 可以通过组合多个不同的功能来增强组件的功能。
缺点:
- 命名冲突:HOC 可能会给组件的属性命名带来冲突,尤其是在 HOC 之间传递 props 时。
- 复杂性增加:如果过度使用 HOC,可能会导致组件树变得复杂,难以调试和维护。
- 性能问题:每次通过 HOC 包装一个组件时,都会返回一个新的组件,这可能导致不必要的渲染,影响性能。
HOC详细点击查看
2.谈谈你对React Fiber的理解
先概述它的基本概念,Fiber是什么、为什么提出Fiber,主要特点是什么,解决什么问题、以及它如何影响 React 的工作方式。然后,我会深入讲解它的核心特性和实现原理,最后给出一个应用场景,展示我对它的实际理解。
React Fiber 是 React 内部的一个新的调度引擎,旨在优化 React 的渲染过程,提高渲染的可控性和性能,尤其是在处理复杂 UI 和高频率更新时。目标是使React能够更好地处理大型应用和动态更
1. 为什么需要 Fiber?
JavaScript引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等待 如果 JavaScript线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡顿 也就是早期版本中使用的叫做“栈式调度”的渲染算法。这个算法是同步的,也就是说,React 渲染一个组件时,会阻塞后续的工作,直到渲染过程完成。当React在渲染组件时,从开始到渲染完成整个过程是一气呵成的,无法中断 如果组件较大,那么js线程会一直执行,然后等到整棵VDOM树计算完成后,才会交给渲染的线程 这就会导致一些用户交互、动画等任务无法立即得到处理,导致卡顿的情况,
JavaScript执行Javascript引擎和页面渲染在同一个线程中,GUI渲染和Javascript执行两者之间是互斥的 工 如果某个任务执行时间过长,浏览器就会推迟渲染。这就引入了Fiber
Fiber 作为 React 的新架构,主要是为了引入“增量渲染”,使得渲染过程可以被中断并分片执行,允许 React 在渲染期间执行其他更紧急的任务,从而改善性能。
2. Fiber 解决了什么问题?
- 异步渲染:Fiber 使得 React 渲染过程可以被分割成多个小任务,任务之间可以被中断和重新调度。这样,当 React 正在渲染时,它可以暂停当前的渲染工作,去处理一些更重要的任务(比如用户输入、动画等)。
- 优先级调度:通过 Fiber,React 能够为不同的渲染任务设置优先级。例如,用户输入和动画可以拥有更高的优先级,而不那么重要的任务(如更新某个非关键的 UI 元素)可以有更低的优先级,从而保证高优先级任务尽快完成。
- 改进的生命周期管理:React 通过 Fiber 可以更好地管理组件生命周期,处理复杂的场景,如 React Suspense 和 Concurrent Mode,这些特性在 Fiber 的架构下能够得到更好的支持。
3. Fiber 的核心特性和实现:
-
增量渲染(Incremental Rendering) :Fiber 将渲染过程分解为多个小任务,每个任务都可以中断和恢复。通过这种方式,React 可以在渲染中间进行调度,优先处理高优先级的任务,如用户交互。
-
优先级调度(Prioritization) :Fiber 引入了优先级的概念。不同的渲染任务可以根据它们的优先级被调度。比如:
- 用户交互(例如点击、滚动等)通常是高优先级的。
- 状态更新或背景渲染可能是低优先级的。
-
Fiber 树:Fiber 引入了一种新的数据结构——Fiber 树,它是虚拟 DOM 的一个升级版。每个 Fiber 节点都表示一个组件实例,包含了关于组件的所有信息,包括它的状态、渲染结果、生命周期方法等。
- Work Units:每个 Fiber 节点对应一个“工作单元”,这些单元可以被异步执行。React 可以把渲染工作分成更小的单位,按需处理。
-
Time Slicing:通过 Fiber,React 可以将大任务切割成多个小任务,在渲染的过程中“切片”时间,让浏览器有机会处理其他任务,比如用户输入、动画等,从而避免界面卡顿。
4. Fiber 与 React 之前版本的区别:
- 同步 vs 异步:在 React Fiber 之前,React 的渲染过程是同步的。也就是说,组件渲染是阻塞式的,直到整个渲染完成。在 Fiber 中,渲染过程被拆解成多个小任务,可以异步执行。
- 改进的生命周期:Fiber 引入了新的生命周期方法,特别是针对异步渲染的生命周期方法(如
getDerivedStateFromProps
和getSnapshotBeforeUpdate
),这些方法有助于提升组件的性能和响应性。 - 并发渲染:Fiber 使得 React 能够支持并发渲染(Concurrent Rendering)。这意味着 React 可以在多个任务之间切换,优先处理用户交互、动画等高优先级任务,降低长时间渲染对用户体验的影响。
5. Fiber 在实际应用中的优势:
- 改善复杂动画:对于需要频繁更新的动画或交互式 UI,Fiber 通过异步渲染和优先级调度可以避免动画卡顿,提升流畅度。
- React Suspense:Fiber 是 React Suspense 功能的基础。它允许 React 在数据加载时“暂停”渲染,等数据准备好后再继续渲染,提升了数据驱动应用的响应速度和流畅性。
- 并发模式(Concurrent Mode) :Fiber 为并发模式奠定了基础,使得 React 可以同时渲染多个版本的 UI,进一步提升性能和用户体验。
总结:
React Fiber 是 React 渲染引擎的一次重大升级,通过引入异步渲染、优先级调度和增量渲染,极大提升了 React 的性能和灵活性。它为未来的 React 特性(如并发模式和 Suspense)提供了基础,同时也优化了复杂 UI 更新和高频交互的性能。虽然 Fiber 的实现较为复杂,但它为 React 提供了更强大的能力,尤其是在需要精细控制渲染过程的场景中。
具体fiber原理见:https://blog.csdn.net/qq_34645412/article/details/145886426?spm=1001.2014.3001.5501
3.说说对React的理解?有哪些特性
React 是一个用于构建用户界面的 JavaScript 库,主要特点包括:
- 组件化,可组合和嵌套:React 将 UI 划分为独立的、可复用的组件,每个组件可以有自己的状态和生命周期。组件化的结构让代码更具可维护性和可复用性。
- 虚拟 DOM:React 通过虚拟 DOM 来优化性能,减少对真实 DOM 的直接操作。每次状态更新,React 会先在虚拟 DOM 中计算差异,然后高效地更新实际 DOM。
- 单向数据流:React 使用单向数据流,父组件通过 props 向子组件传递数据,子组件不能直接修改父组件的状态,确保数据流向清晰,管理更简单。
- JSX:JSX 是 React 使用的语法扩展,它让开发者能够在 JavaScript 中直接写 HTML 结构,提高了代码的可读性和开发效率。
- 声明式编程:React采用声明范式,可以轻松描述应用。开发者只需描述UI应该是什么样子,React会负责实际渲染工作
- Hooks:React 16.8 引入的 Hooks 允许函数组件管理状态和副作用,简化了类组件中复杂的生命周期管理。
- React Router 和 Context:React 通过
React Router
实现单页面应用的路由功能,通过React Context
提供跨组件的数据传递。
这些特性使得 React 在构建高效、可维护的用户界面时非常强大,特别是在构建大型应用时,可以大大提升开发效率和应用性能。
4.说说你对React的state和props有什么区别
突出 state 和 props 的区别
面试官,state
和 props
都是 React 中用于管理和传递数据的方式,但它们有一些重要的区别:
-
state
(状态) :state
是组件内部管理的数据,它决定了组件的可变状态。- 组件可以通过
this.setState
(类组件)或者useState
(函数组件)来更新state
,从而触发组件重新渲染。 - 每个组件有自己的
state
,它是可变的,因此state
主要用于存储需要随时间变化的数据,如用户输入、交互状态等。
-
props
(属性) :props
是父组件传递给子组件的数据,它是只读的,子组件不能修改自己的props
。props
用于组件间的数据传递和共享,是组件之间的通信方式。props
是不可变的,父组件通过更新props
来控制子组件的数据。
关键区别:
- 来源:
state
来自组件内部,props
来自父组件。 - 可变性:
state
是可变的,props
是只读的。 - 用途:
state
用于组件内部的数据管理,props
用于组件间的数据传递。
5.说说你对React的super和super(props)有什么区别
在 React 中,super
和 super(props)
都是与类组件的构造函数相关的,但是它们有细微的区别。
-
super
:super
是调用父类的构造函数。在 React 中,所有的组件类都继承自React.Component
或React.PureComponent
,因此在定义构造函数时,我们需要调用super()
来初始化父类。- 如果没有调用
super()
,子类的构造函数就无法正确执行,会导致错误。
class MyComponent extends React.Component {constructor() {super(); // 调用父类构造函数this.state = { count: 0 };} }
-
super(props)
:super(props)
不仅调用父类的构造函数,还将父组件传递的props
传递给React.Component
的构造函数。- React 需要通过
props
初始化组件的状态或其他操作,因此如果我们想在构造函数中使用this.props
,就必须调用super(props)
。
class MyComponent extends React.Component {constructor(props) {super(props); // 调用父类构造函数并传递 propsthis.state = { count: 0 };} }
关键区别:
super()
:仅仅调用父类的构造函数,不传递props
。super(props)
:调用父类的构造函数,并将父组件传递的props
传递给父类,这样子类的构造函数中就可以访问this.props
。
在使用 React.Component
或 React.PureComponent
时,如果希望在构造函数中访问 this.props
,应该使用 super(props)
。
6.说说你对react中类组件和函数组件的理解,有什么区别?
在React中,类组件和函数组件是两种主要的组件形式,它们有以下区别:
类组件
- 定义方式:
- 类组件是基于ES6的类语法定义的,需要继承自
React.Component
。
- 生命周期方法:
- 类组件可以使用React提供的各种生命周期方法,如
componentDidMount
、componentDidUpdate
和componentWillUnmount
等。
- 状态管理:
- 类组件有自己的状态(
this.state
),可以通过this.setState()
方法来更新状态。
- this关键字:
- 类组件中需要使用
this
关键字来访问组件的属性和方法。
- 性能优化:
- 可以使用
shouldComponentUpdate
生命周期方法来进行性能优化,避免不必要的渲染。
- 代码复杂性:
- 类组件的代码通常比函数组件更复杂,尤其是在处理多个生命周期方法和状态更新时。
函数组件
- 定义方式:
- 函数组件是一个简单的JavaScript函数,接收
props
作为参数并返回React元素。
- Hooks支持:
- 自React 16.8起,函数组件可以使用Hooks(如
useState
、useEffect
等)来管理状态和副作用。
- 状态管理:
- 使用
useState
Hook可以在函数组件中添加和管理状态。
- 简洁性:
- 函数组件通常更简洁,易于理解和维护。
- 性能优化:
- React团队为函数组件引入了
React.memo
高阶组件来进行性能优化,避免不必要的渲染。
- 代码简洁性:
- 函数组件的代码通常更加简洁,尤其是在使用Hooks之后,可以避免类组件中的一些样板代码。
总结
- 类组件适合那些需要使用复杂生命周期方法或者需要在多个生命周期方法中维护状态的场景。
- 函数组件随着Hooks的引入,已经变得非常强大,可以处理大多数场景,包括状态管理和副作用处理。函数组件通常更简洁、易于测试和维护。
随着React的发展,函数组件和Hooks已经成为主流,许多新的特性和优化都是围绕它们展开的。因此,现代React开发中,推荐优先使用函数组件和Hooks。
7.说说你对react中受控组件和非受控组件的理解?应用场景
面试官,在 React 中,受控组件和非受控组件主要的区别在于数据的控制和管理方式。
1. 受控组件(Controlled Components) :
-
定义:受控组件是指那些通过 React 的
state
来管理其值的组件。组件的值由父组件的状态来控制,用户的输入通过事件处理程序更新组件的state
,从而触发重新渲染。 -
实现:在受控组件中,表单元素(如
<input>
、<textarea>
、<select>
等)的值由组件的state
控制,onChange
事件用来更新state
,确保 React 控制表单元素的值。function ControlledInput() {const [value, setValue] = useState('');const handleChange = (e) => {setValue(e.target.value);};return (<input type="text" value={value} onChange={handleChange} />); }
-
特点:
- React 完全控制组件的状态和行为。
- 可以方便地进行表单验证、动态显示错误信息等。
- 更易于调试,因其数据是受控的。
2. 非受控组件(Uncontrolled Components) :
-
定义:非受控组件是指那些不直接通过 React 的
state
来控制其值的组件。相反,组件的值由 DOM 本身管理,而 React 通过ref
来访问表单元素的值。 -
实现:在非受控组件中,表单元素的值并不由 React 的状态管理,而是依赖于 DOM 自身的状态。你可以通过
ref
获取该值。function UncontrolledInput() {const inputRef = useRef();const handleSubmit = () => {alert('Input value: ' + inputRef.current.value);};return (<div><input type="text" ref={inputRef} /><button onClick={handleSubmit}>Submit</button></div>); }
-
特点:
- 组件的状态不由 React 管理,而是由 DOM 自身维护。
- 使用
ref
来直接访问 DOM 元素。 - 在某些简单的场景中使用非受控组件可以减少额外的状态管理,代码更简洁。
3. 受控组件与非受控组件的区别:
-
数据来源:
- 受控组件:表单元素的值由 React 的
state
控制。 - 非受控组件:表单元素的值由 DOM 控制,React 通过
ref
来访问它。
- 受控组件:表单元素的值由 React 的
-
渲染方式:
- 受控组件的每次用户输入都会更新 React 的
state
,并触发组件重新渲染。 - 非受控组件不会每次输入都触发渲染,只有在调用
ref
获取值时才访问 DOM。
- 受控组件的每次用户输入都会更新 React 的
-
灵活性:
- 受控组件能够实现更多的功能(如表单验证、动态更新等),更适合复杂的交互。
- 非受控组件适合那些没有复杂交互逻辑的简单表单,减少了不必要的状态管理。
4. 应用场景:
-
受控组件:
- 适用于需要实时跟踪用户输入、进行表单验证、动态更新 UI 或处理表单数据提交的场景。
- 例如,复杂表单、表单验证、交互式表单(例如,根据用户选择动态渲染其他输入字段)。
-
非受控组件:
- 适用于简单的场景,不需要频繁跟踪输入值的变化。例如,简单的表单或是只在表单提交时才获取值的场景。
- 例如,表单只需要在提交时获取数据,或是需要快速开发一个简单的表单,不关心输入的实时变化。
总结:
- 受控组件通过 React 的
state
来管理表单元素的值,适合需要高控制和实时反馈的场景。 - 非受控组件使用
ref
直接访问 DOM 元素的值,适合简单表单或需要简化代码的场景。
8.说说你对react事件绑定的方式有哪些?区别?
如果这是一个面试题,我会简洁明了地回答 React 中事件绑定的方式,并突出每种方式的区别。以下是我可能的回答:
面试官,在 React 中,事件绑定主要有两种方式:方法绑定(普通函数)和箭头函数绑定。它们的区别在于上下文(this
)的绑定方式。以下是详细解释:
1. 使用 bind
方法绑定事件
- 定义:使用 JavaScript 的
bind()
方法在构造函数中显式地绑定事件处理函数的this
上下文。 - 实现:在构造函数中通过
bind
方法将事件处理函数的this
绑定到当前实例。
class MyComponent extends React.Component {constructor(props) {super(props);this.state = { count: 0 };// 在构造函数中绑定事件处理函数this.handleClick = this.handleClick.bind(this);}handleClick() {this.setState({ count: this.state.count + 1 });}render() {return <button onClick={this.handleClick}>Click</button>;}
}
- 优点:事件处理函数中的
this
指向当前组件实例。 - 缺点:每次组件实例化时,
bind
会创建一个新的函数,可能导致性能问题,尤其是在渲染大量组件时。
2. 使用箭头函数绑定事件
- 定义:在事件处理函数内部使用箭头函数来自动绑定
this
。 - 实现:箭头函数不需要显式绑定
this
,因为箭头函数会从定义位置(类组件)继承this
。
class MyComponent extends React.Component {constructor(props) {super(props);this.state = { count: 0 };}handleClick = () => {this.setState({ count: this.state.count + 1 });}render() {return <button onClick={this.handleClick}>Click</button>;}
}
- 优点:代码简洁,
this
自动绑定,不需要显式调用bind
。 - 缺点:每次渲染时都会创建一个新的箭头函数,可能导致性能问题,尤其是在大量渲染时。
3. 直接传递事件处理函数(函数式组件)
- 定义:对于函数组件,直接传递事件处理函数即可,
this
不存在,事件处理函数直接引用即可。 - 实现:
function MyComponent() {const [count, setCount] = useState(0);const handleClick = () => {setCount(count + 1);};return <button onClick={handleClick}>Click</button>;
}
- 优点:没有
this
,代码更加简洁和易于理解,且性能更好。 - 缺点:适用于函数组件,对于类组件来说不适用。
4. 直接调用事件处理函数
- 定义:直接在 JSX 中调用事件处理函数,不进行绑定。
- 实现:
class MyComponent extends React.Component {handleClick() {alert('Button clicked!');}render() {return <button onClick={() => this.handleClick()}>Click</button>;}
}
- 优点:代码简洁。
- 缺点:每次渲染都会创建一个新的函数,可能会影响性能,特别是在大量渲染时。
5. 传递参数给事件处理函数
- 定义:如果需要在事件处理函数中传递额外的参数,可以通过箭头函数或
bind
来传递。
class MyComponent extends React.Component {handleClick = (param) => {alert(param);};render() {return <button onClick={() => this.handleClick('Hello!')}>Click</button>;}
}
- 优点:可以灵活地传递额外参数。
- 缺点:和直接调用一样,每次渲染都会创建新的函数。
6. 事件处理函数的优化
React.memo
和useCallback
:对于性能要求较高的组件,可以使用React.memo
(函数组件)和useCallback
(函数组件)来避免不必要的渲染和重新绑定函数。这样做可以确保在相同的输入下,事件处理函数保持一致,避免每次渲染都创建新的函数。
总结:可直接回答总结部分
bind
:适用于类组件,在构造函数中绑定this
,但可能引发性能问题。- 箭头函数:简洁的写法,自动绑定
this
,但也可能在渲染时创建新的函数,影响性能。 - 函数式组件:没有
this
,直接传递事件处理函数,性能好且代码简洁。但不适用于类组件 - 直接调用:虽然简洁,但每次渲染都会创建新的函数,性能较差。
- 传递参数:通过箭头函数或
bind
,可以灵活传递额外参数,但要注意性能影响。
在实际开发中,通常推荐使用箭头函数或者函数组件来简化代码,尽量避免不必要的性能开销,尤其是在频繁渲染的组件中。
9.说说react事件机制?
在React中,事件机制是一个重要的核心概念,它通过合成事件(SyntheticEvent) 和 事件委托(Event Delegation) 实现了跨浏览器一致性和性能优化。以下是详细解析:
1. 合成事件(SyntheticEvent)
React的事件对象是对原生浏览器事件的跨浏览器封装,提供了统一的API接口,确保在不同浏览器中行为一致。
-
特点:
- 跨浏览器兼容:例如,
event.preventDefault()
和event.stopPropagation()
在所有浏览器中行为一致。 - 性能优化:事件对象会被复用(事件池机制),在事件回调执行后,事件对象的属性会被重置为
null
。若需异步访问事件属性,需调用event.persist()
。 - 事件类型:支持常见的DOM事件(如
onClick
、onChange
),也支持React特有的合成事件(如onDoubleClick
)。
- 跨浏览器兼容:例如,
-
示例:
function handleClick(event) {event.preventDefault(); // 阻止默认行为event.stopPropagation(); // 阻止冒泡console.log(event.target.value); // 访问事件属性 }
2. 事件委托(Event Delegation)
React将所有事件委托到根节点(React 17之前是document
,17+是ReactDOM.render
的容器节点),而非直接绑定到具体元素。
-
优势:
- 内存优化:减少事件监听器的数量,避免为每个元素单独绑定事件。
- 动态元素支持:动态添加的子元素无需重新绑定事件。
-
示例:
// React内部自动处理事件委托,开发者只需编写事件处理函数 <button onClick={handleClick}>Click Me</button>
3. 与原生事件的区别
- 命名方式:React事件使用驼峰命名(如
onClick
),而非原生的小写(如onclick
)。 - 事件绑定:React通过JSX属性绑定事件,而非
addEventListener
。 - 默认行为:React中需显式调用
event.preventDefault()
,而原生事件可通过return false
阻止默认行为。
4. 事件处理中的this
绑定
在类组件中,事件处理函数需注意this
指向问题:
-
解决方法:
- 构造函数中绑定:
this.handleClick = this.handleClick.bind(this)
- 使用箭头函数:
handleClick = () => { ... }
- 在JSX中直接绑定:
onClick={() => this.handleClick()}
(可能引起性能问题)
- 构造函数中绑定:
5. 事件池(Event Pooling)
-
机制:React会复用合成事件对象以提升性能,事件回调执行后,事件对象的属性会被置为
null
。 -
异步访问:若需在异步操作(如
setTimeout
或Promise
)中访问事件属性,需调用event.persist()
。function handleClick(event) {event.persist(); // 保留事件对象setTimeout(() => {console.log(event.target.value); // 正常访问}, 1000); }
6. React 17+ 的变化
- 事件委托容器:事件不再委托到
document
,而是绑定到ReactDOM.render
的根容器节点,避免与外部DOM树冲突。 - 移除事件池:React 17+ 移除了事件池优化,合成事件对象不再被复用,无需
event.persist()
即可异步访问属性。
7. 应用场景与最佳实践
- 受控组件:使用
onChange
和state
管理表单输入(实时验证、提交)。 - 性能敏感场景:非受控组件结合
ref
直接操作DOM,减少渲染次数。 - 阻止冒泡:在嵌套组件中,通过
event.stopPropagation()
控制事件传播。
总结
React的事件机制通过合成事件和事件委托,在简化开发的同时保证了性能和跨浏览器一致性。理解其核心原理(如this
绑定、事件池、委托策略)能帮助开发者更高效地处理交互逻辑,避免常见陷阱(如异步访问事件属性)。随着React 17+的更新,事件机制进一步简化,更贴近原生行为。
10.说说react构建组件的方式有哪些?区别是?
在 React 中,构建组件的方式主要有以下几种,它们各有特点并适用于不同的场景:
1. 类组件(Class Components)
-
定义方式:通过 ES6 的
class
语法定义,继承自React.Component
。 -
核心特性:
- 使用
this.state
管理状态。 - 通过生命周期方法(如
componentDidMount
、componentDidUpdate
)处理副作用。 - 需要手动绑定事件处理函数的
this
指向。
- 使用
-
适用场景:
- 需要复杂生命周期控制的场景(如精确管理组件挂载、更新、卸载时的逻辑)。
- 旧代码库或需要兼容 React 16.8 之前的版本。
-
示例:
class MyComponent extends React.Component {state = { count: 0 };handleClick = () => {this.setState({ count: this.state.count + 1 });};render() {return <button onClick={this.handleClick}>{this.state.count}</button>;} }
2. 函数组件(Function Components)
-
定义方式:通过普通 JavaScript 函数定义,接受
props
参数并返回 JSX。 -
核心特性:
- 使用 Hooks(如
useState
、useEffect
)管理状态和副作用。 - 无生命周期方法,但可通过
useEffect
模拟生命周期行为。 - 代码更简洁,避免
this
绑定问题。
- 使用 Hooks(如
-
适用场景:
- 新项目或需要简化代码结构的场景。
- 需要逻辑复用(通过自定义 Hooks)。
-
示例:
function MyComponent() {const [count, setCount] = useState(0);const handleClick = () => setCount(count + 1);return <button onClick={handleClick}>{count}</button>; }
3. 高阶组件(HOC, Higher-Order Components)
-
定义方式:接收一个组件并返回一个新组件的函数。
-
核心特性:
- 用于逻辑复用(如权限校验、数据获取)。
- 通过包装组件注入额外 props 或行为。
-
缺点:
- 嵌套过多可能导致“包装地狱”(类似
withA(withB(Component))
)。 - 可能引入命名冲突。
- 嵌套过多可能导致“包装地狱”(类似
-
示例:
function withLogger(WrappedComponent) {return function(props) {useEffect(() => {console.log('Component rendered!');}, []);return <WrappedComponent {...props} />;}; } const EnhancedComponent = withLogger(MyComponent);
4. Render Props 模式
-
定义方式:通过
props
传递一个函数,由子组件决定如何渲染内容。 -
核心特性:
- 解决逻辑复用问题,避免 HOC 的嵌套问题。
- 更灵活地共享组件间的逻辑。
-
示例:
<DataProvider render={data => <ChildComponent data={data} />} />
5. 自定义 Hooks
-
定义方式:通过
useXxx
命名的函数封装可复用逻辑。 -
核心特性:
- 替代 HOC 和 Render Props,更简洁地实现逻辑复用。
- 可以在函数组件中直接调用。
-
示例:
function useCounter(initialValue) {const [count, setCount] = useState(initialValue);const increment = () => setCount(count + 1);return { count, increment }; } // 使用 function MyComponent() {const { count, increment } = useCounter(0);return <button onClick={increment}>{count}</button>; }
6. 复合组件(Compound Components)
-
定义方式:通过多个关联组件共同工作,共享隐式状态(如
<Select>
和<Option>
)。 -
核心特性:
- 通过
React.Context
或React.Children
实现状态共享。 - 提供更直观的 API 设计。
- 通过
-
示例:
const Tabs = ({ children }) => {const [activeTab, setActiveTab] = useState(0);return (<div>{React.Children.map(children, (child, index) =>React.cloneElement(child, {isActive: index === activeTab,onClick: () => setActiveTab(index),}))}</div>); };
各方式的核心区别
方式 | 状态管理 | 逻辑复用 | 代码简洁性 | 适用场景 |
---|---|---|---|---|
类组件 | this.state | 继承、HOC | 较复杂 | 旧项目、复杂生命周期控制 |
函数组件 + Hooks | useState | 自定义 Hooks | 简洁 | 新项目、逻辑复用 |
HOC | 通过 props 注入 | 包装组件 | 中等 | 横切关注点(如鉴权、日志) |
Render Props | 通过函数参数传递 | 动态渲染 | 灵活但稍显冗长 | 需要高度定制的逻辑复用 |
复合组件 | Context 或 Children | 隐式状态共享 | 直观 | 关联组件的组合(如表单) |
总结
- 类组件:适合需要精细控制生命周期的场景,但逐渐被函数组件取代。
- 函数组件 + Hooks:现代 React 的主流方式,代码简洁且逻辑复用能力强。
- HOC/Render Props:在 Hooks 出现前用于逻辑复用,现可结合 Hooks 使用。
- 复合组件:适合构建复杂但 API 友好的组件库(如 Ant Design)。
推荐选择:
- 新项目优先使用 函数组件 + Hooks。
- 需要兼容旧代码时,可混合使用类组件和 Hooks。
- 逻辑复用优先用 自定义 Hooks,其次考虑 HOC 或 Render Props。
11.说说react引入css的方式有哪几种?区别
在 React 中,引入 CSS 的方式多样,每种方法都有其适用场景和优缺点。以下是常见方案及其核心区别:
1. 内联样式(Inline Styles)
-
定义:直接在 JSX 元素中通过
style
属性编写样式,使用 JavaScript 对象表示。 -
特点:
- 作用域:仅作用于当前元素,无全局污染。
- 动态样式:方便根据 props/state 动态修改样式。
- 局限性:不支持伪类(如
:hover
)、媒体查询、动画等。
-
示例:
const divStyle = { color: 'red', fontSize: '20px' }; function Component() {return <div style={divStyle}>Hello</div>; }
2. 普通 CSS 文件(Plain CSS)
-
定义:通过
import './styles.css'
引入全局 CSS 文件。 -
特点:
- 作用域:全局生效,易引发样式冲突。
- 维护性:适合传统项目,但缺乏模块化。
- 功能支持:完整支持所有 CSS 特性。
-
示例:
/* styles.css */ .my-class { color: red; }
import './styles.css'; function Component() {return <div className="my-class">Hello</div>; }
3. CSS Modules
-
定义:通过构建工具(如 Webpack)将 CSS 文件转换为局部作用域的模块。
-
特点:
- 作用域:类名被哈希化,避免全局冲突(如
.my-class_1x2y3
)。 - 维护性:模块化清晰,适合组件化开发。
- 兼容性:需配置构建工具支持(如
css-loader
)。
- 作用域:类名被哈希化,避免全局冲突(如
-
示例:
/* styles.module.css */ .myClass { color: red; }
import styles from './styles.module.css'; function Component() {return <div className={styles.myClass}>Hello</div>; }
4. CSS-in-JS
-
定义:使用 JavaScript 编写 CSS,常见库包括
styled-components
、Emotion
、JSS
。 -
特点:
- 作用域:样式与组件绑定,无全局污染。
- 动态样式:支持基于 props/state 的动态样式。
- 功能支持:完整 CSS 功能(包括伪类、动画)。
- 性能:运行时生成样式,可能影响性能(但通常可优化)。
-
示例(styled-components) :
import styled from 'styled-components'; const StyledDiv = styled.div`color: ${props => props.primary ? 'red' : 'blue'};&:hover { font-size: 20px; } `; function Component() {return <StyledDiv primary>Hello</StyledDiv>; }
5. CSS 预处理器(Sass/Less/Stylus)
-
定义:通过 Sass/Less 等预处理器增强 CSS 功能(变量、嵌套、混合等)。
-
特点:
- 功能增强:支持变量、嵌套、函数等高级特性。
- 结合方式:可与 CSS Modules 或普通 CSS 结合使用。
- 构建依赖:需配置预处理器(如
sass-loader
)。
-
示例(Sass + CSS Modules) :
/* styles.module.scss */ $primary-color: red; .myClass { color: $primary-color; }
import styles from './styles.module.scss'; function Component() {return <div className={styles.myClass}>Hello</div>; }
6. Utility-First CSS(Tailwind CSS)
-
定义:通过预定义的实用类(utility classes)快速组合样式。
-
特点:
- 开发速度:无需手写 CSS,通过类名组合实现样式。
- 定制性:支持通过配置文件扩展主题。
- 学习成本:需记忆大量类名,但 IDE 插件可辅助。
-
示例:
function Component() {return (<div className="text-red-500 hover:text-blue-500">Hello</div>); }
7. CSS 框架(如 Bootstrap)
-
定义:使用现成的 UI 框架(如 Bootstrap、Ant Design)提供的样式。
-
特点:
- 快速开发:直接使用预定义的组件和样式。
- 定制性:通常支持主题覆盖,但可能需覆盖框架默认样式。
-
示例:
import 'bootstrap/dist/css/bootstrap.min.css'; function Component() {return <button className="btn btn-primary">Submit</button>; }
各方案对比
方式 | 作用域 | 动态样式 | 功能支持 | 维护性 | 适用场景 |
---|---|---|---|---|---|
内联样式 | 组件内 | ✅ | ❌(无伪类/媒体查询) | 低 | 简单动态样式 |
普通 CSS | 全局 | ❌ | ✅ | 中 | 传统项目、小型应用 |
CSS Modules | 局部 | ❌ | ✅ | 高 | 组件化开发、避免冲突 |
CSS-in-JS | 局部 | ✅ | ✅ | 高 | 复杂动态样式、主题系统 |
预处理器 | 依赖引入方式 | ❌ | ✅(增强功能) | 高 | 需要高级 CSS 功能 |
Utility-First | 全局/局部 | ✅(通过类名) | ✅ | 中 | 快速开发、减少自定义 CSS |
CSS 框架 | 全局 | ❌ | ✅ | 中 | 快速搭建标准化 UI |
总结
- 简单场景:内联样式或普通 CSS。
- 组件化开发:优先选择 CSS Modules 或 CSS-in-JS(如 styled-components)。
- 动态主题/复杂样式:CSS-in-JS 是最佳选择。
- 快速开发:Tailwind CSS 或现成的 CSS 框架。
- 大型项目:结合 CSS Modules + 预处理器(如 Sass)提升可维护性。
根据项目规模、团队习惯和样式复杂度灵活选择,也可混合使用多种方案(如用 Tailwind 处理布局,CSS-in-JS 处理动态主题)。
12.React生命周期有哪些不同的阶段?每个阶段对应的方法是?
初始化挂载(Mounting) 、更新(Updating) 和 卸载(Unmounting)
1. 生命周期概述
1.1 React 16.3 之前的生命周期
- 初始化阶段
- constructor
- componentWillMount
- render
- componentDidMount
- 更新阶段
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
- 卸载阶段
- componentWillUnmount
1.2 React 16.3 之后的生命周期
- 初始化阶段
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
- 更新阶段
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
- 卸载阶段
- componentWillUnmount
5.2 生命周期方法与 Hooks 对照表
生命周期方法 | Hooks 实现 |
---|---|
constructor | useState |
componentDidMount | useEffect(() => {}, []) |
componentDidUpdate | useEffect(() => {}, [deps]) |
componentWillUnmount | useEffect(() => { return () => {} }, []) |
shouldComponentUpdate | useMemo, useCallback |
getDerivedStateFromProps | useState + useEffect |
详细请看链接https://tutudev.blog.csdn.net/article/details/144718978 |
13.React组件之间如何进行通信?
在 React 中,组件间的通信方式根据组件关系的不同有多种解决方案。以下是常见场景及对应方法:
一、父子组件通信
1. 父 → 子:通过 props
传递数据
-
实现:父组件通过属性(props)将数据传递给子组件。
-
示例:
// 父组件 function Parent() {const data = "Hello";return <Child message={data} />; } // 子组件 function Child({ message }) {return <div>{message}</div>; // 输出:Hello }
2. 子 → 父:通过回调函数
-
实现:父组件传递一个回调函数给子组件,子组件调用该函数传回数据。
-
示例:
// 父组件 function Parent() {const handleData = (data) => console.log(data);return <Child onSend={handleData} />; } // 子组件 function Child({ onSend }) {return <button onClick={() => onSend("Data from child")}>Send</button>; }
二、兄弟组件通信
1. 通过共同的父组件(状态提升)
-
实现:将共享状态提升到父组件,通过 props 和回调函数传递。
-
示例:
function Parent() {const [sharedData, setSharedData] = useState("");return (<><ChildA onUpdate={setSharedData} /><ChildB data={sharedData} /></>); }
三、跨层级组件通信
1. Context API
-
实现:通过
React.createContext
创建上下文,Provider
提供数据,useContext
或Consumer
消费数据。 -
示例:
// 创建 Context const MyContext = React.createContext(); // 父组件(Provider) function App() {return (<MyContext.Provider value="Hello"><Grandchild /></MyContext.Provider>); } // 子组件(Consumer) function Grandchild() {const value = useContext(MyContext);return <div>{value}</div>; // 输出:Hello }
2. 状态管理库(Redux、MobX、Zustand)
-
实现:通过全局 Store 管理状态,组件通过
useSelector
或connect
订阅状态。 -
Redux 示例:
// 定义 Action 和 Reducer const increment = () => ({ type: 'INCREMENT' }); const counterReducer = (state = 0, action) => {if (action.type === 'INCREMENT') return state + 1;return state; }; // 组件中派发 Action function Component() {const count = useSelector(state => state);const dispatch = useDispatch();return <button onClick={() => dispatch(increment())}>{count}</button>; }
四、任意组件通信
1. 事件总线(Event Emitter)
-
实现:使用第三方库(如
events
)或自定义事件系统。 -
示例:
// 创建事件总线 const eventEmitter = new EventEmitter(); // 组件 A:发布事件 function ComponentA() {return <button onClick={() => eventEmitter.emit("event", "Data")}>Send</button>; } // 组件 B:订阅事件 function ComponentB() {const [data, setData] = useState("");useEffect(() => {eventEmitter.on("event", setData);return () => eventEmitter.off("event", setData);}, []);return <div>{data}</div>; }
2. Refs 和命令式方法
-
实现:父组件通过
ref
调用子组件的方法。 -
示例:
// 子组件(类组件) class Child extends React.Component {method() { console.log("Child method called"); }render() { return <div>Child</div>; } } // 父组件 function Parent() {const childRef = useRef();return (<><Child ref={childRef} /><button onClick={() => childRef.current.method()}>Call Method</button></>); }
五、路由参数传递
1. React Router 的 useParams
和 state
-
实现:通过 URL 参数或路由状态传递数据。
-
示例:
// 路由配置 <Route path="/user/:id" component={User} /> // 组件获取参数 function User() {const { id } = useParams();const location = useLocation();const data = location.state?.data; // 通过 state 传递return <div>User ID: {id}, Data: {data}</div>; }
六、Hooks 共享逻辑
1. 自定义 Hooks
-
实现:封装共享逻辑,多个组件复用同一状态。
-
示例:
function useCounter(initialValue) {const [count, setCount] = useState(initialValue);const increment = () => setCount(count + 1);return { count, increment }; } // 组件 A 和 B 共享计数器逻辑 function ComponentA() {const { count, increment } = useCounter(0);return <button onClick={increment}>A: {count}</button>; }
总结
场景 | 解决方案 | 适用场景 |
---|---|---|
父子组件 | Props + 回调函数 | 简单数据传递 |
兄弟组件 | 状态提升 + 共同父组件 | 少量兄弟组件 |
跨层级组件 | Context API、Redux | 主题、用户信息等全局数据 |
任意组件 | 事件总线、状态管理库、消息订阅发布 | 复杂应用状态共享 |
路由跳转传参 | React Router 参数 | 页面间数据传递 |
逻辑复用 | 自定义 Hooks | 跨组件共享业务逻辑 |
选择建议:
- 简单场景优先使用 Props 和 Context。
- 中大型项目使用 Redux/Zustand 管理全局状态。
- 避免过度使用事件总线,以保持数据流清晰。
详细请看链接https://tutudev.blog.csdn.net/article/details/144770984
14.React中组件过渡动画如何实现
在 React 中实现组件过渡动画,通常需要结合 CSS 和 React 的生命周期控制。以下是 5 种主流实现方案,从基础到进阶,附带代码示例:
React 提供了多种方式来实现组件的过渡动画:
- React Transition Group:提供
CSSTransition
和TransitionGroup
,用于实现组件的生命周期过渡动画。 - CSS 动画:对于简单的动画,直接使用 CSS 的
transition
或animation
即可。 - react-spring:适用于需要更加复杂、物理驱动的动画效果。
方案对比
方案 | 复杂度 | 控制粒度 | 适用场景 | 学习成本 |
---|---|---|---|---|
CSS 类名切换 | 低 | 粗 | 简单显示/隐藏 | 低 |
react-transition-group | 中 | 中 | 通用组件过渡 | 中 |
framer-motion | 高 | 精细 | 复杂交互动画 | 高 |
自定义 Hooks | 中 | 灵活 | 需要定制逻辑 | 中 |
react-spring | 高 | 精细 | 物理动画/列表动画 | 高 |
方案 1:纯 CSS 类名切换
适用场景:简单的显示/隐藏过渡
原理:通过状态切换 CSS 类名触发动画
// CSS
.fade-enter {opacity: 0;
}
.fade-enter-active {opacity: 1;transition: opacity 300ms;
}
.fade-exit {opacity: 1;
}
.fade-exit-active {opacity: 0;transition: opacity 300ms;
}// React 组件
function App() {const [show, setShow] = useState(false);return (<div><button onClick={() => setShow(!show)}>Toggle</button>{show && (<div className="fade-enter-active">Content with Fade Animation</div>)}</div>);
}
缺点:无法处理卸载动画(元素会立即消失)
方案 2:react-transition-group 库
适用场景:完整的进入/离开动画控制
安装:npm install react-transition-group
import { CSSTransition } from 'react-transition-group';function App() {const [show, setShow] = useState(false);return (<div><button onClick={() => setShow(!show)}>Toggle</button><CSSTransitionin={show}timeout={300}classNames="fade"unmountOnExit><div className="box">Content with Transition</div></CSSTransition></div>);
}
/* 对应 CSS */
.fade-enter {opacity: 0;
}
.fade-enter-active {opacity: 1;transition: opacity 300ms;
}
.fade-exit {opacity: 1;
}
.fade-exit-active {opacity: 0;transition: opacity 300ms;
}
优势:自动处理动画生命周期,支持卸载动画
方案 3:使用动画库(如 framer-motion)
适用场景:复杂动画序列,物理效果动画
安装:npm install framer-motion
import { motion, AnimatePresence } from 'framer-motion';function App() {const [show, setShow] = useState(false);return (<div><button onClick={() => setShow(!show)}>Toggle</button><AnimatePresence>{show && (<motion.divinitial={{ opacity: 0, y: -20 }}animate={{ opacity: 1, y: 0 }}exit={{ opacity: 0, y: 20 }}transition={{ duration: 0.3 }}>Animated Content</motion.div>)}</AnimatePresence></div>);
}
特点:声明式 API,支持弹簧物理动画、手势交互
方案 4:结合 Hooks 的自定义动画
适用场景:需要精细控制动画过程
function useFadeAnimation(duration = 300) {const [isVisible, setIsVisible] = useState(false);const [isAnimating, setIsAnimating] = useState(false);const fadeIn = () => {setIsAnimating(true);setIsVisible(true);};const fadeOut = () => {setIsAnimating(true);setTimeout(() => {setIsVisible(false);setIsAnimating(false);}, duration);};return {isVisible,isAnimating,fadeIn,fadeOut,animationStyle: {opacity: isVisible ? 1 : 0,transition: `opacity ${duration}ms ease-out`}};
}// 使用示例
function Component() {const { isVisible, fadeIn, fadeOut, animationStyle } = useFadeAnimation();return (<div><button onClick={isVisible ? fadeOut : fadeIn}>Toggle</button><div style={animationStyle}>Animated Content</div></div>);
}
方案 5:列表动画(react-spring)
适用场景:动态列表项的添加/删除动画
安装:npm install @react-spring/web
import { useTransition, animated } from '@react-spring/web';function List() {const [items, setItems] = useState([]);const transitions = useTransition(items, {from: { opacity: 0, height: 0 },enter: { opacity: 1, height: 40 },leave: { opacity: 0, height: 0 },});return (<div><button onClick={() => setItems([...items, Date.now()])}>Add Item</button><div className="list">{transitions((style, item) => (<animated.div style={style} onClick={() => setItems(items.filter(i => i !== item))}>Item {item}</animated.div>))}</div></div>);
}
性能优化技巧
-
优先使用 transform 和 opacity
这些属性不会触发重排(reflow)// 好 { transform: 'translateX(100px)' } // 避免 { left: '100px' }
-
启用 GPU 加速
.animated-element {transform: translateZ(0);will-change: transform; }
-
合理使用 requestAnimationFrame
const animate = () => {requestAnimationFrame(() => {// 更新动画状态}); };
15.说说你在React项目如何捕获错误的?
在 React 中,我们通常使用错误边界(Error Boundaries)来捕获运行时的错误,并做出相应的处理。错误边界是一个组件,它可以捕获其子组件树中的 JavaScript 错误、记录错误信息,并展示一个备用 UI。
1. 错误边界(Error Boundaries)
React 提供了一种名为 错误边界 的机制,允许我们在应用中捕获并处理渲染过程中发生的错误。错误边界是一个类组件,必须实现 **componentDidCatch**
生命周期方法,或者在更现代的版本中实现 static **getDerivedStateFromError**
。
- getDerivedStateFromError捕获错误降级渲染页面UI
- componentDidCatch捕获错误上传服务器日志
使用错误边界:
- 创建一个错误边界组件:
import React, { Component } from 'react';class ErrorBoundary extends Component {constructor(props) {super(props);this.state = { hasError: false, errorInfo: null };}static getDerivedStateFromError(error) {// 更新状态以便下次渲染可以显示备选UIreturn { hasError: true };}componentDidCatch(error, info) {// 可以将错误日志上报到服务器console.error("Error caught by Error Boundary:", error, info);this.setState({ errorInfo: info });}render() {if (this.state.hasError) {// 渲染备选的 UIreturn (<div><h1>Something went wrong.</h1><details style={{ whiteSpace: 'pre-wrap' }}>{this.state.errorInfo && this.state.errorInfo.componentStack}</details></div>);}return this.props.children;}
}export default ErrorBoundary;
- 使用错误边界包裹子组件:
import React from 'react';
import ErrorBoundary from './ErrorBoundary';
import MyComponent from './MyComponent';function App() {return (<ErrorBoundary><MyComponent /></ErrorBoundary>);
}export default App;
getDerivedStateFromError
:这个静态方法会在子组件抛出错误时被调用,用来更新组件的状态并决定是否渲染备用 UI。componentDidCatch
:这个生命周期方法用于捕获错误和错误的调试信息,可以用来记录错误日志或发送错误信息到后台。
2. 错误边界的应用场景
- 捕获子组件的运行时错误:错误边界主要用于捕获子组件的渲染过程中、生命周期方法、构造函数等发生的错误。它能保证父组件不会受到影响,从而防止整个应用崩溃。
- 展示备用 UI:当捕获到错误时,错误边界会展示一个备选的 UI,可以是一个简单的提示信息,或是一个错误日志供用户或开发人员参考。
- 避免应用崩溃:如果没有错误边界,React 会默认让整个应用崩溃。而使用错误边界后,其他不受影响的部分可以继续正常渲染,提升用户体验。
3. 限制与注意事项
-
只能捕获渲染阶段的错误:错误边界仅能捕获组件树中渲染、生命周期方法和构造函数中的错误,不能捕获事件处理、异步代码(如
setTimeout
、fetch
)、服务端渲染等地方的错误。 -
事件处理:如果在事件处理程序中发生错误,React 并不会自动捕获,需手动进行错误处理(例如使用
try/catch
)。const handleClick = () => {try {// 执行可能出错的代码} catch (error) {console.error('Error occurred in event handler:', error);} };
-
不适用于异步代码:如果你在
componentDidMount
或其他生命周期方法中使用了异步代码,需要确保对异步操作中的错误进行处理。可以使用try/catch
或.catch()
来捕获异常。async componentDidMount() {try {const data = await fetchData();this.setState({ data });} catch (error) {console.error('Error fetching data:', error);} }
4. 结合 Error Boundaries
和日志记录
通常,错误边界会与日志服务(如 Sentry、LogRocket)配合使用,以便在捕获到错误时,将错误信息发送到服务器进行日志记录和分析。
componentDidCatch(error, info) {// 假设我们用 Sentry 来捕获错误日志Sentry.captureException(error, { extra: info });this.setState({ errorInfo: info });
}
总结:
- 错误边界 是 React 中专门用于捕获和处理运行时错误的机制。它能够捕获子组件的错误,避免整个应用崩溃,并显示一个备用 UI。
getDerivedStateFromError
和componentDidCatch
是错误边界的核心方法,用来处理错误、更新组件状态和记录错误信息。- 错误边界只能捕获渲染过程中的错误,对于事件处理和异步代码中的错误,开发者仍然需要手动处理。
详细见链接
16.说说对React Refs的理解?应用场景
解释 refs
的概念、如何使用它以及常见的应用场景。以下是我的回答:
React Refs(引用)是 React 用于直接访问组件实例或 DOM 元素的一种方式。通过 refs,开发者可以绕过 React 的声明式数据流,直接与 DOM 元素或 React 组件实例进行交互。
1. Refs 的基本概念
在 React 中,ref
是用来引用 DOM 元素或 React 组件实例的一个特殊对象。通常情况下,React 的数据流是单向的,通过状态(state)来控制视图,但有些情况下我们需要直接操作 DOM 或调用组件的方法,这时就可以使用 ref
。
使用方式:
-
访问 DOM 元素: 使用
React.createRef()
创建一个ref
对象,并将其赋值给 React 组件中的元素或组件实例。import React, { Component } from 'react';class MyComponent extends Component {constructor(props) {super(props);// 创建一个 ref 对象this.myInput = React.createRef();}focusInput = () => {// 直接操作 DOM 元素,调用 focus 方法this.myInput.current.focus();};render() {return (<div><input ref={this.myInput} type="text" /><button onClick={this.focusInput}>Focus the input</button></div>);} }export default MyComponent;
这里的
this.myInput
是一个 ref 对象,通过this.myInput.current
来访问对应的 DOM 元素。 -
访问类组件实例: 如果
ref
被用来引用一个类组件实例,可以通过ref.current
来访问该组件的实例,并调用其方法。class ChildComponent extends React.Component {sayHello() {console.log('Hello from Child');}render() {return <div>Child Component</div>;} }class ParentComponent extends React.Component {constructor(props) {super(props);this.childRef = React.createRef();}callChildMethod = () => {// 调用子组件的方法this.childRef.current.sayHello();};render() {return (<div><button onClick={this.callChildMethod}>Call Child Method</button><ChildComponent ref={this.childRef} /></div>);} }export default ParentComponent;
在这个例子中,
this.childRef.current
是ChildComponent
的实例,我们可以通过它来调用sayHello
方法。
2. ref
的应用场景
(1) 访问和操作 DOM 元素
ref
最常见的应用场景是直接访问 DOM 元素,尤其是在以下情况下:
- 需要聚焦输入框(
input
)。 - 需要获取元素的尺寸或位置(例如,测量一个元素的宽高)。
- 需要实现自定义的滚动行为。
例如,我们可以使用 ref
来聚焦一个输入框,或是控制动画时直接访问 DOM 元素。
(2) 控制子组件的行为
通过 ref
,父组件可以访问子组件的实例,并调用子组件暴露的方法。这对于处理一些业务逻辑(如触发子组件的生命周期方法、重置子组件状态等)非常有用。
(3) 触发动画
在 React 中,如果需要直接操作 DOM 元素来触发动画(例如使用第三方动画库或自定义的动画),ref
可以帮助我们绕过 React 的渲染机制,直接访问 DOM 元素进行操作。
(4) 表单验证与焦点管理
在表单中,可以使用 ref
来获取对输入框的引用,从而控制焦点的跳转或验证某些输入项的内容。例如,当用户提交表单时,可以使用 ref
来自动聚焦到第一个错误的输入框,提供更好的用户体验。
(5) 集成第三方库
在一些情况下,React 组件需要与第三方库集成,尤其是一些需要直接访问 DOM 或依赖 DOM 操作的库。ref
可以让我们将 React 与这些库进行有效的连接。例如,集成图表库、地图库、视频播放器等。
3. useRef
在函数组件中的使用
在函数组件中,useRef
是 ref
的钩子版本。useRef
返回一个可以在整个组件生命周期内持久化的对象,这个对象的 current
属性指向 DOM 元素或组件实例。
示例:使用 useRef
访问 DOM 元素
import React, { useRef } from 'react';function MyComponent() {const inputRef = useRef(null);const focusInput = () => {inputRef.current.focus();};return (<div><input ref={inputRef} type="text" /><button onClick={focusInput}>Focus the input</button></div>);
}export default MyComponent;
示例:useRef
访问组件实例
import React, { useRef } from 'react';function ChildComponent() {const sayHello = () => {console.log('Hello from Child');};return <div>Child Component</div>;
}function ParentComponent() {const childRef = useRef();const callChildMethod = () => {childRef.current.sayHello();};return (<div><button onClick={callChildMethod}>Call Child Method</button><ChildComponent ref={childRef} /></div>);
}export default ParentComponent;
4. ref
的限制与注意事项
- 不应过度使用
ref
:ref
是 React 提供的对 DOM 直接操作的功能,但它打破了 React 的声明式编程方式。通常应该尽量通过 React 的状态(state)来驱动组件的行为,避免过度依赖ref
。 - 无法触发重新渲染:改变
ref
的值不会导致组件重新渲染,因此不应该将其用来存储和管理 React 中的状态。
总结:
-
React Refs 提供了一种访问 DOM 元素或组件实例的方式。
-
ref
的常见应用场景:- 操作 DOM 元素,如聚焦输入框、滚动控制。
- 访问子组件实例,调用其方法。
- 与第三方库集成,特别是需要直接操作 DOM 的库。
- 表单验证和焦点管理。
- 执行动画和效果。
-
在函数组件中,我们使用
useRef
钩子来创建ref
。
17.谈谈React中的setState的执行机制
在 React 中,setState
是一个用于更新组件状态的函数。主要在react中是 1.异步更新(如改变状态后立马打印值是不会改变的),在原生事件中是同步更新(在原生点击事件中使用是同步更新的),2.批量更新,React 会将多个 setState
调用合并成一个更新操作,避免了每次状态改变都导致一次重新渲染。 3.setState
接受一个回调函数作为第二个参数,这个回调函数会在状态更新并重新渲染完成后调用
1. 异步性
setState
通常是异步的,尤其在事件处理器和生命周期方法中。React 并不会立即更新状态,而是将其加入到一个更新队列中,并在下一个渲染周期中批量处理这些更新。- 这种异步执行的机制可以有效减少不必要的重新渲染,提高性能。
2. 批量更新(Batching)
- React 会将多个
setState
调用合并成一个更新操作,避免了每次状态改变都导致一次重新渲染。例如,如果在同一个事件处理函数内调用了多次setState
,React 会合并这些调用,并在渲染过程中只执行一次更新。 - 批量更新的实现方式依赖于 React 的更新队列,React 会在事件循环的末尾处理这些更新,并触发一次新的渲染。
3. 回调函数
setState
接受一个回调函数作为第二个参数,这个回调函数会在状态更新并重新渲染完成后调用。该回调函数的执行是同步的。- 回调函数的使用场景通常是需要在状态更新后执行一些额外的操作,如操作 DOM 或执行网络请求。
this.setState({ count: this.state.count + 1 }, () => {console.log("State updated and component re-rendered");
});
4. 合并状态(State Merging)
setState
会对更新进行合并。当调用setState
更新状态时,它并不会完全覆盖当前状态,而是对指定的属性进行合并。- 例如,如果当前状态是
{ count: 0, name: "John" }
,调用this.setState({ count: 1 })
后,最终的状态会变成{ count: 1, name: "John" }
,而不是{ count: 1 }
。
5. 函数式更新
- 如果你需要基于前一个状态来更新状态,可以传递一个函数给
setState
。这个函数接收当前状态作为参数,并返回更新后的新状态。这样做可以确保在多个setState
调用中正确计算状态。
this.setState((prevState) => ({count: prevState.count + 1
}));
6. 优化
- 在一些场景下,可以通过
shouldComponentUpdate
或React.memo
等机制,避免不必要的渲染。 - 通过
setState
来触发渲染时,React 会检查状态是否真的发生了变化,如果没有变化,React 会跳过渲染步骤。
总结
setState
在 React 中是一个异步的、批量更新的机制。它通过合并状态和优化渲染,尽量减少了不必要的 DOM 更新。理解其异步和批量更新的行为,有助于提高 React 应用的性能和正确性。
18.说说React render方法的原理 ?在什么时候会触发?
React 的 render
方法是其核心机制之一,负责将组件状态和属性转换为用户界面。它的原理和触发时机如下:
一、render
方法的原理
-
虚拟 DOM(Virtual DOM)
render
方法生成的是虚拟 DOM(一个轻量级的 JavaScript 对象),而不是直接操作真实 DOM。- 虚拟 DOM 是真实 DOM 的抽象表示,通过
React.createElement
或 JSX 语法生成。
-
协调(Reconciliation)
- 当组件状态或属性变化时,React 会调用
render
生成新的虚拟 DOM。 - React 通过 Diff 算法对比新旧虚拟 DOM 的差异,找出需要更新的部分(最小化 DOM 操作)。
- 当组件状态或属性变化时,React 会调用
-
批量更新与异步性
- React 会将多个状态更新合并(批处理),避免频繁触发渲染,提升性能。
- 虚拟 DOM 的对比和真实 DOM 的更新是异步的(React 18 默认启用并发模式)。
二、render
方法的触发时机
-
初始渲染(Mounting)
- 组件首次挂载到 DOM 时,会触发
render
方法。
- 组件首次挂载到 DOM 时,会触发
-
状态更新(State Change)
- 通过
setState
更新组件状态时,会触发重新渲染(除非被shouldComponentUpdate
阻止)。
- 通过
-
属性变化(Props Change)
- 父组件重新渲染导致子组件的
props
变化时,子组件会重新渲染。
- 父组件重新渲染导致子组件的
-
Context 更新
- 如果组件订阅了 React Context,当 Context 的值变化时,相关组件会重新渲染。
-
强制更新(Force Update)
- 调用
forceUpdate()
方法会跳过shouldComponentUpdate
,强制触发render
。
- 调用
三、优化渲染的关键点
-
避免不必要的渲染
- 类组件:通过
shouldComponentUpdate
或继承PureComponent
实现浅比较。 - 函数组件:使用
React.memo
包裹组件,或通过useMemo
/useCallback
缓存值和函数。
- 类组件:通过
-
不可变数据
- 直接修改状态(如
this.state.obj.key = 1
)不会触发渲染,必须通过setState
或更新函数(如useState
的 setter)。
- 直接修改状态(如
-
Key 的合理使用
- 列表渲染时,为元素分配唯一且稳定的
key
,帮助 React 高效识别变化。
- 列表渲染时,为元素分配唯一且稳定的
四、常见问题
- 为什么修改了 state,但页面没更新?
可能直接修改了状态对象(未通过setState
),或未正确触发渲染(如异步操作未正确处理)。 - 函数组件如何触发渲染?
函数组件通过useState
的 setter 或useReducer
的 dispatch 触发更新。 - React 18 并发模式下的渲染
React 会优先处理高优先级更新,可能中断并重新开始渲染,提升用户体验。
总结
即回答以下即可
render
方法是 React 类组件中负责渲染 UI 的核心方法,它在组件的 state
或 props
发生变化,通过虚拟 DOM 和 Diff 算法实现高效更新。触发条件包括状态/属性变化、Context 更新等,合理优化可避免性能瓶颈。
- React 会调用
render
生成新的虚拟 DOM。 - React 通过 Diff 算法对比新旧虚拟 DOM 的差异,找出需要更新的部分(最小化 DOM 操作)。
19.说说React 中Real DOM 和 Virtual DOM 的区别?优缺点?
React 中的 Real DOM(真实 DOM)和 Virtual DOM(虚拟 DOM)是两种不同的 DOM 管理机制,它们的核心区别在于操作方式和性能优化策略。以下是它们的区别、优缺点及适用场景:
一、核心区别
特性 | Real DOM | Virtual DOM |
---|---|---|
本质 | 浏览器提供的原生 DOM 对象 | 轻量级的 JavaScript 对象(虚拟表示) |
更新方式 | 直接操作 DOM 节点 | 通过 Diff 算法对比差异后批量更新真实 DOM |
性能开销 | 直接操作成本高(重排、重绘) | 计算差异的 JS 开销,但减少真实 DOM 操作 |
更新粒度 | 每次修改触发完整更新 | 批量合并更新,最小化 DOM 操作 |
跨平台能力 | 依赖浏览器环境 | 可脱离浏览器(如 React Native、SSR) |
开发体验 | 手动管理 DOM,易出错 | 声明式编程,自动管理 DOM 更新 |
二、Real DOM 的优缺点
优点
- 直接控制:可直接操作 DOM,适合需要精细控制 DOM 的场景(如复杂动画)。
- 无中间层:无需维护虚拟 DOM 结构,减少内存占用(适用于极简单页面)。
缺点
- 性能瓶颈:频繁操作 DOM 会导致重排(Reflow)和重绘(Repaint),性能开销大。
- 开发复杂度:手动管理 DOM 状态容易出错(如内存泄漏、事件绑定残留)。
- 跨平台限制:依赖浏览器环境,难以复用逻辑到其他平台(如移动端)。
三、Virtual DOM 的优缺点
优点
-
性能优化:
- 通过 Diff 算法对比差异,仅更新必要的 DOM 节点。
- 批量合并更新,减少真实 DOM 操作次数(如 React 的自动批处理)。
-
声明式编程:
- 开发者只需关注数据状态(State/Props),无需手动操作 DOM。
- 代码更易维护,逻辑更清晰。
-
跨平台能力:
- 虚拟 DOM 是纯 JS 对象,可适配不同渲染目标(浏览器、移动端、服务端等)。
缺点
-
额外开销:
- 维护虚拟 DOM 需要内存和计算资源(生成虚拟 DOM、Diff 对比)。
- 在极简单场景下,可能不如直接操作 DOM 高效。
-
无法完全避免 DOM 操作:
- 最终仍需操作真实 DOM,只是通过中间层优化了流程。
四、为什么 React 选择 Virtual DOM?
-
平衡性能与开发体验:
- 在大多数应用场景中,虚拟 DOM 的 Diff 算法能显著减少 DOM 操作,提升性能。
- 开发者无需手动优化,专注于业务逻辑。
-
跨平台统一:
- 虚拟 DOM 抽象了渲染层,使 React 可同时支持 Web、Native、SSR 等场景。
-
声明式 UI 的优势:
- 通过状态驱动 UI,简化复杂交互的实现(如条件渲染、动态列表)。
五、适用场景
-
Virtual DOM 适用场景:
- 中大型应用,频繁状态更新(如社交网络、仪表盘)。
- 需要跨平台复用逻辑(如 React Native)。
- 团队协作项目,需统一开发范式。
-
Real DOM 适用场景:
- 极简单静态页面,无需复杂交互。
- 对性能要求极高的局部操作(如 Canvas 动画、游戏渲染)。
六、性能对比示例
假设更新 10 个 DOM 节点:
-
Real DOM:直接修改 10 次,触发 10 次重排/重绘。
-
Virtual DOM:
- 生成新虚拟 DOM,对比差异(Diff 算法)。
- 计算最小修改路径(如仅更新 2 个节点)。
- 批量操作真实 DOM,触发 1 次重排/重绘。
七、总结
- Virtual DOM 是 React 的核心优化策略,通过牺牲少量 JS 计算时间,换取真实 DOM 操作的大幅减少,从而提升整体性能。
- Real DOM 直接操作更底层,但在复杂场景下难以维护,适合特殊需求。
- 现代前端框架(如 React、Vue)均采用虚拟 DOM 或类似机制,平衡性能与开发效率。
20.React JSX转换成真实DOM的过程
React 将 JSX 转换为真实 DOM 的过程是一个分层的、高效的工作流程,涉及多个阶段的处理。以下是详细的转换过程:
一、JSX 的本质
JSX 是 JavaScript 的语法扩展,本质上是 React.createElement()
的语法糖。它允许开发者以类似 HTML 的语法描述 UI,但最终会被编译为 JavaScript 对象(即 虚拟 DOM 节点)。
示例:JSX → React.createElement
// JSX 代码
const element = <div className="title">Hello React</div>;// 编译后的 JavaScript 代码
const element = React.createElement("div",{ className: "title" },"Hello React"
);
二、转换过程的核心步骤
1. JSX 编译阶段(Babel 或 TypeScript)
-
工具:通过 Babel(
@babel/preset-react
)或 TypeScript 将 JSX 转换为React.createElement()
调用。 -
输出:生成 React 元素对象(即虚拟 DOM 节点),结构如下:
{type: "div",props: {className: "title",children: "Hello React"},// ...其他内部属性(如 key、ref) }
2. 构建虚拟 DOM 树
-
组件渲染:当组件调用
render()
(类组件)或执行函数组件时,递归生成嵌套的 React 元素对象树。 -
示例:
function App() {return (<div><Header /><Content /></div>); }
转换为:
React.createElement("div", null, React.createElement(Header, null),React.createElement(Content, null) );
3. 协调(Reconciliation)与 Diff 算法
-
触发时机:当状态(State)或属性(Props)变化时,重新生成新的虚拟 DOM 树。
-
Diff 过程:
- React 对比新旧虚拟 DOM 树,找出需要更新的部分。
- 使用 高效 Diff 策略(如按层级比较、Key 优化列表更新)。
4. 生成真实 DOM(提交阶段)
-
首次渲染(Mounting) :
- React 根据虚拟 DOM 树创建真实 DOM 节点。
- 通过
ReactDOM.render()
或根组件的createRoot
(React 18+)将 DOM 插入页面。
-
更新阶段(Updating) :
- 根据 Diff 结果,通过 最小化 DOM 操作(如
appendChild
、removeChild
、updateAttribute
)更新真实 DOM。
- 根据 Diff 结果,通过 最小化 DOM 操作(如
三、详细流程示意图
JSX 代码 → Babel 编译为 React.createElement() → 生成虚拟 DOM 树(React 元素对象) → 协调(Diff 算法对比新旧树) → 生成 DOM 更新指令 → 批量操作真实 DOM
四、关键角色与 API
-
React.createElement(type, props, children)
- 创建 React 元素对象,描述 UI 结构。
-
ReactDOM.render(element, container)
- 将虚拟 DOM 转换为真实 DOM 并挂载到容器(如
document.getElementById('root')
)。
- 将虚拟 DOM 转换为真实 DOM 并挂载到容器(如
-
协调器(Reconciler)
- React 16+ 引入 Fiber 架构,支持可中断的异步渲染,优化性能。
五、性能优化机制
-
批量更新(Batching)
- 将多次状态更新合并为一次渲染,减少 DOM 操作次数。
-
Diff 算法优化
- 同级节点比较、唯一 Key 标识列表项,避免不必要的节点重建。
-
惰性加载与按需渲染
- 通过
React.lazy
或Suspense
延迟加载组件,减少初始渲染压力。
- 通过
六、示例:从 JSX 到真实 DOM 的完整过程
// 1. JSX 代码
function App() {return <button onClick={() => alert("Clicked!")}>Click Me</button>;
}// 2. 编译为 React.createElement
React.createElement("button", { onClick: () => alert("Clicked!") }, "Click Me");// 3. 生成 React 元素对象
const buttonElement = {type: "button",props: {onClick: () => alert("Clicked!"),children: "Click Me"}
};// 4. 转换为真实 DOM(简化版逻辑)
const domNode = document.createElement(buttonElement.type);
domNode.textContent = buttonElement.props.children;
domNode.addEventListener("click", buttonElement.props.onClick);// 5. 插入页面
document.getElementById("root").appendChild(domNode);
七、总结
- JSX 是声明式 UI 的抽象层,通过编译转换为虚拟 DOM 对象。
- 虚拟 DOM 是中间表示,帮助 React 高效计算最小化 DOM 操作。
- 协调与 Diff 算法 是 React 性能的核心保障,避免直接操作真实 DOM 的开销。
- 开发者只需关注数据(State/Props)的变化,React 自动处理 UI 更新,实现高效开发。
21.React中用户如何根据不同的权限查看不同的页面
在 React 中实现基于权限的页面访问控制,通常需要结合 路由管理、全局状态 和 权限验证逻辑。以下是完整的实现方案,包含代码示例和最佳实践:
一、核心思路
- 权限数据管理:从后端获取用户权限信息,存储到全局状态(如 Redux、Context API)。
- 路由动态配置:根据权限生成可访问的路由列表。
- 路由守卫:在导航前校验权限,拦截未授权访问。
- 组件级控制:页面内根据权限隐藏/显示元素。
二、关键优化点
-
路由懒加载:使用
React.lazy
提升性能const AdminPage = React.lazy(() => import("./pages/AdminPage"));
-
权限持久化:通过
localStorage
或 Cookie 保存登录状态 -
接口级鉴权:在 API 请求头中携带 Token,后端验证权限
-
动态菜单渲染:根据权限生成侧边栏菜单项
const filteredRoutes = routes.filter(route => route.isPublic || (user && route.requiredRoles?.includes(user.role)) );
四、安全注意事项
- 前端权限非绝对安全:必须与后端接口权限校验结合
- 敏感数据保护:即使隐藏 UI,也要确保 API 返回数据经过过滤
- Token 有效期管理:使用 refreshToken 机制更新访问令牌
五、完整工作流程
用户登录 → 获取角色权限 → 存储到全局状态 → 动态生成路由 → 导航时校验权限 → 有权限则渲染页面,否则跳转登录/无权限页
22.React.createClass 与 extends component 的区别是什么
在 React 的发展过程中,React.createClass
和 extends React.Component
是两种不同的组件定义方式,它们的区别主要体现在语法、功能特性和历史背景上。以下是详细对比:
一、核心区别对比表
特性 | React.createClass (已废弃) | extends React.Component (ES6 类组件) |
---|---|---|
语法 | React 专属 API | ES6 类语法 |
this 绑定 | 自动绑定方法中的 this | 需手动绑定(或使用箭头函数/类属性) |
状态初始化 | getInitialState() 方法 | 在 constructor 中通过 this.state 初始化 |
默认 Props | getDefaultProps() 方法 | 通过静态属性 static defaultProps 定义 |
PropTypes | 内部属性 propTypes | 静态属性 static propTypes |
Mixins 支持 | 支持 | 不支持(改用高阶组件/Hooks) |
生命周期方法 | 早期方法(如 componentWillMount ) | 相同方法,但需结合 ES6 类语法 |
React 版本支持 | React <15.5,已废弃 | React 15.5+ 推荐写法 |
二、详细区别解析
1. 语法与定义方式
-
React.createClass
通过工厂函数创建组件,是 React 早期 API:const MyComponent = React.createClass({render() {return <div>{this.props.text}</div>;} });
-
extends React.Component
使用 ES6 类继承语法:class MyComponent extends React.Component {render() {return <div>{this.props.text}</div>;} }
2. this
绑定问题
-
React.createClass
自动绑定方法中的this
,无需手动处理:const Component = React.createClass({handleClick() {console.log(this); // 正确指向组件实例},render() {return <button onClick={this.handleClick}>Click</button>;} });
-
extends React.Component
方法中的this
默认不绑定,需手动处理(常见方案):-
构造函数中绑定:
class Component extends React.Component {constructor(props) {super(props);this.handleClick = this.handleClick.bind(this);}handleClick() { /* ... */ } }
-
箭头函数(类属性):
class Component extends React.Component {handleClick = () => { /* ... */ }; }
-
3. 状态与 Props 初始化
-
React.createClass
使用特定方法定义初始状态和默认 Props:const Component = React.createClass({getInitialState() {return { count: 0 };},getDefaultProps() {return { text: "Hello" };} });
-
extends React.Component
在构造函数中初始化状态,通过静态属性定义默认 Props:class Component extends React.Component {constructor(props) {super(props);this.state = { count: 0 };}static defaultProps = { text: "Hello" }; }
4. Mixins 支持
-
React.createClass
支持mixins
实现代码复用:const LoggerMixin = {componentDidMount() {console.log("Component mounted");} };const Component = React.createClass({mixins: [LoggerMixin],// ... });
-
extends React.Component
不支持 Mixins(ES6 类不支持多重继承),改用高阶组件(HOC)或 Hooks:// 高阶组件替代方案 function withLogger(Component) {return class extends React.Component {componentDidMount() {console.log("Component mounted");}render() {return <Component {...this.props} />;}}; }
三、为什么 React.createClass
被废弃?
- ES6 类语法的普及:ES6 类更符合 JavaScript 标准,减少 React 专属 API 的学习成本。
this
绑定灵活性:手动绑定让开发者更清楚代码行为,避免隐式绑定带来的困惑。- 性能优化:类组件与现代 React 特性(如 Fiber 架构)更兼容。
- Mixins 的替代方案:Mixins 易导致命名冲突和代码耦合,HOC 和 Hooks 提供了更清晰的复用模式。
四、迁移建议
-
旧项目迁移:使用
create-react-class
包过渡,或手动改为类组件/函数组件。 -
新项目:直接使用 函数组件 + Hooks(React 16.8+ 推荐):
function MyComponent({ text }) {const [count, setCount] = useState(0);return <div>{text}</div>; }
五、总结
React.createClass
是 React 早期的组件定义方式,已逐渐被淘汰。extends React.Component
是 ES6 类组件的标准写法,更符合现代 JavaScript 规范。- 函数组件 + Hooks 已成为 React 的主流开发模式,兼具简洁性和功能性。
23.React事件与普通的html事件有什么区别
React 事件与普通 HTML 事件在实现机制和使用方式上有显著区别,以下是主要差异及详细说明:
一、核心区别对比表
特性 | React 事件 | 普通 HTML 事件 |
---|---|---|
事件命名 | 驼峰命名(如 onClick ) | 全小写(如 onclick ) |
事件绑定 | JSX 中直接绑定函数(如 onClick={fn} ) | HTML 属性或 addEventListener |
事件对象 | 合成事件(SyntheticEvent ) | 原生 DOM 事件对象 |
事件委托 | 自动委托到根容器(React 17+) | 需手动委托(如 addEventListener ) |
默认行为阻止 | 必须显式调用 e.preventDefault() | 可通过 return false 或 e.preventDefault() |
this 绑定 | 需手动绑定(类组件)或使用箭头函数 | 默认指向触发事件的元素(非严格模式) |
性能优化 | 事件池复用,异步需 e.persist() | 无复用机制 |
跨浏览器兼容性 | 统一封装,无需处理浏览器差异 | 需手动处理(如 IE 兼容性) |
二、详细区别解析
1. 事件命名与绑定方式
-
React 事件
-
使用驼峰命名(如
onClick
、onChange
)。 -
在 JSX 中直接绑定函数,无需字符串:
<button onClick={handleClick}>Click</button>
-
-
HTML 事件
-
使用全小写属性名(如
onclick
)。 -
通过字符串或
addEventListener
绑定:<button onclick="handleClick()">Click</button> <!-- 或 --> <script>document.querySelector("button").addEventListener("click", handleClick); </script>
-
2. 事件对象(Event Object)
-
React 事件
- 使用 合成事件(SyntheticEvent) ,是对原生事件的跨浏览器包装。
- 通过事件池复用,提升性能(异步访问需调用
e.persist()
)。 - 统一接口,无需处理浏览器差异(如
e.stopPropagation()
在所有浏览器中一致)。
const handleClick = (e) => {e.preventDefault(); // 阻止默认行为e.persist(); // 保留事件对象供异步使用 };
-
HTML 事件
- 直接使用原生 DOM 事件对象。
- 需处理浏览器兼容性(如 IE 的
window.event
)。
element.onclick = function(e) {e = e || window.event; // 处理 IE 兼容e.stopPropagation(); };
3. 事件委托机制
-
React 事件
- React 17+ 将事件委托到根容器(如
ReactDOM.createRoot()
挂载的节点),而非document
。 - 减少内存占用,支持多 React 版本共存。
- React 17+ 将事件委托到根容器(如
-
HTML 事件
-
需手动实现事件委托:
document.getElementById("parent").addEventListener("click", (e) => {if (e.target.matches("button")) {// 处理子元素点击} });
-
4. this
绑定问题
-
React 事件(类组件)
-
类组件中需手动绑定
this
,或使用箭头函数:class Button extends React.Component {handleClick() { console.log(this); } // this 默认未绑定render() {return (<button onClick={this.handleClick.bind(this)}>Bind</button>// 或<button onClick={() => this.handleClick()}>Arrow</button>);} }
-
-
HTML 事件
-
内联事件处理函数中
this
默认指向元素:<button onclick="console.log(this)">Click</button> <!-- 输出按钮元素 -->
-
通过
addEventListener
绑定时,this
默认指向元素:button.addEventListener("click", function() {console.log(this); // 输出按钮元素 });
-
5. 默认行为阻止
-
React 事件
- 必须显式调用
e.preventDefault()
。
const handleSubmit = (e) => {e.preventDefault(); // 阻止表单提交 };
- 必须显式调用
-
HTML 事件
-
可通过
return false
(仅限属性绑定)或e.preventDefault()
:<form onsubmit="return false"> <!-- 阻止提交 --> <!-- 或 --> <form onsubmit="handleSubmit(event)"> <script>function handleSubmit(e) {e.preventDefault();} </script>
-
三、React 事件系统的优势
- 跨浏览器一致性
合成事件屏蔽了浏览器差异(如event.target
的兼容性处理)。 - 性能优化
事件池机制减少对象创建开销,提升性能。 - 声明式绑定
JSX 中直接绑定函数,代码更简洁。 - 自动清理
组件卸载时自动解绑事件,避免内存泄漏。
四、示例对比
React 事件处理
function App() {const handleClick = (e) => {e.preventDefault();console.log("React event:", e.nativeEvent);};return <a href="#" onClick={handleClick}>React Link</a>;
}
HTML 事件处理
<a href="#" onclick="handleClick(event)">HTML Link</a>
<script>function handleClick(e) {e.preventDefault();console.log("HTML event:", e);}
</script>
五、总结
- React 事件 是跨浏览器封装的高层抽象,提供一致的开发体验。
- HTML 事件 直接操作原生 DOM,需处理浏览器差异和手动管理。
- 掌握两者区别有助于避免常见陷阱(如
this
绑定、默认行为处理),并写出更健壮的 React 代码。
24.为什么useState要使用数组而非对象?
在 React 的 useState
中,返回数组 [state, setState]
而非对象 { state, setState }
,是出于设计上的灵活性和开发便利性考虑。以下是具体原因和解析:
一、核心原因:解构赋值的灵活性
1. 自由命名变量
-
使用数组解构时,开发者可以自定义变量名,避免对象属性名的强制约束:
// 数组解构:自由命名 const [count, setCount] = useState(0); const [user, setUser] = useState({ name: "John" });// 对象解构(假设):必须固定属性名 const { state: count, setState: setCount } = useState(0); // 冗长且不直观
2. 多状态场景更简洁
-
当组件需要多个
useState
时,数组解构更清晰:// 数组解构:直接且无命名冲突 const [count, setCount] = useState(0); const [text, setText] = useState("");// 对象解构(假设):需要重复属性名 const { state: count, setState: setCount } = useState(0); const { state: text, setState: setText } = useState(""); // 命名冲突!
二、对比:数组 vs 对象返回值的差异
特性 | 数组返回值 | 对象返回值(假设) |
---|---|---|
变量命名 | 自由命名(如 [count, setCount] ) | 固定属性名(如 { state, setState } ) |
多状态声明 | 无命名冲突 | 需重复解构或别名,易冲突 |
代码简洁性 | 更简洁 | 更冗长 |
一致性 | 所有 Hook 统一返回数组(如 useReducer ) | 无统一标准 |
四、示例:数组解构的实际优势
1. 自定义命名
// 状态1:计数器
const [count, setCount] = useState(0); // 状态2:输入框文本
const [text, setText] = useState("");// 状态3:用户数据
const [user, setUser] = useState({ name: "John" });
2. 忽略不需要的值
-
若 Hook 返回对象,无法跳过中间值;数组解构允许占位符:
// 假设 useSomeHook 返回 [a, b, c] const [a, , c] = useSomeHook(); // 忽略第二个值// 若返回对象,需解构所有属性 const { a, b, c } = useSomeHook(); // 必须处理 b
总结
- 返回数组:为了提供命名自由和多状态声明的简洁性。
- 参数类型自由:
useState
的初始值可以是任意类型(数字、对象、数组等),与返回值形式无关。 - 设计一致性:所有 React Hook 遵循相似的返回值约定,降低学习成本。
因此,const [xxx, setXxx] = useState(0)
中的数组解构是 React 的刻意设计,旨在提升开发体验和代码灵活性。
25.React为什么要使用hooks
React 引入 Hooks 的核心目标是解决类组件的设计缺陷,提升代码的可维护性、逻辑复用性和开发体验。以下是具体原因和 Hooks 的核心优势:
总结:为什么使用 Hooks?
- 代码更简洁:消除类组件的冗余代码。
- 逻辑更复用:自定义 Hook 实现高效逻辑共享。
- 开发更高效:函数式编程减少心智负担。
- 未来更友好:适配 React 新特性,代表未来方向。
一、解决类组件的痛点
1. 逻辑复用困难
-
类组件时代:通过高阶组件(HOC)、Render Props 等模式复用逻辑,导致嵌套地狱(Wrapper Hell)和代码冗余。
-
Hooks 方案:通过自定义 Hook(如
useFetch
、useAuth
)直接复用状态逻辑,无嵌套、更简洁。// 自定义 Hook:复用数据请求逻辑 function useFetch(url) {const [data, setData] = useState(null);useEffect(() => {fetch(url).then(res => res.json()).then(setData);}, [url]);return data; }// 在组件中使用 function UserProfile({ userId }) {const user = useFetch(`/api/users/${userId}`);return <div>{user?.name}</div>; }
2. 生命周期方法分散逻辑
-
类组件问题:相关逻辑被拆分到不同生命周期(如
componentDidMount
、componentDidUpdate
),代码难以维护。class Timer extends React.Component {componentDidMount() {this.timer = setInterval(() => {/* 更新状态 */}, 1000);}componentWillUnmount() {clearInterval(this.timer);}// 相关逻辑被拆分到多个方法 }
-
Hooks 方案:用
useEffect
合并生命周期逻辑,按功能组织代码。function Timer() {useEffect(() => {const timer = setInterval(() => {/* 更新状态 */}, 1000);return () => clearInterval(timer); // 清理逻辑}, []); }
3. this
绑定问题
-
类组件问题:需要手动绑定
this
,或使用箭头函数,易出错且代码冗余。class Button extends React.Component {handleClick() { /* 需要绑定 this */ }render() {return <button onClick={this.handleClick.bind(this)}>Click</button>;} }
-
Hooks 方案:函数组件无
this
,直接使用闭包变量,避免绑定问题。function Button() {const handleClick = () => {/* 直接访问 props/state */};return <button onClick={handleClick}>Click</button>; }
二、Hooks 的核心优势
1. 函数式编程范式
- 更简洁的代码:函数组件比类组件代码量更少,结构更清晰。
- 更少的样板代码:无需定义
class
、constructor
或render
方法。
2. 逻辑与 UI 解耦
-
关注点分离:通过自定义 Hook 将业务逻辑抽离,UI 组件只负责渲染。
// 逻辑复用层:useCounter function useCounter(initialValue) {const [count, setCount] = useState(initialValue);const increment = () => setCount(c => c + 1);return { count, increment }; }// UI 组件:只负责渲染 function Counter() {const { count, increment } = useCounter(0);return <button onClick={increment}>{count}</button>; }
3. 更好的 TypeScript 支持
- 函数组件 + Hooks 的类型推断更直观,避免类组件中
this
类型问题。
4. 性能优化更精细
- 通过
useMemo
、useCallback
精确控制重渲染,避免不必要的计算。
三、Hooks 如何推动 React 生态
- 渐进式迁移:支持类组件与函数组件共存,项目可逐步迁移。
- 社区创新:自定义 Hooks 催生了大量开源库(如
react-query
、ahooks
)。 - 未来兼容性:React 新特性(如并发模式)优先支持 Hooks。
四、Hooks 的局限性
- 学习曲线:需理解闭包、依赖数组等概念。
- 规则限制:必须遵守 Hooks 的调用顺序(不能在条件或循环中使用)。
Hooks 是 React 对“关注点分离”和“代码复用”的终极答案,让开发者以更优雅的方式构建可维护的现代应用。
26.在React中如何实现代码分割
在 React 中实现代码分割(Code Splitting)是优化应用性能的关键手段,能够减少初始加载时间,提升用户体验。以下是 5 种核心方法及详细实现步骤:
一、使用 React.lazy
+ Suspense
(组件级分割)
适用场景:按需加载非首屏组件(如弹窗、复杂模块)。
实现步骤:
- 用
React.lazy
动态导入组件。 - 用
Suspense
包裹组件,提供加载状态。
import React, { Suspense } from 'react';// 动态导入组件(Webpack 自动分割代码)
const LazyComponent = React.lazy(() => import('./LazyComponent'));function App() {return (<div><Suspense fallback={<div>Loading...</div>}><LazyComponent /> {/* 使用时才加载 */}</Suspense></div>);
}
二、基于路由的代码分割(路由级分割)
适用场景:SPA 中按路由拆分代码(如不同页面)。
实现步骤(以 React Router v6 为例):
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import React, { Suspense } from 'react';const Home = React.lazy(() => import('./routes/Home'));
const About = React.lazy(() => import('./routes/About'));function App() {return (<BrowserRouter><Suspense fallback={<div>Loading Page...</div>}><Routes><Route path="/" element={<Home />} /><Route path="/about" element={<About />} /></Routes></Suspense></BrowserRouter>);
}
三、动态 import()
语法(手动控制加载时机)
适用场景:在事件触发时加载代码(如点击按钮后加载模块)。
实现步骤:
function App() {const [module, setModule] = useState(null);const loadModule = async () => {const { default: DynamicModule } = await import('./DynamicModule');setModule(<DynamicModule />);};return (<div><button onClick={loadModule}>Load Module</button>{module}</div>);
}
四、使用第三方库 @loadable/component
适用场景:更灵活的代码分割(支持服务端渲染、预加载等)。
实现步骤:
-
安装库:
npm install @loadable/component
-
使用
loadable
包装组件:import loadable from '@loadable/component';const LoadableComponent = loadable(() => import('./Component'), {fallback: <div>Loading...</div>, });function App() {return <LoadableComponent />; }
五、Webpack 配置优化(文件名哈希、分组)
适用场景:精细化控制代码块(chunk)生成策略。
配置示例(webpack.config.js
):
module.exports = {output: {filename: '[name].[contenthash].js',chunkFilename: '[name].[contenthash].chunk.js',},optimization: {splitChunks: {chunks: 'all',cacheGroups: {vendors: {test: /[\/]node_modules[\/]/,name: 'vendors',},},},},
};
六、最佳实践与注意事项
-
优先分割大组件/路由:对性能提升最明显的部分优先处理。
-
预加载关键资源:使用
webpackPrefetch
提前加载未来可能需要的模块。const Component = React.lazy(() => import(/* webpackPrefetch: true */ './Component' ));
-
错误边界处理:用
ErrorBoundary
捕获加载失败。class ErrorBoundary extends React.Component {state = { hasError: false };static getDerivedStateFromError() { return { hasError: true }; }render() {return this.state.hasError ? <div>Error!</div> : this.props.children;} }// 使用 <ErrorBoundary><Suspense fallback="Loading..."><LazyComponent /></Suspense> </ErrorBoundary>
-
避免过度分割:每个分割块会增加 HTTP 请求,平衡数量与体积。
总结
- 轻量级组件 →
React.lazy
+Suspense
- 路由级分割 → 结合 React Router
- 精细控制 →
@loadable/component
或动态import()
- 打包优化 → Webpack 配置
通过合理应用代码分割,可显著提升 React 应用的加载速度与运行时性能。