函数组件和类组件

React 中的组件有两种形式:函数组件(Functional Components)类组件(Class Components)。虽然两者都可以用于构建 UI 组件,但它们有一些关键的区别和不同的使用方式。

函数组件(Functional Components)

函数组件是 React 的一种轻量级组件形式,本质上是一个 JavaScript 函数,接收 props(属性)作为参数,并返回 JSX 来渲染 UI。

特点

  1. 定义简单:函数组件就是一个简单的 JavaScript 函数,不需要使用 this 关键字。
  2. 无状态(Stateless):在 React 16.8 之前,函数组件是无状态的,它们不能使用内部状态和生命周期方法。但是在 React 16.8 引入了 Hooks,现在函数组件也可以管理状态和执行副作用。
  3. 使用 Hooks:通过 useStateuseEffect 等 Hooks,函数组件可以管理状态、执行副作用、访问上下文等,从而具备类组件的大部分功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>Current count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

export default Counter;

在这个例子中,Counter 是一个函数组件,通过 useState Hook 来管理计数器的状态。

函数组件的优点:

  • 简洁性:由于没有 this,函数组件更容易编写和理解。
  • 性能优化:函数组件比类组件更容易进行优化(如通过 React.memo),因为它们没有复杂的生命周期方法和状态管理逻辑。
  • 推荐使用:在 React 16.8 之后,React 团队建议优先使用函数组件,因为它们更加灵活和简洁。

类组件(Class Components)

类组件是 React 中的一种传统组件定义方式,使用 ES6 的 class 关键字来定义,必须继承 React.Component

特点

  1. 状态管理(Stateful):类组件可以拥有自己的状态(state),并通过 this.setState 来更新状态。
  2. 生命周期方法:类组件可以使用一系列生命周期方法(如 componentDidMountcomponentDidUpdatecomponentWillUnmount)来在组件的不同阶段执行特定的操作。
  3. this 关键字:类组件需要通过 this 来访问 propsstate。这使得开发者需要注意 this 的绑定问题(通常通过箭头函数或 bind 来解决)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { Component } from 'react';

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

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

render() {
return (
<div>
<p>Current count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}

export default Counter;

在这个例子中,Counter 是一个类组件,通过 statesetState 来管理状态。

类组件的优点:

  • 生命周期方法:类组件可以通过生命周期方法精确控制组件的行为,适合处理复杂的组件逻辑。
  • 状态管理:虽然函数组件可以通过 Hooks 管理状态,但类组件在 Hook 引入之前是唯一可以管理状态的组件。

函数组件与类组件的比较

特性 函数组件 类组件
定义方式 JavaScript 函数 ES6 类,继承 React.Component
状态管理 使用 useStateuseReducer 使用 this.statesetState
生命周期 使用 useEffect 等 Hooks 使用生命周期方法
this 关键字 不使用 必须使用
性能 通常性能更好 可能因为生命周期方法复杂性稍慢
推荐使用情况 React 16.8 之后推荐使用 在 React 16.8 之前用于复杂逻辑

Hooks 的引入与影响

在 React 16.8 之前,类组件是实现状态管理和生命周期方法的唯一方式。随着 Hooks 的引入,函数组件的功能得到了极大增强,几乎所有过去需要类组件实现的功能都可以通过函数组件完成。

  • 代码简洁性:Hooks 使得函数组件可以处理复杂的状态逻辑和副作用,同时保持代码的简洁和可读性。
  • 逻辑复用:通过自定义 Hook,开发者可以将组件逻辑抽取出来并在多个组件中复用,而在类组件中复用逻辑往往依赖于高阶组件(HOC)或 Render Props。
  • 更好性能:函数组件比类组件具有更好的性能潜力,尤其是在大量更新的场景中。

总结

  • 函数组件 适合编写简洁、灵活且性能优越的组件,尤其是自从 Hooks 引入后,它几乎可以完成类组件所有的功能。因此,函数组件在现代 React 开发中成为主流选择。
  • 类组件 仍然存在于大量历史代码库中,且对于某些复杂场景,特别是在 Hooks 还未出现或团队对 Hooks 不熟悉的情况下,类组件仍有其价值。

函数式组件,在一个应用周期里,什么时机会被调用到呢,函数会被调用多少次

函数式组件在 React 中的生命周期和类组件有一些不同。函数式组件的渲染和重新渲染过程主要由其父组件的状态和属性的变化驱动。以下是函数式组件在一个应用周期中的调用时机和可能的调用次数的详细说明:

函数式组件的调用时机

  1. 初次渲染(Mounting)

    • 当函数式组件第一次被挂载时,React 会调用一次该组件的函数。这通常发生在应用启动或组件首次插入 DOM 树时。
  2. 属性变化(Props Change)

    • 当父组件的状态或属性变化,导致传递给函数式组件的属性发生变化时,React 会重新调用该函数式组件进行重新渲染。
  3. 状态变化(State Change)

    • 如果函数式组件内部使用了 useState Hook,当状态发生变化时,该组件会重新渲染,并且组件函数会再次被调用。
  4. 上下文变化(Context Change)

    • 如果函数式组件使用了 useContext Hook,并且上下文值发生变化时,组件会重新渲染,函数会再次被调用。
  5. 强制更新(Force Update)

    • 在极少数情况下,使用 forceUpdate 方法可以强制组件重新渲染,尽管这在函数式组件中并不常见。

函数式组件被调用的次数

函数式组件的调用次数取决于以下因素:

  1. 父组件的重新渲染

    • 每当父组件重新渲染时,无论是因为状态变化还是属性变化,子函数式组件都会被重新调用。即使传递给子组件的属性没有变化,子组件也会重新渲染。
  2. 状态和属性变化

    • 函数式组件内部状态变化(通过 useState)或接收到新的属性时,组件会重新渲染。
  3. 性能优化

    • 使用 React.memo 包裹函数式组件,可以避免无意义的重新渲染。当属性没有变化时,React.memo 会使用浅比较来判断是否需要重新渲染组件,减少调用次数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { useState, useEffect, memo } from 'react';

// 使用 React.memo 来优化函数式组件
const MyComponent = memo(({ count }) => {
useEffect(() => {
console.log('MyComponent rendered');
});

return <div>Count: {count}</div>;
});

const App = () => {
const [count, setCount] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);

return () => clearInterval(interval);
}, []);

return <MyComponent count={count} />;
};

export default App;

在这个例子中:

  • 初次渲染时,MyComponent 被调用一次。
  • 每次 count 状态变化时,App 组件会重新渲染,并且 MyComponent 也会重新调用。
  • 如果 MyComponent 没有使用 React.memo,每次 App 重新渲染时,无论 count 是否变化,MyComponent 都会被调用。
  • 使用 React.memo 后,MyComponent 只有在 count 变化时才会重新调用,从而减少不必要的渲染。

总结:函数式组件的调用时机主要在初次渲染、属性变化、状态变化和上下文变化时。调用次数取决于父组件的重新渲染频率、组件自身状态和属性的变化,以及是否进行了性能优化(如使用 React.memo)。

什么情况下会触发组件更新?

在 React 中,组件更新(重新渲染)的触发条件主要有以下几种情况:

  1. 属性(Props)变化

    • 当父组件传递给子组件的属性发生变化时,子组件会重新渲染。即使属性值没有变化,只要父组件重新渲染了,子组件也会被重新调用。
  2. 状态(State)变化

    • 组件内部的状态通过 useStatethis.setState 发生变化时,该组件会重新渲染。
  3. 上下文(Context)变化

    • 当使用 useContext Hook 或 Context.Consumer 的组件,其所依赖的上下文值发生变化时,该组件会重新渲染。
  4. 强制更新(Force Update)

    • 使用 this.forceUpdate() 方法可以强制类组件重新渲染。对于函数组件,可以通过更新状态来实现强制更新。
  5. 父组件重新渲染

    • 当父组件重新渲染时,子组件也会重新渲染,即使传递给子组件的属性(props)没有变化。这是因为 React 默认会重新渲染子组件,以确保 UI 的一致性,除非使用了性能优化方法(如 React.memo)。

React.memo 会做什么处理

这是一个高阶组件,用于优化函数组件的性能。React.memo会对函数组件进行浅比较,并且只有在 props 发生变化时才会重新渲染该组件。这样可以避免不必要的重新渲染,从而提高性能。浅比较意味着 React 会逐个比较 props 对象的每个属性,并判断这些属性是否相等(使用 === 运算符)。如果所有属性都相等,React 就会认为 props 没有变化,React 会跳过该组件的渲染和副作用(如 useEffect),直接使用上一次的渲染结果。

如何理解JSX?

JSX 是一种 JavaScript 语法扩展,允许在 JavaScript 代码中编写类似 HTML 的结构。它使得组件结构的描述更加直观,增强了开发体验。

在 React 中,组件返回的 JSX 需要在编译时转化为 JavaScript 代码,以便浏览器能够理解和执行。这一过程通常由 Babel 等编译工具完成。

具体来说,JSX 会被转化成 React.createElement 函数调用。每个 JSX 元素都会被转换为一个 React.createElement 的调用,传入元素的类型、属性和子元素。例如:

1
const element = <div className="container">Hello, World!</div>;

经过编译后,上述代码会被转化为:

