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)));
不可变数据操作技巧:
- 数组:使用
map
、filter
、slice
、展开运算符[...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 中的工作流程可以分为三个步骤:
- 创建虚拟 DOM:当 React 组件渲染时,会生成一个虚拟 DOM 树,用 JavaScript 对象表示 UI 的当前状态
- 比较变化:当组件的状态或属性变化时,React 会生成一个新的虚拟 DOM 树,并与旧树进行比较,找出具体的差异
- 更新真实 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 报错,导致父组件的内容 也无法渲染,页面空白。
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。
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
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 中,!
后缀运算符被称为非空断言操作符。当应用于表达式时,它告诉编译器:「我确信这个表达式的值在这个上下文中不会是 null
或 undefined
」。
在 ReactDOM.createRoot(document.getElementById('root')!)
这行代码中,document.getElementById('root')
返回的是 HTMLElement | null
类型,因为 DOM API 可能找不到与给定 ID 匹配的元素,此时会返回 null
。然而在实际应用中,通常有一个 id 为 root
的 HTML 元素用于挂载 React 应用,所以「断言」这里不可能是 null
。
加上 !
后,ts 编译器会忽略对这一表达式可能为 null
或 undefined
的检查,并假设它始终是一个非空的 HTMLElement
类型实例,这样就可以安全地调用 .createRoot()
方法。
React 如何预防 XSS
JSX 自动转义
React 有一个 dangerouslySetInnerHTML 属性,__html
属性的值是包含 HTML 标签的字符串。
<div dangerouslySetInnerHTML={{ __html: "<script>alert('XSS');</script>" }} />
需要谨慎使用,确保 __html
属性的值是安全的 ,否则可能会导致 XSS 攻击。
可以使用 DOMPurify 库来清理和过滤用户输入的数据,以防止 XSS 攻击。