React前端面试题总结

683 阅读40分钟

【基础】

1.简述React及其优缺点

React 是一个由 Facebook 开发的开源 JavaScript 库,用于构建用户界面,尤其是单页应用程序中的视图层。它于 2013 年发布,并迅速获得了广泛的流行,主要用于构建 Web 应用程序的前端。React 的核心特性包括组件化、声明式编程、虚拟 DOM 和 JSX 语法。

React 的特性

组件化

  • React 通过组件化的方式提高了代码的复用性,每个组件都有自己的状态和生命周期,可以单独管理和渲染。 声明式编程
  • React 采用声明式编程范式,开发者只需关心每个组件应该呈现什么,而不是如何操作 DOM。

虚拟 DOM (Virtual DOM

  • React 在内存中创建了 DOM 的轻量级副本,即虚拟 DOM,通过比较新旧虚拟 DOM 的差异来高效地更新真实 DOM。

JSX

  • JSX 是 JavaScript 的扩展语法,允许在 JavaScript 代码中书写类似 HTML 的结构,增强了代码的可读性和易维护性。

单向数据流

  • React 强调数据的单向流动,通过 props 向下传递数据,通过状态(state)和生命周期方法管理组件的行为。

生态系统

  • React 拥有一个庞大的生态系统,包括路由(React Router)、状态管理(Redux、MobX)、静态网站生成(Gatsby)等。

React 的优点

高效:虚拟 DOM 减少了不必要的真实 DOM 操作,提高了性能。

灵活:可以与其他库或框架(如 Redux、MobX)结合使用,提供灵活的开发方式。

组件复用:组件化结构提高了代码的复用性和测试性。

强大的社区支持:社区活跃,资源丰富,有大量开源项目和工具。

跨平台:通过 React Native 可以使用相同的组件模型构建移动端应用。

易学:相对于其他前端框架,React 的学习曲线较为平缓。

React 的缺点

只关注视图层React 只是一个库,而不是完整的框架,因此需要开发者选择其他库来处理路由、状态管理等。

学习生态系统:虽然 React 本身易于学习,但其生态系统复杂,需要学习额外的库和工具。

频繁的更新React 生态系统中频繁的更新和变化可能导致开发者需要不断学习新的最佳实践。

性能问题:在大型复杂应用中,虚拟 DOM 依然可能成为性能瓶颈,需要优化。

JSX 的争议:一些开发者可能不喜欢 JSX 语法或者认为在 JavaScript 中混入 HTML 结构违背了关注点分离

2.为什么虚拟Dom能提高性能

  • 减少直接的dom操作:实际的dom操作是相当昂贵的,因为他们可能会导致页面重排和重绘。虚拟dom的方式在JS内存中进行dom表示的抽象,这使得任何的更新和计算都可以现在内存中完成,而不是在实际的dom上进行
  • 批量更新和高效的差异对比:在虚拟dom中,组件状态发生变化时,React会创建一个新的虚拟dom树并将其与当前树进行差异对比。React会计算出最小的一组变更,然后再实际的dom上应用他们。这意味着React会批量执行所有dom更新,从而最小化了重排和重绘的发生。
  • 避免不必要的dom更新:使用虚拟dom,react可以知晓具体哪些部分的UI是需要更新的,哪些部分是不变的。对于不需要变化的部分,React可以避免进行不必要的dom操作。
  • 组件及更新:React的组件化结构进一步提高了性能,因为每个组件的状态是局部的。当组件的状态更新时,只有受影响的组件及其子组件会进行差异对比和重新渲染,未受影响的组件将保持不变

3.React18新增特性

  • 并发渲染(Concurrent Rendering) : React 18 引入了并发渲染,使得 React 可以在渲染时更好地利用计算资源。这允许 React 在保持界面响应的同时执行渲染工作。

  • 自动批处理(Automatic Batching) :

    React 18 对 setState 和其他更新机制进行了改进,现在可以自动批处理更多类型的更新,而不仅限于 React 事件处理函数内。这意味着异步操作(如 Promises、setTimeout、原生事件处理等)也可以享受到批处理带来的性能优化。

  • 新的根 API(createRoot

    React 18 提供了新的根 API,createRoot,它替代了旧的 ReactDOM.rendercreateRoot 提供了对并发特性的支持,并允许你选择加入并发更新。

  • 新的挂起模型(Suspense

    React 18 加强了挂起(Suspense)特性的支持,现在开发者可以更好地控制组件加载状态,实现更平滑的用户体验。例如,可以使用 Suspense 来定义数据加载时的占位内容。

  • 新的 Hook(useId

    React 18 引入了一个新的 Hook,useId,用于生成唯一的客户端和服务端(SSR)一致性的 ID,以解决服务端渲染和客户端渲染 ID 不一致的问题。

  • 启动严格模式检查(Strict Mode)的新功能

    React 18 的严格模式(Strict Mode)现在能够检测更多的潜在问题,包括意外的副作用、过时的 API 调用等。

  • 新的测试工具(React Testing Library

    随着 React 18 的发布,测试 React 应用的库也进行了更新,以支持并发特性和新的渲染行为

4. props和state的区别

在 React 中,propsstate 都是用于管理和传递数据的,但它们有一些关键区别:

props(属性):

  1. Read-Only(只读)props 是从父组件传递到子组件的。子组件不应该修改接收到的 props
  2. 外部传递props 用于使组件可配置,从外部给组件传递数据,类似于函数的参数。
  3. 不变性props 一旦被定义,在组件的生命周期中保持不变。
  4. 跨组件通信props 可以用于父子组件、子组件或兄弟组件之间的通信,主要用来从上层传递数据到下层。
  5. 状态提升:当多个组件需要共享状态时,通常会将状态提升到它们共同的父组件中,然后通过 props 将状态传递到每个子组件。

state(状态):

  1. Mutable(可变)state 是组件内部的,可以被组件自身更改(但要使用 setState 方法更新)。
  2. 内部管理state 是组件的内部状态,不从外部接收,组件根据 state 来渲染 UI 并使其变得可交互。
  3. 变化性state 可以随时根据用户的交云或网络响应等改变,从而触发组件的重新渲染。
  4. 组件封装state 用于一个组件的封装,通常不会直接被外部访问或修改。
  5. 响应状态变化state 的改变会导致组件的重新渲染,以反映新的状态。

5. react组件通信

React 应用中,组件通信是非常重要的一部分,因为组件需要分享状态和行为。以下是 React 中组件之间进行通信的几种常用方式:

父子组件通信

  • Props(属性) :父组件可以通过 props 向子组件传递数据和回调函数。子组件通过读取这些 props 来接收数据,并且可以通过调用传递下来的回调函数与父组件通信
  • React Context API:当需要向多个层级嵌套的子组件传递数据时,可以使用Context API避免 props 的逐层传递。

兄弟组件通信

兄弟组件之间的通信通常通过它们共同的父组件来协调:

状态提升(Lifting State Up :将共享状态提升到父组件中管理,然后通过props向兄弟组件传递状态或回调函数。

跨组件通信

在复杂的应用中,可能需要跨越多个组件层次进行通信,除了上述的 Context API,还可以使用以下方法:

  • 状态管理库:例如 Redux、MobX 等状态管理库可以在全局范围内管理状态,任何组件都可以连接到这个状态库并进行读写操作。
  • 自定义事件系统:使用全局事件发射器(Event Emitter)或事件总线(Event Bus),组件可以广播事件,其他组件可以监听并响应这些事件。

组件组合

组件可以通过插槽(Slots)或子组件(Children)来进行内容组合,这不仅能实现布局复用,也可以通过组合不同的子组件来传递数据

6. 简述Context(为什么不推荐使用)

Context是React提供的一个功能,它允许组件树中跨层级地传递数据,无需在每个层级手动的通过props传递。Context提供了一种方式,可以共享内些对于一个组件树而言是“全局”的数据,例如主题或首选语言

如何工作

Context 主要包括两个部分:ProviderConsumer

  • Provider 是一个组件,它将值放置到 Context 中。任何嵌套在它下面的组件都可以访问这个值,而不管它们在组件树中的深度如何。
  • Consumer 是一个组件,它读取 Context 的值,并使用它。在新版的 React 中,可以使用 useContext Hook 来更简便地消费 Context
// 创建一个 Context 对象
const MyContext = React.createContext(defaultValue);

// 使用 Provider 组件包裹子组件,以提供 Context 值
<MyContext.Provider value={/* 某个值 */}>
  {/* 子组件 */}
</MyContext.Provider>

// 在子组件中,使用 Consumer 组件或 useContext Hook 来访问 Context 值
<MyContext.Consumer>
  {value => /* 基于 Context 值进行渲染 */}
</MyContext.Consumer>

// 或者使用 useContext Hook
const value = useContext(MyContext);

不推荐滥用Context

  • 组件耦合性:使用Context 使得消费者组件依赖于Context 结构,增加了组件之间的耦合性,可能会导致组件的复用性降低
  • 组件重渲染:当Context 发生变化时,所有使用它的组件都会重新渲染。如果Context 在高频更新,或者包含了大量的消费者组件,可能会导致性能问题
  • 不容易追踪:使用Context 传递的数据不如props 容易追踪。在组件树中查找数据来源可能会比较困难,因为它可能是在组件树的任何地方被定义的
  • 更新复杂性:如果需要在组件树深处更新Context ,可能需要使用额外的模式,如组合Context 提供者和高阶组件,从而增加了复杂性

7. setState是同步的还是异步的

既可以是同步的也可以是异步的,取决于它调用的上下文

  • 在事件处理中调用setState,react会将状态更新批量处理,这时候就是异步调用;
  • 在函数组件中使用hooks(如useState,useRender),状态更新在逻辑上是异步的,但他们不会合并多个状态更新
  • 在自定义的异步函数中通常表现为同步的,如setTimeOut、Promise、原生事件的回调中setState,react无法控制这些异步函数,因此状态会立即更新
  • 在某些生命周期或react同步模式中,也可能表现为同步

8. setState的第二个参数是做什么的

setState 方法在 React 组件中用于更新组件的状态,并触发组件的重新渲染流程。这个方法可以接受两个参数:

  • 第一个参数是一个新的状态值或者一个函数,用于计算基于当前状态的新状态。
  • 第二个参数是一个可选的回调函数,这个函数会在 setState 导致的状态更新和组件重新渲染完成之后被调用。

由于 setState 是异步的,React 可能会对多个 setState 调用进行批处理以提高性能。这意味着你不能保证立即在 setState 之后就获得最新的状态,因为实际的状态更新可能会被延迟。 因此,如果你需要在状态更新之后执行某些操作,并且这些操作依赖于最新的状态,你应该使用 setState 的第二个参数——回调函数。这个回调函数确保了你的代码只会在 React 完成状态更新和组件渲染之后运行。

9. setState批量更新的过程是什么

React 的 setState 方法在默认情况下会对状态更新进行批量处理(batching),以提高应用性能。批量更新意味着 React 并不会为每次 setState 调用立即更新组件,而是积累一定量的状态改变,然后一次性更新组件。这个过程主要发生在 React 管理的事件处理和生命周期方法中。以下是批量更新的基本过程:

  • 当事件处理函数或生命周期方法执行时,React 会设置一个标记,表明现在正在执行由 React 管理的上下文。
  • 在这个上下文中,所有的 setState 调用都会被收集到一个队列中,并不会立即执行状态的合并和组件的更新。
  • 一旦事件处理或生命周期方法执行完毕,React 会离开由它管理的上下文。
  • React 检查是否有积累的状态更新,如果有,它会将这些更新合并到一个或多个状态变更中。这主要是为了避免不必要的重复渲染和计算,因为多次 setState 可能只需要一次组件更新。
  • React 开始执行更新过程,调用组件的生命周期方法,如 shouldComponentUpdatecomponentWillUpdaterender,和 componentDidUpdate。在这个过程中,组件将基于累积的状态变更重新渲染。
  • 组件的新虚拟 DOM 树将与旧的虚拟 DOM 树进行比较,React 会计算出需要进行的最少修改,然后更新实际的 DOM。

需要注意的是,在 React 16 以前,批量更新只在 React 合成事件和生命周期方法中发生。如果 setState 被调用在异步代码、setTimeout 或原生事件处理函数中,则每个 setState 都可能会导致单独的同步更新,而不是批量更新。

10. 构造函数中调用super(props)的作用

React 中,当你定义一个类组件并且它继承自 React.Component,在其构造函数中调用 super(props) 是非常重要的。这样做有几个作用:

  • 初始化父类构造函数:super() 调用会初始化父类(在这里是 React.Component)的构造函数。如果你不调用 super()this 关键字就无法在构造函数中被使用,因为子类没有被正确初始化。这将导致一个引用错误。
  • 传递props到父类:在调用 super(props) 时,你将 props 传递给父类的构造函数。这样,React 在构建组件时可以设置 this.props,确保你在组件的任何地方(包括生命周期方法中)都可以访问到 props

11. React生命周期

在 React 16.3 版本之前,组件生命周期可分为以下三个主要阶段:

  1. 挂载(Mounting)
  2. 更新(Updating)
  3. 卸载(Unmounting)

在 React 16.3 之后,部分生命周期方法被认为是不安全的,并且在未来的版本中将被弃用。React 团队引入了新的生命周期方法来替代它们。以下是各个生命周期阶段详细的方法列表:

挂载(Mounting)

  1. constructor(props)

    • 创建组件时最先被调用,用于初始化状态和绑定事件处理器。
  2. static getDerivedStateFromProps(props, state)

    • 在挂载和更新时被调用。用于根据传入的 props 来设置 state。
  3. render()

    • 唯一必须实现的方法,返回组件的 JSX 表示。它应该是纯函数,不应包含任何改变组件状态的代码。
  4. componentDidMount()

    • 在组件挂载到 DOM 后立即调用。常用于执行 DOM 操作或发送网络请求等异步操作。

更新(Updating)

  1. static getDerivedStateFromProps(props, state)

    • 当组件接收到新的 props 或 state 时,用于根据 props 设置 state。
  2. shouldComponentUpdate(nextProps, nextState)

    • 决定是否重新渲染组件,默认返回 true。当你想要优化性能时,可以在此方法中实现复杂的比较逻辑。
  3. render()

    • 更新时也会调用 render 方法来确定是否需要更新 DOM。
  4. getSnapshotBeforeUpdate(prevProps, prevState)

    • 在 DOM 更新之前立即被调用,用于捕捉更新前的 DOM 状态。任何在此方法中返回的值都将作为参数传递给 componentDidUpdate()
  5. componentDidUpdate(prevProps, prevState, snapshot)

    • 在更新后立即被调用,适用于 DOM 操作或执行一些更多的异步请求。

卸载(Unmounting)

  1. componentWillUnmount()

    • 在组件卸载和销毁之前直接被调用。用于执行必要的清理操作,如取消网络请求、清除组件中使用的定时器等。

错误处理

  1. static getDerivedStateFromError(error)

    • 当后代组件抛出错误时,用来渲染备用 UI。
  2. componentDidCatch(error, info)

    • 捕获后代组件错误,记录错误信息,并发送错误报告。

需要注意的是,自 React 16.3 版本以来,下列生命周期方法已被认为是不安全的,未来可能会被完全弃用:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

为了使用 React 的新特性,如 Concurrent Mode,并提高应用性能和代码可维护性,强烈推荐更新和使用新的生命周期方法

12. React生命周期触发是同步还是异步的

React 组件的生命周期方法是同步调用的。它们被嵌入到 React 的更新机制中,在组件的生命周期的特定时点同步执行,以便开发者可以在正确的时间点对组件的状态进行操作。

例如,以下是常见的 React 生命周期方法(在 React 16 之前的版本):

  • componentWillMount: 在组件挂载到 DOM 之前调用,是 render 方法的前一步。
  • componentDidMount: 在组件挂载到 DOM 之后立即调用,可以在这里进行 AJAX 请求、DOM 操作等。
  • componentWillReceiveProps: 当组件接收到新的 props 时被调用,可以在这里根据 props 更新组件的 state。
  • shouldComponentUpdate: 返回一个布尔值,指示 React 是否应该继续执行更新流程。
  • componentWillUpdate: 在接收到新的 props 或 state 之前立即调用,但不能在这里调用 this.setState
  • componentDidUpdate: 在组件更新后被立即调用,可以执行 DOM 操作或发送请求。
  • componentWillUnmount: 在组件卸载和销毁之前直接调用,可以执行清理任务,如无效计时器或取消网络请求。

从 React 16.3 开始,引入了新的“安全”的生命周期方法来避免一些常见错误,并为未来的异步渲染功能做准备:

  • static getDerivedStateFromProps: 在渲染前调用,用于根据 props 的变化来更新 state。
  • getSnapshotBeforeUpdate: 在 DOM 更新前调用,用于获取更新前的 DOM 快照。
  • componentDidMountcomponentDidUpdatecomponentWillUnmount: 保持不变,仍然是同步调用。

这些生命周期方法都是在 React 的更新过程中同步调用的,以确保在渲染执行前后有机会进行必要的操作。但是,需要注意的是,虽然生命周期方法本身是同步的,它们可能会触发异步操作,如 AJAX 请求或动态导入。

在 React 18 中,引入了并发特性(Concurrent Features),允许 React 根据用户的设备能力和当前页面的加载情况分批次更新状态。这可能会影响组件的生命周期,因此开发者需要注意新的并发模式下的最佳实践。

13. class组件和函数组件有什么区别

  • 声明方式:类组件是通过ES6的类来声明的,函数组件是通过普通的JavaScript 函数或箭头函数来声明

  • 状态管理:类组件可以使用this.statethis.setState来持有和更新状态。函数组件可以用用Hooks来管理状态(useState)和副作用(useEffect)等

  • 生命周期方法:类组件可以使用生命周期方法,如 componentDidMountcomponentDidUpdatecomponentWillUnmount 等来执行代码。函数组件可以通过特定的 Hooks 来模拟生命周期行为

  • this关键字:类组件中可以使用 this 关键字访问组件实例和它的属性和方法。函数组件不需要处理this关键字

  • 简洁性:比函数组件来说,类组件的语法更为复杂,特别是需要管理 this 的绑定

  • 组件重用

    类组件可以使用高阶组件(HOCs)和 render props 等模式来重用组件逻辑

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    // 生命周期钩子
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return <button onClick={this.handleClick}>Click me</button>;
  }
}

函数组件可以通过自定义 Hooks 来重用逻辑,这通常比高阶组件和 render props 更简洁清晰

function MyComponent() {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    // 副作用钩子
  }, []);

  const handleClick = () => {
    setCount(count + 1);
  };

  return <button onClick={handleClick}>Click me</button>;
}

14. 受控组件和非受控组件有什么区别

受控组件是由Reactstate来管理表单数据的组件,非受控组件反之。主要区别在于

  • 数据管理:受控组件中表单的数据由Reactstate管理,而非受控组件中表单数据直接由DOM管理
  • 数据访问:在受控组件中,时刻能访问或更新表单的值,而在非受控组件中,只有需要时才去通过ref访问dom节点来获取值
  • 复杂性:受控组件通常需要更多的代码和事件处理来保持表单的状态,而非受控组件代码更简洁,但可能会使得状态的管理变得更加困难。
  • 性能:由于受控组件涉及到对每个键入或表单变化的响应和重渲染,如果表单非常大或复杂,肯呢个会有性能上的考量。非受控组件由于较少的状态管理,可能在某些情况下表现更好

15. 简述React Hook及其作用

  • useState: 让一个函数组件能够使用本地状态的hook。返回一个状态变量和一个用于更新这个状态的函数,每次状态更新都会重新渲染组件
const [count, setCount] = useState(0);
  • useEffect:在组件中执行副作用操作,如数据获取、订阅、手动修改dom等。它可以被看作是componentDidMountcomponentDidUpdatecomponentWillUnmount这些生命周期方法的组合
useEffect(() => {
  // 副作用逻辑
}, [count]); // 仅在count变化时更新
  • useContext:允许在组件树中访问React Context而不必使用<Context.Consumer>组件
const value = useContext(MyContext);
  • useReducer:可以在组件中管理复杂状态逻辑的Hook。它通常用于管理组件的多个子状态或当useState不足以表达富足的状态更新逻辑时
const [state, dispatch] = useReducer(reducer, initialState);
  • useCallbackuseCallback返回一个记忆化的回调函数,该函数仅在它的依赖项改变时才会更新。这对于防止不必要的渲染非常有用
const memoizedCallback = useCallback(() => {
  // 你的逻辑
}, [dependencies]);
  • useMemo:用于计算记忆化的值。这个Hook仅在依赖项改变时重新计算缓存的值。它可以用于优化性能,避免在每次渲染时都进行昂贵的计算
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • useRef:返回一个可变的ref对象,其.current属性被初始化为传递的参数。返回的对象在组件的整个生命周期内保持不变
const inputEl = useRef(null);
  • useLayoutEffectuseLayoutEffect的用法和效果类似于useEffect,但是它在所有的DOM变更之后同步调用,可以用来读取DOM布局并同步触发重渲染
useLayoutEffect(() => {
  // 读取布局并同步触发重渲染的逻辑
});

-useImperativeHandleuseImperativeHandle用于在使用ref时自定义暴露给父组件的实例值。它应与forwardRef一起使用。

useImperativeHandle(ref, () => ({
  focus: () => {
    // 实例方法
  }
}));

16. useEffect和useLayoutEffect区别

useEffect

  • 执行时机:useEffect 在组件渲染到屏幕之后执行,不会阻塞 DOM 更新,因此适用于大多数副作用场景。由于它在浏览器完成布局与绘制后运行,这使得它适合执行不需要立即反映到屏幕输出的操作,如发送网络请求、设置订阅和定时器等。
  • 执行顺序:延迟执行,不会影响页面的可见性。
  • 使用场景:大部分的副作用逻辑,除非需要同步执行(比如更新 DOM)。

useLayoutEffect

  • 执行时机:useLayoutEffect 与 componentDidMount 和 componentDidUpdate 生命周期方法具有相同的调用时机。它在所有的 DOM 变化之后同步调用,但在浏览器对屏幕进行绘制之前执行,这意味着你可以在浏览器绘制前同步地读取布局信息(如获取元素尺寸、位置等)并进行修改,以避免可能的布局抖动(layout thrashing)。
  • 执行顺序:同步执行,可以在浏览器绘制之前更新 DOM,以避免视觉上的不一致。
  • 使用场景:需要同步调整布局或 DOM,并且对渲染性能有要求的情况。例如,计算和修改 DOM 尺寸或位置时使用。

区别总结

  • 执行时机不同useEffect 是在浏览器完成渲染后异步执行;而 useLayoutEffect 则是在所有 DOM 变更后、浏览器绘制前同步执行。
  • 用途不同useEffect 用于执行大多数副作用,而 useLayoutEffect 用于处理需要同步执行的与布局相关的更新。
  • 性能考虑:频繁使用 useLayoutEffect 可能会导致性能问题,因为它会在每次渲染后同步执行,可能会阻塞视觉更新,导致视觉上的延迟。通常,应当首选 useEffect。 在大多数情况下,useEffect 足以满足需要,但如果你遇到由于 DOM 更新导致的闪烁或不一致,并且需要立即修正,那么 useLayoutEffect 可能是更好的选择。

17. useMemo和useCallback有什么区别

他们的主要用途是优化组件的性能,尤其是在处理昂贵的计算和不必要的渲染时。主要区别在于

  • 用途不同:useMemo用于记忆化复杂计算的结果,useCallback用于记忆化函数的引用。
  • 返回值不同:useMemo返回的是函数执行的结果,useCallback返回的是函数本身
  • 优化点不同:useMemo是为了避免执行昂贵的计算,useCallback是为了避免渲染性能开销或避免对子组件进行不必要的渲染

18. 如何让useEffect支持async/await

useEffct中不支持async函数,因为async函数返回的是一个promise对象,而useEffect期望的返回值要么是一个清理函数,要么什么都不返回

  • 使用IIFE(立即调用的函数表达式) image.png
  • 定义一个普通异步函数,然后调用该函数, image.png 这两种情况,异步操作都被正确的封装在useEffect内部,不违反useEffect规则

19. React Hook闭包陷阱及解决方案

闭包陷阱:这个问题通常发生在一个函数组件内部的某个函数捕获了它被创建时的props或state值,当props或state更新时,该函数仍然保留旧的值 image.png 解决方案

  • 使用函数式更新,直接传递一个函数,而不是一个值
  • useEffect中添加依赖
  • 使用useRef来跟踪最新值
  • 可以在useEffect内部定义函数,这样自然能访问到最新

20. react-router里的 <Link> 标签和<a>标签有什么区别

  • <a>标签
    • <a>标签是HTML中的标准链接元素,用于从当前页面跳转到指定的URL,
    • 使用<a>标签浏览器会发送一个新的GET请求到服务器,并加载新页面的HTML,这意味着整个页面会被重新加载
    • 在单页应用中使用<a>标签会导致应用重新启动,因为整个页面都被重新获取和渲染了,这会导致不必要的性能损耗,并丢失应用状态
  • <Link>标签
    • <Link>是React Router提供的一个组件,用于在应用内部进行导航而不会触发页面刷新
    • <Link>不会向服务器重新发送新的请求
    • 使用同时ink>可以保持单页应用的状态不变,因为它只更新必要的组件,而不是更新加载整个页面
    • <Link>通常接收一个to属性,用于指定路由路径

【进阶】

1.React高阶组件(HOC)的理解

  • 什么是高阶组件:复用组件逻辑的一种高级技巧。一个高阶组件就是一个函数,它接受一个组件作为参数并返回一个新的组件。高阶组件的主要目的是对传入的组件进行修改和扩展,并不会改变原组件,有助于代码的模块化和维护
  • 一个高阶组件通常做以下几件事:
    • 传递不相关的props到被包裹的组件
    • 通过ref回调反向继承
    • 抽象状态,使得多个组件可以共享同样的逻辑
    • 为组件添加额外的生命周期处理逻辑

2. React性能优化的方法

  • 使用React.memo进行组件记忆优化:对于函数组件,可以使用React.memo来防止不必要的渲染。如果组件的props没有发生变化,React将跳过渲染该组件和它的子组件
  • 使用useMemouseCallback: 利用useMemo来缓存复杂计算的结果,使用useCallback来记住函数的引用可以避免无谓的计算和渲染
  • 避免内联函数和对象:在渲染方法中直接定义的内联函数和对象会在每次渲染时创建新的引用,这可能导致子组件不必要的重渲染。尽量将函数和对象定义在组件外部,或使用useCallbackuseMemo来避免这种情况。
  • 拆分组件:将大组件拆分成小的、可复用的组件,这样可以减少单个组件的复杂性,并且当状态变化时,React只需要重新渲染需要更新的那部分组件。
  • 使用shouldComponentUpdateReact.PureComponent: 对于类组件,可以使用shouldComponentUpdate生命周期方法或继承自React.PureComponent来避免不必要的更新。PureComponent对组件的propsstate进行浅比较,来决定是否需要重新渲染组件
  • 优化列表渲染:当渲染列表时,应当使用唯一的key值,这有助于React在重新渲染时更高效地识别列表项的变化。避免使用数组索引作为key,尤其是在列表项可能会被重新排序或删除的情况下。
  • 懒加载和组件路由:使用React.lazy来实现组件的懒加载,这可以减少应用的初始加载大小。对于路由,可以结合React.lazySuspense来按需加载路由组件。

3. 简述React.forwardRef及其作用

React.forwardRef允许将ref从父组件传递给子组件。这在管理焦点、文本选择或媒体播放等场景非常有用,并能够协助进行DOM操作。也可用于高阶组件,使得能够将ref通过HOC传递给内部组件。

使用场景

转发 refs 到 DOM 组件: 当你需要从父组件直接访问子组件中的 DOM 节点时,你可以使用 forwardRef在高阶组件(HOCs)中转发 refs: 如果你创建了一个高阶组件,你可能想要将父组件传递的 ref 自动转发到 HOC 内部包装的组件。

基本用法

forwardRef 接受一个渲染函数作为参数,这个函数接受 propsref 参数并返回一个 React 节点。以下是一个简单的例子:

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 父组件中使用 FancyButton 组件
class ParentComponent extends React.Component {
  constructor(props) {
    super(props);
    this.buttonRef = React.createRef();
  }

  componentDidMount() {
    // 可以直接访问 DOM button 元素
    this.buttonRef.current.focus();
  }

  render() {
    // 将 ref 传递给子组件
    return <FancyButton ref={this.buttonRef}>Click me!</FancyButton>;
  }
}

在这个例子中,FancyButton 是一个通过 forwardRef 创建的组件,它将接收到的 ref 转发到内部的 DOM button 元素上。ParentComponent 创建了一个 ref(this.buttonRef),并将其作为 ref 属性传递给 FancyButton。这样,父组件就可以控制子组件中的 DOM 元素。

提示

  • forwardRef 创建的组件会接收 ref 作为第二个参数,这点与只接收 props 的常规函数组件不同。
  • 转发的 ref 只能作为特殊的 ref 属性传递,而不能作为普通的 props

使用 React.forwardRef 只在你真正需要在父组件中直接访问子组件的 DOM 元素或组件实例时才必要。不要过度使用它,因为大多数情况下,组件的状态和提供的回调应该足以满足大部分需求。

4. React事件机制

React事件机制是对原生浏览器事件系统的封装和抽象,旨在提供浏览器的一致性,并与React的组件化和虚拟DOM特性相整合。主要特点是:

合成事件(SyntheticEvent)

React事件被封装到SyntheticEvent对象中,这是React对原生事件的跨浏览器包装。合成事件确保你在不同浏览器中得到一致的事件对象和行为。SyntheticEvent具有与原生事件相同的接口,包括stopPropagation()preventDefault()等方法,但它们一般都会进行一些正规化处理,以保证跨平台的一致性。

事件委托

React在底层使用事件委托,所有的事件都被绑定到了文档的根节点上(通常是document)。这意味着一个组件中的事件处理函数并不是直接绑定在对应的真实DOM元素上,而是通过React的事件系统在根节点上统一管理和分发。这有几个好处,比如减少内存消耗,避免频繁解绑和重绑事件监听器,以及确保即使子组件更新后,事件监听器依旧有效。

事件池

React为了提高性能,会重用SyntheticEvent对象。事件处理完成后,SyntheticEvent对象的属性会被清空,然后加入一个事件池中以供后续重用。这就意味着你不能在异步代码中引用事件对象,因为它可能已被清空。如果确实需要在异步代码中访问事件对象,你可以调用event.persist()方法来从池中移除事件对象,并避免其属性被清空。

自定义组件的事件

对于自定义的React组件,你可以将函数作为props传递给它们,并在组件内部调用这些函数来处理事件。这种模式类似于原生HTML元素的事件处理,但是你可以定义任何你需要的事件处理props。

注意事项

  • React的事件名称采用驼峰命名法(camelCase),而不是原生事件中的小写。例如,React中的click事件应该写为onClick
  • 在JSX中,你可以直接给元素添加事件处理函数,例如<div onClick={handleClick}>
  • React事件处理函数中的this指向问题可以通过箭头函数或在构造函数中绑定函数来解决。

总之,React的事件机制通过合成事件和事件委托,为开发者提供了一个高效、跨浏览器一致的事件处理方式,同时很好地与React的组件生命周期和状态管理集成。这种设计也简化了事件处理的开发模式,使开发者能够专注于应用逻辑,而不需要担心底层的事件细节和浏览器兼容性问题。

5. React强制刷新

  • 使用setState方法,即使是设置相同的state,也会导致组件重新渲染
`    this.setState({ state: this.state });`

或者使用一个函数来确保状态的更新

`    this.setState(state => ({ ...state }));`
  • 使用 forceUpdate 方法

    这个方法会强制组件调用 render 方法,跳过 shouldComponentUpdate。这并不是一个通常推荐的做法,因为它会绕过组件的正常生命周期(例如,不会触发 componentWillUpdate 和 shouldComponentUpdate

  • 改变key属性

    给组件一个新的 key 属性可以导致组件卸载并重新挂载。这是强制刷新的一种更加激进的方法,因为它不仅会导致组件重新渲染,还会丢失组件状态和生命周期。

<MyComponent key={uniqueValue} />
  • 使用hooks

    如果你在函数组件中使用hooks,可以使用useState或useRender来触发组件的重新渲染

const [, forceUpdate] = useReducer(x => x + 1, 0);

function handleForceUpdate() {
  forceUpdate();
}

6. Redux核心组件,reducer的作用

Redux是一个用于JavaScript应用的状态容器,它提供可预测化的状态管理。Redux的核心组件主要包括以下几个部分:

  • Store: Store是存储应用状态的对象。在Redux应用中,有且仅有一个store来存储整个应用状态树。
  • Actions: Actions是描述发生了什么的普通JavaScript对象。每个action都有一个type字段,用于表示要执行的动作类型。Actions可以携带数据,这些数据是用于更新状态的必要信息。
  • Reducers: Reducer是一个函数,它根据action的类型来决定如何更新状态,返回新的状态。它接受两个参数:当前状态和一个action。Reducers必须是纯函数,也就是说在相同的输入下必须返回相同的输出,且不产生副作用。
  • Action Creators: Action Creators是创建action的函数。尽管你可以直接编写action对象,但是使用函数来创建可以使代码更加清晰且易于复用。
  • Middleware: Middleware提供了一个第三方插件的扩展点,用于解决不同的问题,如异步操作、日志记录等。Middleware可以访问dispatch和getState方法,以及下一个middleware的函数,并可以选择传递action或者修改它。

Reducer的作用

Reducer的作用是指定应用状态根据action如何改变。在Redux中,整个应用的状态是通过一个或多个reducer函数来维护的。每当发生一个action时,Redux就会调用一个或多个reducer,并传递当前的状态和该action作为参数。Reducer函数会根据action的类型以及其他数据来决定如何更新状态,然后返回新的状态。

7. React与Vue的区别

  • 组件编写:VueReact都是用声明式渲染,这使得交互式用户界面更加简单和直观。但是React使用的是JSX,他是一种JS语法扩展,Vue使用的是模板语法,它是基于HTML的,允许开发者使用简单的模板标记来声明性地描述UI
  • 响应式系统:
    • React使用状态(state)和属性(props)来创建响应式视图。当状态或属性变化时,组件会重新渲染。React的类组件通过setState方法来更新状态,而函数组件在引入Hooks之后,可以通过useState和其他Hooks来管理状态。
    • Vue的响应式系统是通过Object.defineProperty(Vue 2.x)或Proxy(Vue 3.x)实现的。当数据变化时,Vue能够自动检测这些变化并更新视图。Vue的组件数据是通过data函数返回的对象来管理的。
  • 状态管理:React并没有内置的状态管理解决方案,但它有一个强大的生态系统。最常用的状态管理库是Redux,此外还有MobX、Context API和新的Recoil等。Vue提供了一个官方的状态管理库Vuex,它与Vue的响应式系统紧密整合。Vue 3.x提供了Composition API,这是一种新的写法,允许更好的逻辑复用和组织。
  • 工具链和生态系统:React拥有一个庞大的生态系统,提供了大量的第三方库、中间件和工具。Create React App是React的官方脚手架工具,用于生成新的项目结构。Vue也有一个健康的生态系统,Vue CLI是官方提供的标准工具,用于快速搭建Vue项目。Vue社区也提供了大量的第三方库和插件。
  • 使用场景:React适合那些需要高度灵活和扩展性的大型应用和团队,它允许开发者选择最适合自己项目的库和工具。Vue适合各种规模的项目,特别是对于中小规模的应用,Vue的学习曲线更平滑,上手更快。

【源码】

1. Virtual

  • 创建Virtual DOM树: 当应用的状态改变时,一个全新的Virtual DOM树会被创建。这个树只是JavaScript对象的树,它反映了应用的UI结构。
  • 比较(Diffing) : 当新的Virtual DOM树被创建后,它会与前一次渲染时的旧的Virtual DOM树进行比较。这个过程称为Diffing。在比较的过程中,Virtual DOM算法会高效地确定两棵树之间的差异。
  • 计算变更: 当两棵树被比较后,变更(或“差异”)会被计算出来。这些变更是必须应用到真实DOM上以便UI保持更新的最小更新集。
  • 更新真实DOM: 一旦变更被计算出,真实的DOM会高效地更新以反映新的Virtual DOM树。这个过程尽可能地少触碰真实DOM,因为操作DOM是昂贵的,并且会导致性能问题。

2. React Diff原理

React Diff原理的核心策略:

  • 树级别的比较: React首先会在树的层级上进行比较。如果一个组件从树中被移除,那么这个组件以及其子组件会被完全卸载,不会进行进一步的比较。
  • 组件类型的比较: 如果一个元素是同一类型的React元素,React会保持DOM节点不变,仅仅在这个节点上修改改变的属性。如果元素类型不同,React会卸载旧节点及其所有子节点,并从头开始创建和挂载新的DOM节点。
  • 子元素列表比较: 当比较两个相同类型的组件或元素的子元素列表时,React使用了一种称为“列表项的keys”的策略来优化性能。在列表中,每个元素应该有一个独一无二的、稳定的key属性。这使得React能够识别出列表中哪些元素是新的、哪些元素被移除或是重新排序,从而减少不必要的元素重建。

React Diff的三个假设:

  • 两个不同类型的元素将产生不同的树结构: 当元素的类型发生变化,如从<a>变为<img>,React会销毁老树并建立新树。
  • 开发者可以通过key prop来暗示哪些子元素在不同的渲染下是稳定的: 给列表中的每个子元素分配适当的key值,可以提高Diff的性能。
  • 同一层级的一组子节点,它们可以通过唯一的ID进行区分: 当遍历子节点时,React假设同一层级的兄弟节点不含有相同的ID(即key)。这帮助React识别出哪些节点是新的,哪些节点被移动了位置

3. render的基本步骤和原理

  • 虚拟DOM: React中每个组件的渲染都会生成一棵虚拟DOM树。这个虚拟DOM是一个轻量级的描述真实DOM结构的JavaScript对象。
  • 渲染组件: 当组件的状态(state)或属性(props)发生变化时,React会创建一个新的虚拟DOM树。
  • 差异比较(Diffing Algorithm) : React使用高效的差异比较算法来对比新旧虚拟DOM树的差异。这个过程称为Reconciliation(协调)。React比较两棵树,并确定需要进行实际DOM更新的最小操作集。
  • 生成操作列表: 一旦React确定了必须进行的更改,它会生成一个操作列表,这个列表描述了如何最有效地更新DOM。
  • 批处理和更新: React会对这些操作进行批处理,然后统一执行,这有助于避免不必要的DOM操作,提高性能。
  • 实际DOM更新: 最后,React会将这些操作应用到实际的DOM上,这通常会触发浏览器的重排和重绘过程。
  • 生命周期方法: 在这个过程中,React会调用生命周期方法,比如componentDidMountcomponentDidUpdate,让开发者有机会在特定时刻加入自己的代码 React的这种设计允许它保持高效,因为它避免了频繁且不必要的DOM操作,并且只对真正需要变更的部分进行更新。虚拟DOM并不是没有成本的,但在大多数情况下,它提供的性能优势远远超过了它的开销。

React 16 之后,引入了Fiber架构,它进一步优化了渲染过程,特别是在执行协调算法时的性能和响应性。Fiber架构允许React将渲染工作分割成多个小任务,这些小任务可以被中断和重新调度,从而更好地适应主线程的工作负载,避免阻塞用户界面的交互。

4. 对Fiber架构的理解

React Fiber是React 16中引入的新的协调引擎或重新实现的核心算法。它的主要目标是提高React应用的性能,尤其是在动画、布局和手势等需要高频更新的场景下。Fiber架构能够使React做到中断和恢复工作,以及为更新分配优先级,这带来了更平滑的用户界面和更好的性能表现。

以下是对React Fiber架构的一些关键理解:

  • 任务分割: 在之前的React版本中,渲染和更新过程是同步和递归的,这可能会导致长时间的阻塞,影响用户体验。Fiber架构允许React将更新分割成多个小的任务单元,这些任务可以被执行、中断、重新开始或取消,增加了调度的灵活性。
  • 增量渲染: Fiber可以将渲染工作拆分成多个小块,并且在浏览器需要进行其他工作,如处理用户输入或动画时,可以暂停、中断或恢复这些小任务。
  • 任务优先级: Fiber允许React根据任务的重要性给予不同的优先级。例如,动画相关的更新可能会有更高的优先级,这意味着React可以先处理这些更新,从而确保动画的流畅性。
  • 双缓冲技术: Fiber架构使用了类似于游戏引擎中的“双缓冲”技术。React在内存中保持了两棵树:一棵是当前屏幕上显示的树,另一棵是即将更新的工作树。更新过程在工作树上进行,在一切准备就绪后,React会“翻转”这两棵树,以最小化主线程上的工作量。
  • 更好的错误处理: 在Fiber架构中,由于能够跟踪整个渲染过程的状态,React能够更优雅地处理错误。组件可以捕获在生命周期方法中发生的错误,并且可以渲染备用UI以防止整个应用崩溃。
  • 新的生命周期方法: 为了更好地与Fiber架构配合,React引入了新的生命周期方法,如getDerivedStateFromPropsgetSnapshotBeforeUpdate

简而言之,Fiber架构使React能够利用浏览器的空闲时间执行低优先级的工作,而对用户的交互和动画等高优先级任务立即响应,从而提高了大型应用和复杂更新的性能和响应性。Fiber不是单独的功能或API,而是React内部的一种改进,开发者可以透明地从这些性能优化中获益,无需改变编写组件的方式。