1
2
3
4
5
const element = React.createElement(
'div',
{ className: 'container' },
'Hello, World!'
);

这个转换的核心是将 JSX 语法转化为一个描述组件结构的 JavaScript 对象,React 后续会使用这些对象构建虚拟 DOM,并根据它们更新实际的 DOM。

当我们在 React 中编写 JSX(JavaScript XML)时,JSX 语法本质上是 React 的一种语法糖,它并不是原生 JavaScript 代码。在编译阶段,JSX 会被转译成普通的 JavaScript 函数调用,主要是通过 React.createElement 来实现的。

JSX 的编译流程

当我们编写 JSX 代码时,例如:

1
const element = <h1>Hello, world!</h1>;

在编译阶段,这段代码会被转译成 JavaScript 代码,具体为:

1
const element = React.createElement('h1', null, 'Hello, world!');

React.createElement 的参数解释

React.createElement 函数接受三个主要参数:

  • 第一个参数:标签名称,表示将要创建的 HTML 元素或 React 组件。例如,'h1' 表示一个普通的 HTML 标签。如果是自定义组件,则是组件的名称。
  • 第二个参数:一个对象,包含该元素的属性(props)。如果没有属性,则传入 null
  • 第三个参数及之后的参数:子元素,可以是文本、React 元素,或是其他的 JSX 片段。

复杂 JSX 的转译

更复杂的 JSX 结构同样会被转译为一系列 React.createElement 的嵌套调用。例如,以下代码:

1
2
3
4
5
6
const element = (
<div>
<h1>Hello, world!</h1>
<p>This is a paragraph.</p>
</div>
);

会被转译为:

1
2
3
4
5
6
const element = React.createElement(
'div',
null,
React.createElement('h1', null, 'Hello, world!'),
React.createElement('p', null, 'This is a paragraph.')
);

带有属性的 JSX 转译

当 JSX 中的元素带有属性时,编译器会将属性转译为 React.createElement 中的第二个参数。比如:

1
const element = <button className="btn" disabled={true}>Click me</button>;

会被转译为:

1
const element = React.createElement('button', { className: 'btn', disabled: true }, 'Click me');

属性被转化为一个对象 { className: 'btn', disabled: true },作为 React.createElement 的第二个参数。

自定义组件的 JSX 转译

如果 JSX 中使用了自定义的 React 组件,React.createElement 的第一个参数就会是组件的名称,而不是字符串。

例如:

1
2
3
4
5
function MyComponent() {
return <h1>Hello from MyComponent</h1>;
}

const element = <MyComponent />;

会被转译为:

1
const element = React.createElement(MyComponent, null);

自定义组件 MyComponent 被作为第一个参数传递给 React.createElement,因为它是一个函数(或者类组件),而不是字符串形式的 HTML 标签。

JSX 转译工具:Babel

React 项目通常使用 Babel 来编译 JSX。Babel 是一个 JavaScript 编译器,它可以通过插件将 JSX 转换为 JavaScript 代码。

默认情况下,Babel 在编译 JSX 时会自动引入 React.createElement。但从 React 17 开始,React 引入了新的 JSX 转译方式,允许不再显式地导入 React,而是自动将 JSX 转化为元素。

如果你使用的是 React 17 及以上,JSX 的编译输出可能是这样的:

1
2
3
import { jsx as _jsx } from 'react/jsx-runtime';

const element = _jsx('h1', { children: 'Hello, world!' });

这里使用了 react/jsx-runtime 中的 _jsx 函数来代替 React.createElement,这是 React 17 引入的新 JSX 转译方式,目的是为了减少 React 不必要的引入和文件大小。

React 17 及以上的 JSX 转译

React 17 引入了新的 JSX 转译机制,这种机制下,你不再需要在文件顶部显式导入 React,因为编译器会自动处理 React.createElement_jsx 函数调用。例如:

1
const element = <h1>Hello, world!</h1>;

在 React 17 及以上版本的编译输出可能是:

1
2
3
import { jsx as _jsx } from 'react/jsx-runtime';

const element = _jsx('h1', { children: 'Hello, world!' });

JSX 转化为纯 JavaScript 的流程总结

  • JSX 是一种语法糖,它需要通过编译器(如 Babel)转译为 JavaScript。
  • 在 React 16 及更早的版本中,JSX 被转译为 React.createElement 的形式。
  • 在 React 17 及以上版本中,JSX 被转译为 jsxjsxs 函数调用,并使用 react/jsx-runtime 提供的自动导入。

总结

当编写 JSX 代码时,在编译阶段它会被转化为对 React.createElement_jsx 函数的调用。这是 JSX 在编译时转化的主要过程:

  • 简单的 HTML 标签(如 <h1>)会转化为 React.createElement('h1', ...)
  • 自定义组件(如 <MyComponent />)会转化为 React.createElement(MyComponent, ...)
  • 属性被转化为 JavaScript 对象,传递给 React.createElement 的第二个参数。
  • 子元素则作为后续参数传递,用于创建嵌套结构。

这种转译机制使得 JSX 能够简化组件的创建过程,同时提供了灵活的组件构建能力。

React创建的dialog组件,子组件可以是任意组件–children

在 React 中创建一个 Dialog 组件,并且让其能够接收任意子组件,可以通过 props.children 实现。props.children 是 React 中非常常见的一种模式,允许父组件在 JSX 中包裹子组件,然后通过 props 传递给父组件进行渲染。

下面,我们将实现一个通用的 Dialog 组件,它可以接收任意的子组件(例如输入框、按钮、文本等)。我们将通过 open prop 控制弹窗的显示与隐藏,通过 title prop 显示弹窗的标题,并且将通过 props.children 让用户能够自由地传递子组件。

创建通用 Dialog 组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React from 'react';

const Dialog = ({ open, title, onClose, children }) => {
if (!open) {
return null; // 当 open 为 false 时,不渲染 dialog
}

return (
<div className="dialog-overlay" onClick={onClose}>
<div className="dialog" onClick={(e) => e.stopPropagation()}>
<div className="dialog-header">
<h3>{title}</h3>
<button className="dialog-close-button" onClick={onClose}>
X
</button>
</div>
<div className="dialog-content">
{children} {/* 这里的 children 是用户传递的任意子组件 */}
</div>
</div>
</div>
);
};

export default Dialog;

使用 Dialog 组件

使用这个 Dialog 组件,并传递任意子组件,如输入框、按钮、文本等。通过 useState 来控制 Dialog 的显示和隐藏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { useState } from 'react';
import Dialog from './Dialog';

const App = () => {
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState('');

const handleOpenDialog = () => setIsOpen(true);
const handleCloseDialog = () => setIsOpen(false);

return (
<div>
<button onClick={handleOpenDialog}>Open Dialog</button>

{/* 使用 Dialog 组件 */}
<Dialog open={isOpen} title="Sample Dialog" onClose={handleCloseDialog}>
{/* 这里传递了任意的子组件 */}
<p>Welcome to this dialog! Please enter some information:</p>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Enter something..."
/>
<button onClick={handleCloseDialog}>Submit</button>
</Dialog>
</div>
);
};

export default App;

代码说明

  • Dialog 组件Dialog 通过 open prop 控制弹窗的显示与隐藏。当 openfalse 时,Dialog 不会渲染。弹窗包含一个头部(显示 title 和关闭按钮),以及内容区域(通过 children 来渲染传递的子组件)。
  • onClose 回调:点击遮罩层或者关闭按钮时调用 onClose 回调关闭弹窗。我们还使用了 stopPropagation 阻止点击对话框本身时关闭弹窗。
  • **props.children**:使用 props.children 来渲染任意传入的子组件。在 App 组件中,我们传递了一个 p 标签、一个 input 元素和一个 button 元素。

扩展功能

  • 动画效果:可以使用 CSS 或者 React Transition Group 给 Dialog 添加显示和隐藏时的动画效果。
  • 尺寸控制:可以通过 props 控制对话框的尺寸,比如 widthheight,或者设计一个弹窗的最大高度和滚动条。
  • 不同的类型:可以让 Dialog 组件接受不同类型的子组件,比如表单、图片、列表等。你甚至可以通过 children 动态注入复杂的组件结构。

总结

通过 props.children,我们可以非常灵活地向父组件(Dialog)传递任意的子组件。这种模式在 React 中非常常见,适合构建可重用、灵活的容器组件,例如 DialogModalCard 等。在 Dialog 中,你可以传递任何组件,且这些子组件将按照插入顺序渲染在对话框的内容区域内。这使得 Dialog 组件可以适应多种场景,比如展示文本、表单或者按钮等复杂内容。

React的fiber架构

React 的 Fiber 架构是 React 自 16 版本引入的一种全新的协调引擎,旨在提升性能、提高界面的响应速度,并支持更多复杂的 UI 场景。Fiber 架构是 React 内部的一次核心重写,目的是为了改进传统的同步渲染机制,使得 React 能够支持 可中断的更新优先级更新并发渲染

React 渲染流程:传统 vs Fiber

传统的 React 渲染(React 15 及以前)

  • 在旧的 React 版本中,整个渲染过程是递归同步执行的,也就是说,更新是不可中断的。
  • 当组件树变得非常复杂时,如果一次渲染需要很长时间,浏览器的主线程就会被 React 占用,导致页面的卡顿和不响应。
  • 递归调用栈:传统架构基于递归执行,会在组件树递归过程中阻塞整个渲染更新,导致性能瓶颈。

