跳到主要内容

React

简介

React 是一个用于构建 Web 和原生交互界面的库,核心概念是组件化设计声明式编程

React 的特点:当数据发生变化时,UI 能够自动把变化反映出来。

单向数据流和不可变数据

单向数据流

单向数据流指的是数据在组件之间传递的方向是单一的,通常是从父组件向子组件传递,子组件不能直接修改父组件传递过来的数据。这样做的好处是数据流更清晰,易于追踪和调试。如果子组件需要改变数据,必须通过回调函数通知父组件,由父组件来更新状态,然后再传递新的数据给子组件。

// 父组件
function Parent() {
const [count, setCount] = useState(0);
const onUpdate = () => setCount(count + 1);
// 通过 props 传递数据和更新函数给子组件
return <Child count={count} onUpdate={onUpdate} />;
}

// 子组件
function Child({ count, onUpdate }) {
return (
<div>
<p>Count: {count}</p>
<button onClick={onUpdate}>Increment</button>
</div>
);
}

不可变数据

不可变数据是指数据被创建后就不能被修改。任何对数据的更改都会生成一个新的数据副本,而不是在原数据上进行修改。

React 使用不可变数据来管理组件的状态,当状态发生变化时,会创建一个新的不可变对象,而不是修改原来的对象。

// 错误
const [todos, setTodos] = useState([{ text: 'Learn React', done: false }]);
todos[0].done = true; // 直接修改原数组
setTodos(todos); // React 无法检测到变化,不会触发重新渲染

// 正确:创建新数据副本
setTodos(todos.map((todo, index) => (index === 0 ? { ...todo, done: true } : todo)));

不可变数据操作技巧:

  • 数组:使用 mapfilterslice、展开运算符 [...arr] 等。
  • 对象:使用展开运算符 { ...obj }Object.assign
  • 深层嵌套结构:使用 Immer 库。

哪些操作会破坏不可变性?

  • 直接修改原数组:push、pop、splice
  • 直接修改原对象属性:obj.key = newValue
  • 浅拷贝不彻底:const newObj = Object.assign({}, obj),若 obj 包含嵌套对象,仍需深层拷贝

虚拟 DOM

虚拟 DOM(Virtual DOM),是对真实 DOM 的一个轻量级表示,保存在内存中。通过 Object 模拟真实的 DOM 节点对象,再通过特定的 render 方法将其渲染成真实的 DOM 节点。

在每次组件更新时,React 会先生成新的虚拟 DOM 树,并与之前的虚拟 DOM 树进行 diff,只对比出变化的部分,再应用到真实 DOM 上,从而避免了大量的 DOM 操作。

以前是基于浏览器 DOM 的 API 去控制 DOM 节点的创建、修改和删除。

虚拟 DOM 在 React 中的工作流程可以分为三个步骤:

  1. 创建虚拟 DOM:当 React 组件渲染时,会生成一个虚拟 DOM 树,用 JavaScript 对象表示 UI 的当前状态
  2. 比较变化:当组件的状态或属性变化时,React 会生成一个新的虚拟 DOM 树,并与旧树进行比较,找出具体的差异
  3. 更新真实 DOM:根据比较结果,React 计算出需要对真实 DOM 执行的最小更新操作,然后高效地应用这些更新

虚拟 DOM 的优点:

  • 性能优化:虚拟 DOM 可以减少对真实 DOM 的操作次数,提高性能
  • 跨平台:虚拟 DOM 作为一个抽象层,支持多个平台,如 React Native 用于移动端开发
  • 声明式编程:通过声明式语法,可以描述出 UI 的状态,让 React 根据状态的变化,自动更新 UI
  • 处理兼容性:对浏览器的原生事件进行封装,提供统一的事件处理方式

Diff 算法

Diff 算法是虚拟 DOM 的核心算法,它用来比较两个虚拟 DOM 树的差异,找出需要更新的最小部分,然后批量更新到真实的 DOM 上。

通过给每个节点设置一个唯一的 key,Diff 算法可以快速找到相同的节点,减少比较次数。

最小化更新

找出差异后,React 会计算出需要更新的最小操作(称为 patch),只更新实际 DOM 中变化的部分,而不是重绘整个页面。

批量更新

虚拟 DOM 可以将多次状态变更合并成一次更新,而不是每次状态变化就立即更新 DOM,减少了对 DOM 的操作次数,提高了性能。

在 React 18 中,引入了 自动批处理(Automatic Batching) 优化,所有场景默认批处理。

查看文档的批量处理状态更新

import { useState } from 'react';

export default function Counter() {
const [number, setNumber] = useState(0);

const handleClick = () => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
};

return (
<>
<h1>{number}</h1>
<button onClick={handleClick}>+3</button>
</>
);
}

尽管调用了三次 setNumber,number 最终只增加 1,而不是 3。因为 React 将这三次调用合并为一次更新,仅触发一次渲染。

错误边界

错误边界(Error Boundary)是一个 React 组件,该组件可以捕获其子组件的错误,并渲染出备用 UI。