Fiber 架构的 React 渲染(React 16+)

  • Fiber 架构的目标:实现可中断的异步渲染,即在渲染过程中可以暂停和继续,分批次地更新 DOM,而不是一次性完成所有工作。
  • Fiber 的核心思想是将渲染工作分为多个小的任务单元,每次处理一部分,处理完一部分后将控制权交还给浏览器,使其能在处理其他更重要的任务(如用户输入、动画)后再继续渲染任务。

Fiber 解决什么问题?

React 的 Fiber 架构主要是为了解决以下几个问题:

  1. 同步渲染导致卡顿:在传统架构中,React 渲染过程是同步的,整个更新过程会持续占用主线程,导致长时间的 UI 不响应。如果组件树很大,或者更新的工作很复杂,用户可能会感觉到明显的卡顿。

  2. 无法设置渲染优先级:在旧的架构中,React 没有处理任务优先级的机制。所有更新都是同步、不可打断的,UI 中的关键任务(如用户输入)也无法被优先处理。

  3. 长任务不能被切片:长任务无法拆分为更小的可中断任务,React 的协调(Reconciliation)过程就是一口气进行完的。如果任务执行时间过长,UI 可能会卡顿,阻止浏览器响应用户的交互。

Fiber 的工作方式

什么是 Fiber?

Fiber 是一种数据结构,它代表 React 组件树中的一个单元或节点。每个组件实例都会对应一个 Fiber 节点。可以将 Fiber 想象成组件的一个执行单元,它保存了组件更新所需的所有信息,包括组件的状态、props、上下文等。

Fiber 节点的关键属性:

  • type:对应的组件类型(类组件、函数组件、DOM 节点等)。
  • props:组件的属性。
  • stateNode:对于类组件,stateNode 是组件的实例;对于 DOM 节点,它指向 DOM 元素。
  • return:指向父 Fiber 节点。
  • child:指向子 Fiber 节点。
  • sibling:指向下一个兄弟 Fiber 节点(如果有)。
  • alternate:指向上一次渲染的 Fiber 节点(用于双缓冲机制)。

双缓冲机制

  • Fiber 架构使用了双缓冲机制,即维护两颗 Fiber 树:current Fiber 树(当前屏幕上展示的内容)和 work-in-progress Fiber 树(正在构建的 Fiber 树)。每次更新时,React 会在 work-in-progress 树上执行工作,更新完成后将其替换为 current 树。
  • 这种机制可以确保渲染过程中可以保留当前显示的内容,而不会影响用户体验。

可中断的任务调度

Fiber 的最大特点是可中断的任务调度。在 Fiber 架构中,React 将整个渲染工作分成多个小任务(微任务),每个任务会检查当前是否有高优先级的任务需要处理。如果有,它会暂停当前任务,优先处理更重要的任务,之后再继续未完成的任务。

优先级机制

Fiber 为每个任务分配了不同的优先级,React 会根据不同优先级来决定先渲染哪些任务,哪些任务可以推迟处理。

任务优先级可以分为:

  • 同步优先级:立即执行,通常用于用户交互事件。
  • 异步优先级:可以被打断,通常用于不太紧急的更新,例如动画、屏幕的非关键部分更新。
  • 低优先级:如后台数据的更新,可以推迟执行。

工作分配(工作单元 Work Unit)

  • 在 Fiber 架构中,渲染工作被分解为多个工作单元。每次执行一个工作单元后,React 会将控制权还给浏览器,浏览器可以在帧之间处理用户交互或渲染更新。下次执行时,React 会继续未完成的工作,直到所有任务完成。

    这避免了长时间的渲染工作阻塞主线程,使 UI 响应更流畅。

Fiber 架构的渲染流程

Fiber 架构的渲染分为两个阶段:

Reconciliation 阶段(协调阶段,Diff 阶段)

在这个阶段,React 会根据组件的状态和 props 计算 Fiber 树的变化(新旧 Fiber 树的对比),并生成一颗新的 work-in-progress 树。在这个阶段,React 可以暂停、恢复任务,也就是可中断的阶段。这一阶段的工作主要是:

  • 比较新旧 Fiber 树,生成新的更新。
  • 标记哪些节点需要更新 DOM。

该阶段不直接修改 DOM,属于“虚拟”阶段。

Commit 阶段

一旦 Reconciliation 阶段完成,React 会进入 Commit 阶段。这一阶段不可打断,主要用于:

  • 将 Reconciliation 阶段生成的更新应用到真实 DOM 中。
  • 调用组件的生命周期方法(如 componentDidMountcomponentDidUpdate 等)。
  • 更新 DOM 结构,反映在屏幕上。

Commit 阶段是同步执行的,通常非常快,因为 Reconciliation 阶段已经标记了所有需要修改的部分。

Fiber 对开发者的影响

Fiber 架构虽然大部分在 React 内部实现,但它带来的影响是直接的:

  • 更流畅的用户体验:通过可中断渲染和优先级处理,React 在 Fiber 架构下可以避免 UI 卡顿,尤其是在大型、复杂的应用中。
  • 支持 Concurrent Mode:Fiber 架构为未来的 Concurrent Mode 打下了基础,使得 React 能够在不同优先级任务之间智能调度,进一步提升渲染性能。
  • 更灵活的生命周期:由于 Fiber 的任务中断特性,部分生命周期方法(如 componentWillMountcomponentWillReceiveProps)可能会多次调用,React 因此废弃了这些方法,鼓励使用更安全的新生命周期(如 componentDidMountcomponentDidUpdate)或 hooks

总结

React 的 Fiber 架构通过以下方式优化了性能并增强了可扩展性:

  • 可中断、可恢复的渲染任务:任务被拆分为小的工作单元,每次只处理一部分任务,并在有更高优先级的任务出现时暂停当前工作。
  • 优先级调度:通过不同的任务优先级处理,使得用户的交互能够得到及时响应,而不被其他长时间的渲染任务阻塞。
  • 支持并发模式:Fiber 架构为未来的并发渲染模式(Concurrent Mode)奠定了基础,使得 React 能够在复杂的应用场景中更加高效。

Fiber 架构极大地提升了 React 应对大型应用和复杂用户交互的能力,确保在高性能和流畅用户体验之间取得平衡。

React的生命周期

  1. 挂载(Mounting):componentDidMount
  2. 更新(Updating):shouldComponentUpdate 、componentDidUpdate
  3. 卸载(Unmounting):componentWilUnmount

React如何处理事件

React使用合成事件,这是对原生事件的跨浏览器封装,事件绑定使用驼峰命名法,一般在JSX使用。

useState 是同步还是异步?

在React中,useState 是一个用于在函数组件中添加状态的Hook,状态更新是异步的。

当调用setState(由useState返回的状态更新函数),React不会立即更新状态。相反,React会将状态更新排入队列,并在稍后的某个时间点进行批量处理。这种行为是为了优化性能,避免不必要的重新渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

const handleClick = () => {
setCount(count + 1);
console.log(count); // 这里的 count 仍然是旧的值
};

return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}

export default Counter;

在上面的代码中,当你点击按钮时,setCount会将状态更新排入队列,但console.log(count)会输出旧的状态值。这是因为状态更新是异步的,setCount不会立即更新count

批量更新

React会在一个事件循环中批量处理状态更新,以提高性能。这意味着在一个事件处理函数中多次调用setState,React会将这些更新合并成一次更新。

1
2
3
4
5
6
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
console.log(count); // 这里的 count 仍然是旧的值
};

在这个例子中,即使你多次调用setCountcount的值在console.log中仍然是旧的值。React会将这些更新合并,并在下一次渲染时更新状态。

更新后的回调

如果你需要在状态更新后执行某些操作,可以使用useEffect Hook,它会在状态更新后执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState, useEffect } from 'react';

function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log(`Count updated: ${count}`);
}, [count]);

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

return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}

export default Counter;

在这个例子中,useEffect会在count更新后执行,并输出更新后的count值。

总结

  • 异步更新useState的状态更新是异步的,React会将状态更新排入队列,并在稍后的某个时间点批量处理。
  • 批量处理:在一个事件循环中多次调用setState,React会将这些更新合并成一次更新,以提高性能。
  • 更新后的操作:如果需要在状态更新后执行某些操作,可以使用useEffect Hook。

理解useState的异步特性对于正确使用React状态管理和避免一些常见的陷阱是非常重要的。

如何同步?

使用useEffect

useEffect是一个用于在组件渲染后执行副作用的Hook。你可以利用useEffect在状态更新后执行某些操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState, useEffect } from 'react';

function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log(`Count updated: ${count}`);
}, [count]); // 当 count 更新时,执行这个 effect

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

return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}

export default Counter;

在这个例子中,当count更新时,useEffect会在渲染后执行,并输出更新后的count值。

使用useRef保存最新状态

有时你可能需要在事件处理函数中立即获取最新的状态值。你可以使用useRef来保存最新的状态值,并在事件处理函数中访问它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { useState, useEffect, useRef } from 'react';

function Counter() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);

useEffect(() => {
latestCount.current = count;
console.log(`Count updated: ${count}`);
}, [count]);

const handleClick = () => {
setCount(latestCount.current + 1);
console.log(`Latest count: ${latestCount.current}`);
};

return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}

export default Counter;

在这个例子中,latestCount保存了最新的count值,并在handleClick事件处理函数中使用它。

使用回调函数形式的setState

如果你需要基于当前状态值进行更新,使用回调函数形式的setState是一个更安全的选择。这种方式确保你总是基于最新的状态进行更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

const handleClick = () => {
setCount(prevCount => {
const newCount = prevCount + 1;
console.log(`New count: ${newCount}`);
return newCount;
});
};

return (
<div>
<p>{count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}

export default Counter;

在这个例子中,setCount的回调函数接收当前的状态值prevCount,并返回新的状态值。这确保了状态更新是基于最新的状态进行的。

变为同步的方法总结

  1. 使用useEffect在状态更新后执行操作。
  2. 使用useRef保存最新的状态值,并在事件处理函数中访问它。
  3. 使用回调函数形式的setState,确保状态更新基于最新的状态。

React Diff算法的理解

在 React 中,Diff 算法(也称为协调算法,Reconciliation)是用来比较两棵虚拟 DOM 树之间的差异,并高效地更新实际 DOM 的算法。React 通过这个算法,可以快速更新页面中的最小部分,而不是重新渲染整个 UI。

React Diff 算法的基本原理

React Diff 算法的核心思想是最小化 DOM 操作,以提高渲染性能。由于直接操作 DOM 是昂贵的,React 通过维护一个虚拟 DOM(Virtual DOM)来减少不必要的操作。当应用状态或 props 改变时,React 会:

  • 生成新的虚拟 DOM 树。
  • 使用 Diff 算法与之前的虚拟 DOM 树进行对比,找出不同的地方。
  • 最后仅更新发生变化的部分到实际的 DOM 中。

Diff 算法的三大优化策略

React 的 Diff 算法并不是逐个节点进行深度比较,而是通过一些优化规则来提升效率。它的核心优化规则如下:

  • 不同类型的元素,直接移除并重建:如果两个节点的类型不同(比如一个是 <div>,另一个是 <p>),React 直接移除旧的节点,创建新的节点,而不会进一步比较它们的子节点。

    1
    2
    <div></div>  // 旧的虚拟 DOM
    <p></p> // 新的虚拟 DOM

    React 发现类型不同,直接替换整个节点。

  • 同级节点顺序变化,使用 key 进行优化:如果一组兄弟节点的顺序发生了变化,React 通过 key 属性来追踪每个节点,从而确定哪些节点需要移动或更新。如果没有 key,React 会假设节点是按顺序排列的,这可能会导致性能问题。

  • 相同类型的组件,递归比较子节点:如果两个节点的类型相同,React 会保留该节点,并递归比较它们的子节点。这种优化确保只有发生变化的部分才会重新渲染。

关键点

  • O(n) 复杂度:React Diff 算法的时间复杂度为 O(n),即它会在线性时间内比较两棵树的不同。相比传统的 DOM Diff 算法(O(n^3)),React 的算法更加高效。
  • 基于层级的比较:React 通过对比同一层级的节点,避免深度递归,提高了比较的效率。

React 组件的渲染过程

React 的渲染过程分为以下几个阶段:

初次渲染(Mounting)

  1. 构建虚拟 DOM

    • 当 React 组件被首次渲染时,React 会根据组件的 render 方法生成一棵虚拟 DOM 树。虚拟 DOM 只是一个轻量级的 JavaScript 对象,描述了组件的结构。
  2. 比较并更新 DOM

    • React 会将虚拟 DOM 转化为真实 DOM,接着插入到页面中。因为是首次渲染,所以不需要 Diff 比较,React 直接将虚拟 DOM 变为实际的 DOM。

更新过程(Updating)

当组件的状态(state)或属性(props)发生变化时,React 会重新渲染组件并触发更新过程。

  1. 生成新的虚拟 DOM

    • 组件的 render 方法被重新调用,生成一个新的虚拟 DOM 树,描述了组件更新后的结构。
  2. Diff 算法比较新旧虚拟 DOM

    • React 使用 Diff 算法将新旧虚拟 DOM 进行比较,找出不同的部分。

    具体步骤:

    • 同一层次的比较:React 先从树的根部开始,比较每个节点的类型和属性。
    • 删除或新增节点:如果发现节点被删除,React 会在 DOM 中移除对应的元素。如果发现有新的节点,React 会在 DOM 中插入新的元素。
    • 更新节点:如果节点类型相同(比如都是 <div>),React 会只更新该节点的属性(如 classNamestyle)和事件监听器。
  3. 批量更新实际 DOM

    • React 不会立即将每个变更应用到实际 DOM 中,而是会将所有变更汇总,并在最后一次性更新实际 DOM。这一批量处理机制优化了性能,减少了不必要的 DOM 操作。

React Diff 算法的工作流程

以一个具体的例子来展示 React Diff 的流程:

初次渲染:

1
2
3
4
5
6
const App = () => (
<div>
<h1>Hello World</h1>
<p>This is React.</p>
</div>
);

在首次渲染时,React 会:

  • 生成虚拟 DOM:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
type: 'div',
props: {},
children: [
{
type: 'h1',
props: {},
children: ['Hello World']
},
{
type: 'p',
props: {},
children: ['This is React.']
}
]
}
  • 将虚拟 DOM 转换为实际 DOM,插入到页面中。

更新阶段:

假设组件更新后变成了以下结构:

1
2
3
4
5
6
const App = () => (
<div>
<h1>Hello React</h1> {/* 这里发生了变化 */}
<p>This is React.</p>
</div>
);
  1. React 重新生成一个新的虚拟 DOM:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
type: 'div',
props: {},
children: [
{
type: 'h1',
props: {},
children: ['Hello React']
},
{
type: 'p',
props: {},
children: ['This is React.']
}
]
}
  1. React 使用 Diff 算法对比新旧虚拟 DOM,发现:
    • divp 节点没有变化。
    • h1 节点的文本内容从 Hello World 变成了 Hello React
  2. React 只会更新 <h1> 元素中的文本,其他部分不变,从而减少了 DOM 的操作。

key 的重要性

在 React 的 Diff 算法中,key 是一个非常重要的优化手段。key 可以帮助 React 更好地识别哪些元素发生了变化,尤其是在列表渲染中。

  • key 的作用:当渲染一个列表时,React 需要追踪每个子元素是否有变化。通过给每个子元素分配唯一的 key,React 能够精确地识别哪些元素被添加、删除或更新。

  • key 不唯一时的问题:如果列表中的 key 不唯一,React 会错误地重新渲染整个列表,导致性能下降。

示例:

1
2
3
4
5
6
7
const App = () => (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);

这里的 key={item.id} 可以确保 React 正确追踪每个 <li> 元素。

批量更新与异步渲染

React 16+ 的 Fiber 架构带来了新的特性,比如可中断的更新。在传统的 React 渲染中,所有的更新都是同步的,更新过程无法暂停。而在 Fiber 架构中,React 将更新过程拆分为多个小任务,可以在合适的时机暂停和恢复更新。

这种机制确保了高优先级的任务(如用户输入、动画等)能够及时响应,而不会被长时间的渲染任务阻塞。

总结

  • React Diff 算法:React 使用 O(n) 复杂度的 Diff 算法,来高效比较新旧虚拟 DOM 树的差异,并最小化对实际 DOM 的操作。
  • 组件渲染过程:React 渲染过程分为两部分,初次渲染时直接构建并插入 DOM,而在更新阶段使用 Diff 算法比较并只更新变化的部分。
  • key 的作用:在列表渲染中,key 用于帮助 React 识别哪些元素被修改、删除或添加,提升渲染性能。
  • Fiber 架构:Fiber 使得 React 能够进行异步渲染,允许在渲染过程中暂停并恢复任务,以提高页面的响应速度。

React 的 Diff 算法和 Fiber 架构使得其能够高效处理大型应用中的频繁更新场景,确保在复杂应用中保持良好的性能表现。

useRef

useRef 是 React 中的一个 Hook,用于创建一个可变的引用,它在组件的整个生命周期内保持不变。useRef 返回一个包含 current 属性的对象,这个属性可以存储任何可变的数据,且不会引起组件重新渲染。

访问 DOM 元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { useRef } from 'react';

function MyComponent() {
const inputRef = useRef<HTMLElement>(null);

const focusInput = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};

return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}

存储可变值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useRef } from 'react';

function TimerComponent() {
const countRef = useRef(0);

const incrementCount = () => {
countRef.current += 1;
console.log(countRef.current);
};

return (
<div>
<button onClick={incrementCount}>Increment Count</button>
</div>
);
}

useEffect 中使用 useRef

useEffect 的第二个参数中,通常会传入依赖数组,以便在依赖发生变化时执行 effect。然而,useRef 对象的 current 属性的修改不会导致 useEffect 被重新执行。因为 useRef 返回的对象是稳定的引用,React 不会将其视为依赖项。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useEffect, useRef } from 'react';