默认情况下,如果 React 渲染期间发生错误,整个组件树都会被卸载。例如有一个父组件,有若干个子组件,当其中一个子组件报错时,整个组件树都不会被渲染。理想情况是,其他组件正常渲染,而报错的组件被隐藏或者替换成备用 UI。

可以使用 react-error-boundary 解决这个问题。

pnpm add react-error-boundary

示例,因为子组件 Foo 报错,导致父组件的内容也无法渲染,页面空白。

Error.jsx
import { useState } from 'react';

function Foo() {
// 这里会报错,没有bar方法
bar();
return <div>foo</div>;
}

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

const handleClick = () => {
setCount(count + 1);
};
return (
<>
<div>{count}</div>
<button onClick={handleClick}>click</button>
<Foo />
</>
);
}

export default App;

使用react-error-boundary后,控制台依然会报错,但是页面正常渲染,错误组件会渲染出备用 UI。

ErrorBoundary.jsx
import { useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

function Foo() {
bar();
return <div>foo</div>;
}

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

const handleClick = () => {
setCount(count + 1);
};
return (
<>
<div>{count}</div>
<button onClick={handleClick}>click</button>
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<Foo />
</ErrorBoundary>
</>
);
}

export default App;

fallback 就是备用 UI,当子组件报错时,会渲染出这个备用 UI。

React.lazy 与 Suspense 实现懒加载

优势:代码分割,减少初始加载体积,提升性能。

  • React.lazy 能够让组件延迟加载,按需加载
  • Suspense 允许在子组件完成加载前展示备用方案

React.lazy 是用于动态导入组件的,通常和 import() 函数一起使用,这样在打包时,相关组件会被单独拆分成块,按需加载。而 Suspense 则是用来包裹这些懒加载的组件,提供加载过程中的 fallback 内容,如加载中的提示。

1、使用 React.lazy 定义懒加载组件

import() 返回的是一个 Promise,React.lazy() 接受一个返回这个 Promise 的函数,将组件转换为懒加载模块。

// 普通导入
// import OtherComponent from './OtherComponent';

// 懒加载导入
import { lazy } from 'react';
const OtherComponent = lazy(() => import('./OtherComponent'));

2、使用 Suspense 包裹懒加载组件,并通过 fallback 属性指定加载中的占位内容。

import { Suspense } from 'react';

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

看一个项目中经常遇到的代码如下,希望在 show 变为 true 时再加载 Foo 组件,实际上控制台依然打印出了 Foo

Foo.jsx
console.log('Foo');

function Foo() {
return <div>Foo</div>;
}

export default Foo;
import Foo from './Foo';
import { useState } from 'react';

function App() {
const [show, setShow] = useState(false);
return (
<>
<div>App</div>
<button onClick={() => setShow(true)}>显示</button>
{show && <Foo />}
</>
);
}

export default App;

使用 <Suspense> 可以在懒加载的组件加载时显示一个正在加载的提示,这个提示可以在控制台将网速设置较慢的时候可以看到。

实现懒加载之后的代码如下:

import { useState, lazy, Suspense } from 'react';

const Foo = lazy(() => import('./Foo.jsx'));

function App() {
const [show, setShow] = useState(false);
return (
<>
<div>App</div>
<button onClick={() => setShow(true)}>显示</button>
<Suspense fallback={<div>Loading...</div>}>{show && <Foo />}</Suspense>
</>
);
}

export default App;

结合 TS 使用

React.FC

不推荐写 React.FC参考这里

type Props = {};

// 不推荐
const App: React.FC<Props> = props => {};

// 推荐
const App = (props: Props) => {};

通过 vite 创建 react 项目

通过 vite 创建的 react 项目里,ReactDOM.createRoot(document.getElementById('root')!),这里最后的 ! 的作用是什么?

! 的作用是为了告诉 TS 编译器:document.getElementById('root') 不会是 null,不要对此产生类型错误警告。

在 TypeScript 中,! 后缀运算符被称为非空断言操作符。当应用于表达式时,它告诉编译器:「我确信这个表达式的值在这个上下文中不会是 nullundefined」。

ReactDOM.createRoot(document.getElementById('root')!) 这行代码中,document.getElementById('root') 返回的是 HTMLElement | null 类型,因为 DOM API 可能找不到与给定 ID 匹配的元素,此时会返回 null。然而在实际应用中,通常有一个 id 为 root 的 HTML 元素用于挂载 React 应用,所以「断言」这里不可能是 null

加上 ! 后,ts 编译器会忽略对这一表达式可能为 nullundefined 的检查,并假设它始终是一个非空的 HTMLElement 类型实例,这样就可以安全地调用 .createRoot() 方法。

React 如何预防 XSS

XSS 跨站脚本攻击

JSX 自动转义

React 有一个 dangerouslySetInnerHTML 属性,__html 属性的值是包含 HTML 标签的字符串。

<div dangerouslySetInnerHTML={{ __html: "<script>alert('XSS');</script>" }} />

需要谨慎使用,确保 __html 属性的值是安全的,否则可能会导致 XSS 攻击。

可以使用 DOMPurify 库来清理和过滤用户输入的数据,以防止 XSS 攻击。