function MyComponent() {
const countRef = useRef(0);

useEffect(() => {
console.log('Effect executed');
}, [countRef]); // 不会因为 countRef.current 的变化而重新执行

const incrementCount = () => {
countRef.current += 1; // 修改 current 属性不会导致 effect 重新执行
};

return (
<div>
<button onClick={incrementCount}>Increment Count</button>
</div>
);
}

在这个示例中,修改 countRef.current 的值不会导致 useEffect 中的代码重新执行。如果想要在某些情况下响应变化,最好使用 useState 或者通过其他方式来触发更新。

react初次渲染会渲染页面几次,render调用几次?

在 React 中,初次渲染会执行两次 render 方法。这是因为在 React 中,组件的生命周期包括三个阶段:挂载阶段、更新阶段和卸载阶段。在挂载阶段,React 会先执行一次 render 方法生成虚拟 DOM 树,然后将这棵树渲染到页面上,此时视图才会显示出来。在更新阶段,如果组件的 props 或 state 发生了改变,React 会再次执行 render 方法,生成新的虚拟 DOM 树,并将其与旧的虚拟 DOM 树进行比较,找出需要更新的部分,最终只更新这些部分,从而避免了全量更新整个 DOM 树的代价。

所以,初次渲染会渲染页面一次,但会执行两次 render 方法,第一次是生成虚拟 DOM 树,第二次是将虚拟 DOM 树渲染到页面上。在后续的更新阶段中,如果组件需要更新,则会再次执行 render 方法,但不一定会重新渲染整个页面。如果虚拟 DOM 树与旧的虚拟 DOM 树没有变化,则不会进行任何操作,如果有变化,则只会更新变化的部分。

React什么情况下会重新渲染?

在 React 中,组件的重新渲染(Re-render)是基于组件状态state)和属性props)的变化来触发的。渲染的过程由 React 的虚拟 DOM(Virtual DOM)机制和Diff 算法优化,以确保性能。

组件的状态(state)发生变化

每当调用 setState 或者使用 useState 更新组件的状态时,React 会触发重新渲染。React 会根据新的状态重新生成虚拟 DOM 树,并使用 Diff 算法计算需要更新的部分,最后将变化应用到真实 DOM。

1
2
3
4
5
6
7
8
9
10
11
function App() {
const [count, setCount] = React.useState(0);

return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

组件的属性(props)发生变化

当父组件重新渲染并传递新的 props 给子组件时,子组件也会触发重新渲染。React 会根据新的 props 更新虚拟 DOM 并进行差异比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ChildComponent({ message }) {
return <p>{message}</p>;
}

function ParentComponent() {
const [msg, setMsg] = React.useState("Hello");

return (
<div>
<ChildComponent message={msg} />
<button onClick={() => setMsg("Hi there!")}>Update Message</button>
</div>
);
}

父组件重新渲染

当父组件重新渲染时,其所有子组件也会随之重新渲染,即使子组件的 props 没有变化。React 通过 Diff 算法优化了这种情况,确保只有真实发生变化的部分才会更新到 DOM。

forceUpdate 强制重新渲染

在类组件中,调用 this.forceUpdate() 可以强制组件重新渲染,即使 stateprops 没有变化。虽然可以使用,但这是一种不常见的做法,因为强制更新可能会导致性能问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class App extends React.Component {
forceUpdateComponent() {
this.forceUpdate(); // 强制组件重新渲染
}

render() {
return (
<div>
<p>Force update example</p>
<button onClick={() => this.forceUpdateComponent()}>Force Update</button>
</div>
);
}
}

useContext 中的值发生变化

如果组件使用了 React 的 Context API 并且依赖的 context 值发生了变化,那么该组件会触发重新渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const ThemeContext = React.createContext();

function App() {
const [theme, setTheme] = React.useState("light");

return (
<ThemeContext.Provider value={theme}>
<button onClick={() => setTheme("dark")}>Change Theme</button>
<ThemedComponent />
</ThemeContext.Provider>
);
}

function ThemedComponent() {
const theme = React.useContext(ThemeContext);
return <p>Current theme: {theme}</p>;
}

每当 theme 改变时,ThemedComponent 会根据新的 context 值重新渲染。

React相关Hooks

原理

React Hooks 是通过 闭包链表存储状态依赖顺序调用 来实现的。它们与 React 的 Fiber 树紧密结合,React 通过内部的数据结构维护每个组件的 Hook 状态和副作用,使得函数组件具备类组件的功能。

  • 闭包:JavaScript 中的闭包机制允许函数“记住”外部变量,并在后续调用时访问它们。这在 React 中用来保持组件的状态。
  • 调用顺序与调用次数:React Hooks 的设计依赖于组件的渲染顺序,Hooks 必须按固定的顺序调用。React 通过内部机制跟踪每个 Hook 调用的顺序,以保证状态与逻辑的正确更新。

因此,Hooks 不能出现在条件语句、循环或嵌套函数中。它们必须始终在组件的顶层执行。

React 的 Hooks 提供了一种在函数组件中使用状态和其他 React 特性的方式。

Hooks 的状态存储机制

React Hooks 的状态存储和管理依赖于 Fiber 树,这是 React 内部的数据结构,它表示组件的渲染单元。

  • 状态链表:React Hooks 在每个组件的 Fiber 节点中维护一个状态链表,这个链表保存了每个 Hook 的状态。在组件重新渲染时,React 依赖这个链表来更新和保持状态值。
  • 指针跟踪:每当 React 调用 useStateuseEffect 等 Hook 时,它会通过一个内部的指针遍历这个链表,确保每个 Hook 的状态或副作用被正确应用。

useState

useState 是用于在函数组件中添加状态的 Hook。

1
2
3
4
5
6
7
8
9
10
11
12
import React, { useState } from 'react';

function Counter() {
const [count, setCount] = useState(0);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}

需要在函数组件中管理局部状态时使用。useState 的初始值只在第一次渲染时使用,后续渲染会忽略初始值。状态更新是异步的,不能依赖状态的立即更新。

useEffect

useEffect 是用于在函数组件中执行副作用的 Hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useEffect, useState } from 'react';

function DataFetcher() {
const [data, setData] = useState(null);

useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => setData(data));

return () => {
console.log('Cleanup');
};
}, []); // 空依赖数组表示只在挂载和卸载时执行

return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

数据获取、订阅、手动更改 DOM 等副作用。依赖数组控制副作用的执行时机,空数组表示只在挂载和卸载时执行。回调函数可以返回一个清理函数,用于清理副作用。

[!IMPORTANT]

useEffect 不传递依赖项数组时,副作用函数会在每次渲染后都执行。

当传递一个空的依赖项数组时,useEffect 只会在组件挂载时执行一次。

useEffect 的清理函数在组件卸载时或下一次执行副作用前运行,用于清理副作用。

useContext

useContext 是用于在函数组件中访问上下文的 Hook。

1
2
3
4
5
6
7
8
9
import React, { useContext } from 'react';

const ThemeContext = React.createContext('light');

function ThemedButton() {
const theme = useContext(ThemeContext);

return <button style={{ background: theme === 'dark' ? '#333' : '#FFF' }}>Themed Button</button>;
}

当需要跨组件层级传递数据时使用,确保上下文提供者(Provider)在组件树中包裹了需要使用上下文的组件。

useReducer

useReducer 是用于在函数组件中管理复杂状态逻辑的 Hook,类似于 Redux 的 reducer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);

return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}

当状态逻辑复杂且涉及多个子值时使用,useReducer 返回的状态和 dispatch 函数类似于 Redux 的 statedispatch

useRef

useRef 是用于在函数组件中访问 DOM 元素或保存任意值的 Hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useRef, useEffect } from 'react';

function TextInputWithFocusButton() {
const inputEl = useRef(null);

const onButtonClick = () => {
inputEl.current.focus();
};

return (
<div>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</div>
);
}

访问 DOM 元素或保存不需要触发重新渲染的值,useRef 返回的对象在组件的整个生命周期内保持不变。

useLayoutEffect

useLayoutEffectuseEffect 类似,但它会在所有 DOM 变更之后同步执行。

1
2
3
4
5
6
7
8
9
10
11
12
import React, { useLayoutEffect, useRef } from 'react';

function MeasureLayout() {
const divRef = useRef();

useLayoutEffect(() => {
const rect = divRef.current.getBoundingClientRect();
console.log(rect);
}, []);

return <div ref={divRef}>Measure my layout</div>;
}

需要在 DOM 更新后立即执行副作用,例如测量 DOM 元素的大小和位置,useLayoutEffect 会阻塞浏览器的绘制,影响性能,慎用。

useMemo

useMemo 是用于在函数组件中优化性能的 Hook,通过记住计算结果来避免不必要的计算。

1
2
3
4
5
6
7
8
9
10
11
12
import React, { useMemo } from 'react';

function ExpensiveCalculationComponent({ num }) {
const expensiveCalculation = (n) => {
console.log('Calculating...');
return n * 2;
};

const result = useMemo(() => expensiveCalculation(num), [num]);

return <div>Result: {result}</div>;
}

当计算开销较大且依赖于特定值时使用,useMemo 仅在依赖项变化时重新计算结果。

useCallback

useCallback 是用于在函数组件中优化性能的 Hook,通过记住函数实例来避免不必要的重新创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useState, useCallback } from 'react';

function Counter() {
const [count, setCount] = useState(0);

const increment = useCallback(() => {
setCount(c => c + 1);
}, []);

return (
<div>
<button onClick={increment}>Increment</button>
<p>{count}</p>
</div>
);
}

当函数作为子组件的 props 传递且依赖于特定值时使用,useCallback 返回的函数在依赖项未变化时保持不变。

useImperativeHandle

useImperativeHandle 是用于自定义暴露给父组件的实例值的 Hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { useImperativeHandle, forwardRef, useRef } from 'react';

const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();

useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));

return <input ref={inputRef} />;
});

function ParentComponent() {
const inputRef = useRef();

return (
<div>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus Input</button>
</div>
);
}

当需要暴露自定义的实例方法给父组件时使用,需要与 forwardRef 一起使用。

useDebugValue

useDebugValue 是一个用于在 React 开发者工具中显示自定义 Hook 调试信息的 Hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useDebugValue } from 'react';

function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);

// 假设这里有一些逻辑来订阅好友状态并更新 isOnline

useDebugValue(isOnline ? 'Online' : 'Offline');

return isOnline;
}

function FriendListItem({ friend }) {
const isOnline = useFriendStatus(friend.id);

return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{friend.name}
</li>
);
}

用于自定义 Hook 中,帮助在 React 开发者工具中显示有意义的调试信息,仅在开发环境中使用,对生产环境没有影响。

useDeferredValue

useDeferredValue 用于延迟更新某些值,以避免在高频率更新的情况下影响性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { useState, useDeferredValue } from 'react';

function SearchComponent() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);

// 假设这里有一些逻辑使用 deferredQuery 进行搜索

return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}

要延迟某些值的更新,以避免高频率更新对性能的影响,适用于优化性能,但需要谨慎使用,确保延迟更新不会影响用户体验。

useTransition

useTransition 用于管理 UI 状态的过渡,允许在某些状态更新时显示加载指示器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState, useTransition } from 'react';

function TransitionComponent() {
const [isPending, startTransition] = useTransition();
const [count, setCount] = useState(0);

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

return (
<div>
<button onClick={handleClick}>Increment</button>
{isPending ? <p>Loading...</p> : <p>Count: {count}</p>}
</div>
);
}

需要在状态更新时显示加载指示器,改善用户体验,确保过渡状态不会影响用户体验,合理使用加载指示器。

总结

  • **useState**:管理组件内部的状态。

  • **useEffect**:处理副作用,如数据获取和 DOM 操作。

  • **useContext**:访问上下文数据。

  • **useReducer**:管理复杂状态逻辑。

  • **useRef**:访问 DOM 元素或保存不需要触发重新渲染的值。

  • **useLayoutEffect**:在 DOM 更新后立即执行副作用。

  • **useMemo**:优化性能,通过记住计算结果避免不必要的计算。

  • **useCallback**:优化性能,通过记住函数实例避免不必要的重新创建。

  • **useImperativeHandle**:自定义暴露给父组件的实例值。

    高阶组件

HOC 是一个函数,接收一个组件并返回一个新的组件:

1
2
3
4
5
6
7
8
const withEnhancement = (WrappedComponent) => {
return class EnhancedComponent extends React.Component {
render() {
// 你可以在这里添加额外的逻辑或属性
return <WrappedComponent {...this.props} />;
}
};
};

React Hooks 的规则及其原因

React Hooks 是一种用于在函数组件中使用状态和副作用的机制,但使用 Hooks 时必须遵守一些严格的规则。这些规则是为了保证 React 内部的状态管理和渲染逻辑的正确性和一致性。如果违反规则,React 会在开发模式中抛出错误。

Hooks 的规则

  1. 只在最顶层调用 Hook
  2. 只在 React 函数组件或自定义 Hook 中调用 Hook

规则 1:只在最顶层调用 Hook

  • 不要在循环、条件语句、嵌套函数中使用 Hook
  • Hooks 必须在组件的最顶层被调用。

示例(错误用法):

1
2
3
4
5
6
7
function MyComponent() {
if (someCondition) {
const [count, setCount] = useState(0); // ❌ 错误:Hook 不能在条件语句中使用
}

return <div>My Component</div>;
}

React 是通过Hook 的调用顺序来管理组件的状态和副作用的。如果 Hook 的调用顺序在每次渲染时发生变化(如在 if 语句或循环中),React 会无法正确关联状态与组件

  • 为什么这么做?
    • React 依赖于每次渲染期间 Hook 的调用顺序和位置。如果 Hook 的顺序在渲染过程中发生改变(如条件语句中不同的 Hook 调用路径),React 无法保证正确更新状态或执行副作用。

正确用法:

1
2
3
4
5
6
7
8
9
function MyComponent() {
const [count, setCount] = useState(0); // ✅ 在最顶层调用 Hook

if (someCondition) {
console.log('条件为 true');
}

return <div>{count}</div>;
}

规则 2:只在 React 函数组件或自定义 Hook 中使用 Hook

描述:

  • 不要在普通函数或类组件中使用 Hook
  • Hooks 只能在 React 函数组件自定义 Hook 中调用。

示例(错误用法):

1
2
3
function doSomething() {
const [count, setCount] = useState(0); // ❌ 错误:Hook 不能在普通函数中使用
}

React Hooks 是专门为 React 函数组件和自定义 Hook 设计的,普通函数没有组件的生命周期和状态管理,也不会触发 React 的渲染机制

  • 为什么这么做?
    • Hooks 的状态与 React 的组件树和生命周期紧密绑定。普通函数不在 React 的生命周期内,React 无法对这些函数中的状态和副作用进行跟踪和管理。

正确用法:

1
2
3
4
5
6
7
8
9
10
11
// 自定义 Hook
function useCustomHook() {
const [state, setState] = useState(0);
return [state, setState];
}

// 函数组件中使用自定义 Hook
function MyComponent() {
const [count, setCount] = useCustomHook();
return <div>{count}</div>;
}

为什么需要遵守这些规则?

  1. 保证 Hook 的调用顺序不变

    • React 内部通过 Hook 的调用顺序来管理状态。如果 Hook 的调用顺序变化,React 无法正确地分配状态和副作用。
  2. 确保状态和副作用能与组件正确绑定

    • 只有在 React 组件中调用 Hook,React 才能跟踪状态变化并触发组件重新渲染。
  3. 避免潜在的错误和复杂逻辑

    • 如果允许在条件语句、循环中使用 Hook,会导致状态管理的复杂性大大增加,可能引入难以排查的 bug。
  4. 提高代码的一致性和可维护性

    • 遵循这些规则,可以让代码逻辑更加清晰,开发者能预期 Hook 的行为和调用顺序。

总结

React Hooks 的规则:

  1. 只在最顶层调用 Hook,避免在循环、条件、嵌套函数中调用。
  2. 只在 React 函数组件或自定义 Hook 中使用 Hook,不要在普通函数或类组件中使用。

为什么需要这些规则?

这些规则的核心目的是:

  • 保证状态管理的正确性
  • 简化组件逻辑,避免复杂的状态错误。
  • 提升代码的可读性和一致性

遵循这些规则,你可以避免常见的 Hooks 误用问题,同时更好地利用 React 的状态管理和生命周期机制。

组件通信

父组件向子组件传递数据(Props)

这是最常见的通信方式,父组件可以通过props将数据或函数传递给子组件。子组件只能读取props,不能直接修改它。

1
2
3
4
5
6
7
8
function ChildComponent({ message }) {
return <div>{message}</div>;
}

function ParentComponent() {
const parentMessage = "Hello from Parent!";
return <ChildComponent message={parentMessage} />;
}

子组件向父组件通信(回调函数 Props)

虽然props是单向数据流(从父到子),但我们可以通过将回调函数作为props传递给子组件,使子组件能够调用该回调函数来向父组件发送数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
function ChildComponent({ sendMessage }) {
const message = "Hello from Child!";

return <button onClick={() => sendMessage(message)}>Send Message</button>;
}

function ParentComponent() {
const handleMessage = (childMessage) => {
console.log(childMessage);
};

return <ChildComponent sendMessage={handleMessage} />;
}

兄弟组件之间通信(通过父组件共享状态)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Sibling1({ updateMessage }) {
return <button onClick={() => updateMessage("Hello from Sibling 1!")}>Update Message</button>;
}

function Sibling2({ message }) {
return <div>Message: {message}</div>;
}

function ParentComponent() {
const [message, setMessage] = React.useState("Initial Message");

return (
<>
<Sibling1 updateMessage={setMessage} />
<Sibling2 message={message} />
</>
);
}

跨层级组件通信(Context)

使用React的Context API,在组件树中传递数据,无需手动逐层传递props。这在某些情况下可以避免props层层传递的繁琐过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const MessageContext = React.createContext();

function ChildComponent() {
const message = React.useContext(MessageContext);
return <div>{message}</div>;
}

function ParentComponent() {
const message = "Hello from Context!";

return (
<MessageContext.Provider value={message}>
<ChildComponent />
</MessageContext.Provider>
);
}

在这个例子中,MessageContext用于共享数据,ChildComponent通过useContext获取父组件提供的message,而无需通过props逐层传递。

Redux或其他状态管理库

使用Redux等状态管理库来集中管理应用的状态,不同组件可以通过订阅状态来实现通信,或者通过派发动作来修改状态。

通过事件总线(Pub/Sub 模式)

在一些场景下,你可以使用事件总线模式,通过订阅和发布事件来进行跨组件通信。这种方式不依赖于组件树的结构。第三方库如EventEmitter可以帮助实现这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { EventEmitter } from 'events';

const eventBus = new EventEmitter();

function Component1() {
const sendMessage = () => {
eventBus.emit('message', 'Hello from Component1');
};

return <button onClick={sendMessage}>Send Message</button>;
}

function Component2() {
React.useEffect(() => {
eventBus.on('message', (message) => {
console.log(message);
});

return () => {
eventBus.removeAllListeners();
};
}, []);

return <div>Listening for messages...</div>;
}

Ref

使用ref引用直接访问子组件的DOM或实例。

WebSocket或其他通信协议

在网络应用中,可以使用WebSocket等通信协议,在客户端和服务器之间进行实时通信。

封装自定义hooks

要实现一个用于定时器的 React Hook,你可以使用 useState 和 useEffect Hooks 来创建和管理定时器的状态和行为。以下是一个简单的示例,展示了如何创建一个自定义的 useTimer Hook,它可以启动、暂停和重置定时器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import React, { useState, useEffect, useRef } from 'react';

// 自定义 useTimer Hook
function useTimer(initialSeconds = 0, { onTick, onEnd } = {}) {
const [seconds, setSeconds] = useState(initialSeconds);
const [isActive, setIsActive] = useState(false);
const timerRef = useRef(null);

useEffect(() => {
if (isActive && !timerRef.current) {
// 创建定时器
timerRef.current = setTimeout(() => {
if (seconds > 0) {
setSeconds((prevSeconds) => prevSeconds - 1);
} else {
clearTimeout(timerRef.current);
timerRef.current = null;
setIsActive(false);
onEnd?.();
}
}, 1000);

// 返回清除定时器的函数
return () => {
clearTimeout(timerRef.current);
timerRef.current = null;
};
}

// 如果有 onTick 回调,每次状态更新时调用它
onTick?.(seconds);

}, [isActive, seconds, onTick, onEnd]); // 依赖项数组,确保当这些值变化时重新执行 effect

// 暴露控制定时器的方法
const start = () => setIsActive(true);
const pause = () => {
clearTimeout(timerRef.current);
timerRef.current = null;
};
const reset = (newSeconds = initialSeconds) => {
pause();
setSeconds(newSeconds);
};

return {
seconds,
isActive,
start,
pause,
reset
};
}

// 使用 useTimer Hook 的组件示例
function TimerComponent() {
const { seconds, isActive, start, pause, reset } = useTimer(10, {
onTick: (currentSeconds) => console.log(`Current time: ${currentSeconds}s`),
onEnd: () => console.log('Timer finished!')
});

return (
<div>
<p>Timer: {seconds}s</p>
<button disabled={isActive} onClick={start}>Start</button>
<button disabled={!isActive} onClick={pause}>Pause</button>
<button onClick={() => reset(5)}>Reset to 5s</button>
</div>
);
}

export default TimerComponent;

在这个 useTimer Hook 中

我们使用 useState 创建了 seconds 和 isActive 状态变量,它们分别用于存储定时器的剩余秒数和定时器的活动状态。
使用 useRef 创建了一个 timerRef 引用,用于存储定时器的 setTimeout 返回值,以便稍后清除它。
在 useEffect 中,我们根据 isActive 状态决定是否创建定时器。当定时器需要运行时,我们设置定时器,并在每次 seconds 减少时更新状态。
我们还提供了 start、pause 和 reset 方法来控制定时器的行为,这些方法可以在组件内部或其他地方使用。
onTick 和 onEnd 是可选的回调函数,分别在定时器每次更新和结束时调用。

在 TimerComponent 组件中,我们展示了如何使用 useTimer Hook,并提供了开始、暂停和重置定时器的按钮。同时,我们使用了 onTick 和 onEnd 回调来在控制台输出当前时间和定时器结束的消息。

请注意,这个 Hook 是一个基本示例,你可以根据具体需求进行调整和扩展。例如,你可以添加更复杂的逻辑来处理定时器的精度、错误处理、多个定时器的管理等。

React性能优化有哪些

使用 React.memo 防止不必要的重新渲染

React.memo 是一个高阶组件(HOC),用于将函数组件进行“记忆化”(memoization)。如果组件的 props 没有发生变化,它可以防止组件的重新渲染。

1
2
3
const MyComponent = React.memo(function MyComponent(props) {
return <div>{props.name}</div>;
});

通过 React.memo,只有当 data prop 改变时,MyComponent 才会重新渲染。适用于那些纯展示型的组件,避免每次父组件更新时都重新渲染。

使用 useMemo 缓存计算结果

useMemo 用于缓存函数的计算结果。它可以防止每次渲染时重新计算一些开销较大的计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useMemo } from 'react';

function ExpensiveComponent({ items }) {
const sortedItems = useMemo(() => {
return items.sort();
}, [items]); // 仅当 items 变化时重新计算

return (
<div>
{sortedItems.map(item => (
<div key={item}>{item}</div>
))}
</div>
);
}

useMemo 确保 items 只有在变化时才会重新进行排序,避免不必要的计算。

[!IMPORTANT]

当依赖数组为空时,意味着useMemo中的函数只会在组件初次渲染时执行一次,以后不会再重新执行,即使组件重新渲染。

使用 useCallback 缓存函数

useCallbackuseMemo 的变体,用于缓存函数引用,防止每次渲染时创建新的函数。它常用于防止子组件不必要的重新渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useCallback } from 'react';

function ParentComponent() {
const [count, setCount] = useState(0);

const increment = useCallback(() => {
setCount(c => c + 1);
}, []);

return <ChildComponent onIncrement={increment} />;
}

function ChildComponent({ onIncrement }) {
return <button onClick={onIncrement}>Increment</button>;
}

通过 useCallbackincrement 函数在依赖项不变的情况下不会重新创建,从而防止 ChildComponent 的不必要重新渲染。

[!IMPORTANT]

如果依赖数组为空,useCallback 只会在组件首次渲染时创建一次函数,此后不会再重新生成该函数,无论组件如何重新渲染。

避免匿名函数的传递

在 JSX 中,传递匿名函数会导致每次渲染时都创建新的函数,可能导致不必要的子组件重新渲染。

原始:

1
<button onClick={() => handleClick()}>Click me</button>

每次渲染时都会创建一个新的匿名函数,导致子组件重新渲染。

优化:

1
<button onClick={handleClick}>Click me</button>

通过直接传递函数引用而不是匿名函数,避免了不必要的渲染。

使用 shouldComponentUpdatePureComponent

对于类组件,shouldComponentUpdate 方法可以用来控制组件是否需要重新渲染。PureComponentReact.Component 的一个优化版本,它会对 propsstate 进行浅比较,从而自动阻止不必要的渲染。

1
2
3
4
5
class MyComponent extends React.PureComponent {
render() {
return <div>{this.props.data}</div>;
}
}

PureComponent 中,只有当 propsstate 浅比较发生变化时,才会重新渲染。

使用 SuspenseReact.lazy 进行代码拆分(Code Splitting)

React 支持通过 React.lazySuspense 来进行动态加载组件。这有助于减小初始加载的 JavaScript 体积,提升首屏加载速度。

1
2
3
4
5
6
7
8
9
10
11
import React, { Suspense, lazy } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

function MyComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}

LazyComponent 只有在需要时才会被加载,减少初始加载体积。

使用 useTransitionstartTransition

React 18 引入了并发模式,并且提供了 useTransitionstartTransition 来更好地调度 UI 更新。这对于那些较为繁重的更新任务,可以将它们标记为“过渡更新”,从而不会阻塞用户的交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState, useTransition } from 'react';

function MyComponent() {
const [isPending, startTransition] = useTransition();
const [inputValue, setInputValue] = useState('');

const handleChange = (e) => {
startTransition(() => {
setInputValue(e.target.value);
});
};

return (
<div>
{isPending ? <p>Loading...</p> : null}
<input type="text" onChange={handleChange} />
</div>
);
}

startTransition 可以将更新操作标记为低优先级的过渡任务,从而不会阻塞其他重要的渲染任务。

懒加载图片(Lazy Load Images)

通过懒加载图片,可以减少页面初次加载时的带宽占用,尤其是对于大量图片的页面。

可以使用原生的 loading="lazy" 属性,或者使用第三方库如 react-lazyload 来实现图片懒加载。

1
<img src="image.jpg" alt="description" loading="lazy" />

使用 Immutable 数据结构

React 依赖于 propsstate 的不可变性来检测组件是否需要重新渲染。使用不可变的数据结构(如 Immutable.js 或直接深拷贝数据)可以减少因数据变更导致的性能问题。

1
2
3
4
import { Map } from 'immutable';

const data = Map({ name: 'React' });
const newData = data.set('name', 'React Optimized');

Immutable.js 提供了高效的不可变数据结构,可以优化 React 组件的性能。

避免深层次的组件树——嵌套过深

深层次的组件树会导致更多的渲染和状态传递。可以通过提升状态(state lifting)或使用 Context API 来减少不必要的状态传递。

使用 Web Workers 处理重计算任务

如果应用中有需要大量计算的任务,可以将这些计算任务放到 Web Worker 中运行,避免阻塞主线程的 UI 渲染。

开启生产环境构建

React 在开发环境下包含大量的警告和错误检查,这在生产环境中是不需要的。因此,确保在生产环境下运行时,通过 NODE_ENV=production 构建生产版本,可以显著提升性能。

避免不必要的 Refs

虽然 Refs 在某些场景下是必要的,但不当的使用会导致代码复杂性和性能问题。除非需要直接操作 DOM,否则尽量避免使用 refs

总结

React 提供了多种工具和方法来优化性能,主要包括避免不必要的渲染、缓存计算结果、动态加载代码以及合理使用生命周期方法等。通过使用这些优化策略,可以显著提升 React 应用的性能,减少资源消耗和提升用户体验。

React18的新特性

React 18 引入了一些重要的新特性,旨在提升性能、改善开发体验,并为未来的功能扩展奠定基础。

并发特性(Concurrent Features)

React 18 的并发特性是它的核心升级之一。并发模式允许 React 更加灵活地处理 UI 更新,通过优先处理紧急的用户交互,推迟一些不那么重要的更新任务,从而提升应用的响应速度和流畅度。

并发模式并不是默认启用的,需要在应用中显式使用诸如 useTransitionstartTransition 等新的 API 才能触发并发特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState, useTransition } from 'react';

function MyComponent() {
const [isPending, startTransition] = useTransition();
const [inputValue, setInputValue] = useState('');

const handleChange = (e) => {
startTransition(() => {
setInputValue(e.target.value);
});
};

return (
<div>
{isPending ? <p>Rendering in progress...</p> : null}
<input type="text" onChange={handleChange} />
</div>
);
}
  • 并发模式的目的是减少 UI 中因长时间计算导致的卡顿,React 可以在后台处理非紧急的任务,并保持界面响应流畅。
  • React 18 允许将某些渲染任务标记为低优先级,使得 React 在用户交互操作时可以优先处理关键任务,避免界面“卡死”。

startTransitionuseTransition

startTransition 是 React 18 引入的新 API,用于将某些更新标记为“过渡性”的低优先级更新,避免与用户的直接交互冲突。它允许 React 在后台处理复杂的任务,而不影响用户的流畅操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { useState, startTransition } from 'react';

function App() {
const [value, setValue] = useState('');
const [list, setList] = useState([]);

const handleInputChange = (e) => {
setValue(e.target.value);
startTransition(() => {
// 模拟较重的计算任务
const newList = Array.from({ length: 10000 }, (_, index) => `${e.target.value} ${index}`);
setList(newList);
});
};

return (
<div>
<input type="text" value={value} onChange={handleInputChange} />
<ul>
{list.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}

在上面的例子中,startTransition 将列表的生成标记为过渡性任务,这样 React 可以优先处理输入框的更新,而不是让计算和渲染任务阻塞用户的操作。

自动批处理(Automatic Batching)

批处理(Batching)是 React 在同一个事件中对多次状态更新进行合并的一种优化手段。在 React 18 中,自动批处理的范围扩展到了 异步操作Promise,不仅局限于事件处理函数中,这可以减少不必要的渲染次数,从而提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useState } from 'react';

function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');

const handleClick = () => {
fetch('/api')
.then(() => {
// React 18 中自动批处理多个状态更新
setCount(c => c + 1);
setName('React 18');
});
};

return (
<div>
<button onClick={handleClick}>Click me</button>
<p>Count: {count}</p>
<p>Name: {name}</p>
</div>
);
}

在这个例子中,setCountsetName 将在一次渲染中被批处理,而不是导致两次渲染。这在 React 18 中是自动处理的,不需要手动进行批处理。

useId Hook

useId 是 React 18 新增的一个 Hook,用于生成稳定的唯一 ID,适用于无论服务器端还是客户端渲染的场景。这在需要多个相同组件中生成唯一标识时非常有用,比如表单元素的 id 属性。

1
2
3
4
5
6
7
8
9
10
11
12
import React, { useId } from 'react';

function Form() {
const id = useId();

return (
<div>
<label htmlFor={`${id}-input`}>Name:</label>
<input id={`${id}-input`} type="text" />
</div>
);
}

通过 useId,在客户端和服务器端渲染中可以生成一致的 ID,从而避免 ID 冲突或不一致的问题。

useSyncExternalStoreuseInsertionEffect

  • **useSyncExternalStore**:这是 React 18 中新增的一个 Hook,专门用于订阅外部存储(如 Redux store)的更新,提供了一种安全的方式来确保 React 的渲染与外部存储保持同步。这个 Hook 确保即使在并发模式下,React 也能正确地处理外部状态的变化。

  • **useInsertionEffect**:这是另一个新 Hook,主要用于 CSS-in-JS 库等场景,在所有 DOM 变更之前执行,用于确保在渲染发生之前将样式插入到 DOM 中。

Suspense 的增强

React 18 对 Suspense 进行了增强,使其不仅可以用于代码拆分,还可以用于数据加载。配合未来的 React Server ComponentsSuspense 可以帮助处理服务端渲染中的异步数据,从而简化异步 UI 的开发。

1
2
3
4
5
6
7
8
9
10
11
import React, { Suspense, lazy } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}

Suspense 未来将不仅限于懒加载组件,还会支持更多异步场景(如数据获取),大大提升数据密集型应用的开发体验。

Strict Mode 的改进

在 React 18 中,Strict Mode 进行了改进,模拟了双重渲染的机制,以帮助开发者更早地发现潜在的副作用问题。它使得应用在开发环境中会进行双次渲染以暴露潜在的副作用,从而提升应用的稳定性。

双重渲染:

React 18 的 Strict Mode 触发了一些组件的双次渲染,以确保所有副作用能够正确处理。这种机制帮助开发者尽早发现一些生命周期副作用的不当用法。

React Server Components(未来特性)

虽然 React 18 已经为 React Server Components 做了准备,但它目前还没有完全推向生产环境。Server Components 允许在服务器上渲染组件,减少前端 JavaScript 的负担,适合内容密集型的应用。

总结

React 18 带来了多个新特性,重点是提升应用的性能、并发处理能力和开发体验。主要特性包括:

  • 并发模式:startTransitionuseTransition
  • 自动批处理状态更新
  • useId 生成稳定唯一 ID
  • Suspense 的增强
  • 新的 Hooks:useSyncExternalStoreuseInsertionEffect
  • 改进的 Strict Mode

这些新特性为开发更高效、更响应迅速的应用提供了更好的支持,也为未来的 React 发展铺平了道路。

React-Router原理

React Router 是一个基于 React 的库,用于在单页应用程序(SPA)中处理路由。它允许开发者在客户端进行页面的导航而无需重新加载整个页面,这样可以提供类似于传统多页应用的用户体验。React Router 通过管理浏览器的历史记录来实现这一点,使用 HTML5 History API 或者 hash-based routing 来同步 UI 与 URL。下面是 React Router 工作原理的详细解释:

组件与路由映射

React Router 主要依赖几个核心组件,如 <Router>, <Route>, <Link>, <Switch> 等,来实现路由功能:

  • <Router>:这是所有路由组件的容器,它监听浏览器地址的变化,并将其传递给其子组件。
  • <Route>:此组件用于声明路由规则和与之对应的组件。当URL与某个 <Route> 的路径匹配时,React Router 就会渲染这个路由对应的组件。
  • <Link>:用于创建可导航的链接,在点击时更新 URL 而不会导致页面重载。
  • <Switch>:用于包裹多个 <Route>,并且只渲染与当前 URL 匹配的第一个 <Route> 子项。

历史记录管理

React Router 通过封装 History 库来管理浏览器的历史记录。这个库提供了接口来监听和操作浏览器的历史(即 URL),包括:

  • history.push():向历史堆栈添加一个新的条目。
  • history.replace():替换当前历史堆栈中的条目。
  • history.goBack()history.goForward():后退和前进浏览历史。

路由模式

React Router 提供两种主要的路由模式:

  • Browser Router:使用 HTML5 History API 来保持 UI 和 URL 的同步。这需要服务器的支持,以便在任何路由请求时都返回同一个 index.html 页面。
  • Hash Router:使用 URL 的哈希部分(即 window.location.hash)来保持 UI 和 URL 的同步。这种模式不需要服务器的特殊配置,因为哈希值的改变不会导致服务器的请求。

动态路由

React Router 允许定义动态路由,这意味着路由中的某些部分可以是变量。例如,使用 /users/:id 可以匹配 /users/1/users/2。匹配的参数 (id) 可以通过 React Router 提供的钩子(如 useParams)在组件内部获取。

5. 嵌套路由

React Router 支持路由的嵌套,这使得在具有复杂 UI 结构的大型应用中的路由管理变得更加方便。例如,一个具有多个层级导航的应用可以在每个层级定义 <Route>,从而实现层次化的视图更新。

总结来说,React Router 的工作原理是通过监听和同步浏览器的 URL 来动态渲染对应的 React 组件,同时通过操作历史记录 API 来实现客户端的导航,这些都在不重新加载页面的情况下完成。这样既优化了用户体验,又保持了 Web 应用的响应性和效率。