1. React 18 的主要更新
1. React 18(2022年3月)
1. 并发渲染 (Concurrent Rendering)
- 核心概念: React 18 的核心更新是引入了并发渲染,这是一种新的渲染机制,允许 React 在不阻塞主线程的情况下执行多个渲染工作,允许 React 在渲染过程中中断、暂停或调整优先级,尤其是在处理大型或计算密集型任务时,可以保持 UI 的响应性,从而提高应用的响应速度和用户体验。
异步、可中断可恢复、优先级调度的渲染引擎
-
关键特性:
- 可中断渲染:React 可以在渲染过程中暂停并优先处理用户交互(如点击、输入),避免 UI 卡顿。
- 优先级调度:React 可以区分紧急更新(如用户输入)和非紧急更新(如数据加载),确保关键操作优先执行。
- 渐进式升级:并发特性是选择性启用的,开发者可以逐步适配,无需一次性重写整个应用。
并发渲染并不是一个开箱即用的功能,而是通过引入一些新的 API 来让开发者选择性地使用
-
startTransition: 这个新的 API 允许你将某些状态更新标记为“非紧急”的。当一个过渡更新被中断时(例如,用户输入了更紧急的更新),React 会丢弃旧的过渡渲染结果,只关注最新的紧急更新。这在处理一些耗时的 UI 变化(例如,筛选列表、路由切换)时非常有用,可以避免 UI 卡顿。import { startTransition } from 'react'; function SearchInput() { const [inputValue, setInputValue] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const handleChange = (e) => { setInputValue(e.target.value); // 将 searchQuery 的更新标记为非紧急的过渡 startTransition(() => { setSearchQuery(e.target.value); }); }; return ( <> <input value={inputValue} onChange={handleChange} /> <SearchResults query={searchQuery} /> </> ); } -
useTransition
useTransition 是一个帮助你在不阻塞 UI 的情况下更新状态的 React Hook。
useTransition 返回一个由两个元素组成的数组:
isPending,告诉你是否存在待处理的 transition。startTransition函数,你可以使用此方法将状态更新标记为 transition。
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ……
}
-
useDeferredValueHook:useDeferredValue允许你延迟更新一个值的副本。当这个值发生变化时,它会等到其他更紧急的更新完成后再更新自己。这和startTransition有些类似,但useDeferredValue适用于延迟单个值的更新,而不是整个函数块。它通常用于延迟渲染开销较大的部分。import { useDeferredValue } from 'react'; function Typeahead() { const [inputValue, setInputValue] = useState(''); const deferredInputValue = useDeferredValue(inputValue); // 延迟 inputValue 的更新 return ( <> <input value={inputValue} onChange={e => setInputValue(e.target.value)} /> <SlowList text={deferredInputValue} /> {/* SlowList 会在 deferredInputValue 更新时才渲染 */} </> ); }
2. 批量更新/自动批处理 (Automatic Batching
-
核心概念: React 18 会自动将多个状态更新合并,并在一次渲染中完成(批量更新),减少了不必要的渲染。
-
变化:
- React 17 及之前:仅对 React 事件(如
onClick)中的更新进行批处理,setTimeout、setInterval、Promise或原生事件中的更新不会批处理,它们会触发多次重新渲染。 - React 18:所有更新(包括异步代码)都会自动批处理,减少渲染次数。
- React 17 及之前:仅对 React 事件(如
3. 新的 Root API
React 18 引入了一个新的根 API,用于渲染你的 React 应用。旧的 ReactDOM.render 方法现在被称为“遗留 Root API”,虽然仍然可以使用,但它不支持 React 18 的并发特性。
- createRoot: 用于创建新的 React 根节点,取代了之前的 ReactDOM.render。
//React 17 import React from "react" import ReactDOM from "react-dom" import App from "./App" const root = document.getElementById("root") ReactDOM.render(<App/>,root) // 卸载组件 ReactDOM.unmountComponentAtNode(root) // React 18 import React from "react" import ReactDOM from "react-dom/client" import App from "./App" const root = document.getElementById("root") ReactDOM.createRoot(root).render(<App/>) // 卸载组件 root.unmount() - hydrateRoot: 用于在服务器渲染的 HTML 上进行客户端渲染。
// React 17 import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; const root = document.getElementById("root"); ReactDOM.render(<App />, root); // 卸载组件 ReactDOM.unmountComponentAtNode(root); // React 18 import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; const root = document.getElementById("root"); ReactDOM.createRoot(root).render(<App />); // 卸载组件 root.unmount();
4. Strict Mode
- 新增规则:
<StrictMode>会模拟卸载和重新挂载组件,帮助你在开发过程中尽早地发现组件中的常见错误。
当你使用严格模式时,React会对每个组件返回两次渲染,以便你观察一些意想不到的结果,在react17中去掉了一次渲染的控制台日志,以便让日志容易阅读。react18取消了这个限制,第二次渲染会以浅灰色出现在控制台日志-
严格模式启用了以下仅在开发环境下有效的行为:
- 组件将 重新渲染一次,以查找由于非纯渲染而引起的错误。
- 组件将 重新运行 Effect 一次,以查找由于缺少 Effect 清理而引起的错误。
- 组件将被 检查是否使用了已弃用的 API。
-
- 常见警告:
- 未使用的 state
- 无效的 useEffect 依赖项
- 组件在卸载时仍持有计时器或其他资源
5. Suspense
- 核心概念: 可以用来处理异步操作,比如数据加载、代码分割等。
- 具体表现:
- 加载状态: 在数据加载期间显示加载状态。
- 错误边界: 可以捕获异步操作中的错误。
6. React 组件返回值更新
- 在 React 17 中,返回空组件只能返回
null,显式返回undefined会报错。 - 在 React 18 中,支持
null和undefined返回。
其他更新
- React.lazy 和 Suspense 的结合: 可以实现代码分割,按需加载组件。
- 新的服务器端渲染 API: 提升了服务器端渲染的性能和灵活性。
- 去掉了对IE浏览器的支持,react18引入的新特性全部基于现代浏览器,如需支持需要退回到react17版本。
2. React 16(2017年9月)Fiber 架构与 Hooks 的革命
- Fiber 架构(16.0.0): 2017年9月 引入异步渲染,提高性能,支持任务中断和恢复。
- 新生命周期:调整生命周期方法(如
getDerivedStateFromProps)。 - Hooks(16.8) :2019年2月发布,允许函数组件使用状态和副作用(
useState,useEffect)
2. 什么是 React?它与传统前端框架有什么区别?
React 是一个用于构建用户界面的 JavaScript 库。
数据驱动视图
MVVM 模型中,UI=f(data)。通过上面这个公式得出,如果要渲染界面,不应该直接操作 DOM,而是通过修改数据(state 或 prop),数据驱动视图更新。
声明式编程
React 使用 JSX 语法,以声明式的方式描述 UI,让开发者更关注 UI 的最终状态,而不是具体的 DOM 操作。
组件化
React 将 UI 拆分成可复用的组件,提高代码可维护性。
每个组件都符合开放-封闭原则,封闭是针对渲染工作流来说的,指的是组件内部的状态都由自身维护,只处理内部的渲染逻辑。开放是针对组件通信来说的,指的是不同组件可以通过props(单项数据流)进行数据交互
虚拟 DOM
React 使用虚拟 DOM 来优化 DOM 操作,提高性能。
由浏览器的渲染流水线可知,DOM操作是一个昂贵的操作,很耗性能,因此产生了虚拟DOM。虚拟DOM是对真实DOM的映射,React通过新旧虚拟DOM对比,得到需要更新的部分,实现数据的增量更新
数据流
React: 采用单向数据流,数据从父组件传递给子组件,子组件不能直接修改父组件的数据。这种方式使得数据流更加清晰,更容易管理。
3. React 和 Vue 的区别
相同点
- 都使用 MVVM、组件化、虚拟 DOM。
不同点
数据流
- React: 采用单向数据流,数据从父组件传递给子组件,子组件不能直接修改父组件的数据。这种方式使得数据流更加清晰,更容易管理。
- Vue: 支持双向数据绑定,当数据发生变化时,视图会自动更新,反之亦然。
- 数据双向绑定: 数据的变化能够在视图和数据模型之间同步进行。Vue.js 通过
v-model指令来实现这种双向绑定,常用于表单元素中。双向绑定允许视图中的用户输入直接更新数据模型,同时数据模型的变化也会自动更新视图。在这个例子中,输入框的值与<template> <input v-model="message"/> <p>{{ message }}</p> </template> <script> export default { data() { return { message: 'Hello' }; } }; </script>message绑定。当用户在输入框中输入内容时,message的值会自动更新,并在视图中展示最新的message值。
- 数据双向绑定: 数据的变化能够在视图和数据模型之间同步进行。Vue.js 通过
模板语法
- React: 使用 JSX,将 JS 和 HTML 结合在一起,语法相对灵活。
- Vue: 使用基于 HTML 的模板语法,更接近传统的 Web 开发方式。
高阶组件
- React: 可以通过高阶组件 (Higher Order Components -- HOC) 来扩展。
- Vue: 需要通过 mixins 来扩展。
- 原因:高阶组件就是高阶函数,而 React 的组件本身就是纯粹的函数,所以高阶函数对 React 来说易如反掌。相反,Vue.js 使用 HTML 模板创建视图组件,这时模板无法有效的编译,因此 Vue 不采用 HOC 来实现。
生态系统
- React: 生态系统非常丰富,有大量的第三方库和工具可供选择,如 Redux、React Router 等。
- Vue: 生态系统也比较完善,有 Vuex、Vue Router 等官方支持的工具。
学习曲线
- React: 学习曲线相对较陡,需要掌握 JSX、组件化、状态管理等概念。
- Vue: 学习曲线相对平缓,更容易上手。
4. JSX 是什么?为什么使用 JSX?它和 JavaScript 有什么区别?
JSX 是什么?
JSX 是 JavaScript 的语法扩展,允许你在 JavaScript 文件中书写类似 HTML 的标签。主要用于描述 UI 结构。它最终会被编译成标准的 JavaScript 代码。
JSX 和 JS 的区别
| 特性 | JSX | JS(纯 JavaScript) |
|---|---|---|
| 语法 | 类似 HTML,可直接写 <div>、<Component /> | 纯 JavaScript,不能直接写 HTML 标签 |
| 用途 | 用于定义 React 组件的 UI 结构 | 用于编写逻辑、数据处理、函数等 |
| 编译方式 | 需要 Babel 或 TypeScript 编译成 JS | 浏览器或 Node.js 可以直接执行 |
| 表达式嵌入 | 使用 {} 包裹 JavaScript 表达式 | 直接写 JS 代码 |
| 注释方式 | {/* 注释内容 */} | // 单行注释 或 /* 多行注释 */ |
为什么使用 JSX?
- 更直观的 UI 描述:比纯
React.createElement更接近 HTML,提高可读性。 - 编译时优化:Babel 会优化 JSX 编译结果,减少手动编写虚拟 DOM 的代码量。
- 类型安全(TS 支持) :TypeScript 可以直接检查 JSX 语法错误。
- JS 表达式嵌入: 可以直接在 JSX 中嵌入 JavaScript 表达式,方便动态渲染数据。
JSX 工作原理
- 编写 JSX: 在 React 组件中,你可以使用 JSX 来描述 UI 结构。
- Babel 编译: Babel 会将 JSX 编译成普通的 JavaScript 代码,即
React.createElement(React17之前) 或者_jsx(React17+)的调用。 - 虚拟 DOM: React 会将编译后的 JavaScript 代码转换为虚拟 DOM。
- Diff 算法: React 会比较新的虚拟 DOM 和旧的虚拟 DOM,找出差异。
- DOM 更新: React 只会更新那些真正发生变化的 DOM 节点,从而提高性能。
JSX 规则
- 组件名必须大写开头
- 只能返回一个根元素
- 标签必须闭合
- 使用驼峰式命名法给大部分属性命名
- 样式内联使用对象
- 事件小驼峰命名,传递函数引用而非调用
- JavaScript 表达式嵌入使用大括号{}
// 变量
const title = <h1>{user.name}</h1>;
// 表达式计算
const sum = <p>结果: {1 + 2 + 3}</p>;
// 函数调用
function formatName(user) {
return `${user.first} ${user.last}`;
}
const greeting = <h2>{formatName(user)}</h2>;
// 三元表达式
const status = <span>{isLoggedIn ? '已登录' : '未登录'}</span>;
// 数组渲染
const items = ['Apple', 'Banana', 'Cherry'];
const list = (
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
React 组件为什么不能返回多个元素
这个问题也可以理解为 React 组件为什么只能有一个根元素,原因如下:
- 函数返回值限制: React 组件最后会编译为 render 函数,函数的返回值只能是一个。如果不用单独的根节点包裹,就会并列返回多个值,这在 JavaScript 中是不允许的。
- 虚拟 DOM 树结构: React 的虚拟 DOM 是一个树状结构,树的根节点只能是一个。如果有多个根节点,无法确认是在哪棵树上进行更新。Vue 的根节点也只有一个同样的原因。
React 组件怎样可以返回多个组件
- 使用 HOC(高阶组件)
- 使用
React.Fragment, 可以让你将元素列表加到一个分组中,而且不会创建额外的节点(类似于 Vue 的<template>)
import React from 'react';
function MyComponent() {
return (
<React.Fragment>
<h1>Title</h1>
<p>This is a paragraph.</p>
</React.Fragment>
);
}
export default MyComponent;
在这个例子中,MyComponent 返回了多个元素,但这些元素被包裹在一个 React.Fragment 中,因此不会在最终的 DOM 中创建额外的节点。
5. React 组件中是否需要引入 react
1. 在 React 17 之前:必须引入
在 React 17 之前,所有使用 JSX 的组件都必须显式引入 React,因为 JSX 会被 Babel 编译为 React.createElement() 调用,如果没有 React 在作用域内,代码会报错。
示例(React 16 或更早):
import React from 'react'; // 必须引入
function App() {
return <h1>Hello World</hitch1>;
}
编译后的代码:
import React from 'react';
function App() {
return React.createElement("h1", null, "Hello World");
}
👉 如果不引入 React,会报错:React is not defined!
2. 在 React 17+:可以省略引入(但推荐保留)
React 17 引入了新的 JSX 转换方式,不再强制要求引入 React,因为 Babel 和 TypeScript 会自动从 react/jsx-runtime 导入 _jsx 函数,而不再依赖 React.createElement。
示例(React 17+):
function App() {
return <h1>Hello World</h1>;
}
编译后的代码(React 17+):
import { jsx as _jsx } from 'react/jsx-runtime';
function App() {
return _jsx("h1", { children: "Hello World" });
}
👉 即使不写 import React,也能正常运行!
3. 什么情况下仍然需要引入 React?
虽然 React 17+ 可以省略 React 导入,但以下情况仍然需要手动引入:
-
直接使用
React上的 API(如React.memo、React.lazy) -
类组件(需要
React.Component):import React from 'react'; // 必须引入 class App extends React.Component { render() { return <h1>Hello World</h1>; } } -
某些工具链或旧项目(如果 Babel/TS 未配置新的 JSX 转换)。
4. 最佳实践
| 情况 | 是否需要 import React? | 代码示例 |
|---|---|---|
| 函数组件 + Hooks(React 17+) | ❌ 可省略 | import { useState } from 'react'; |
| 类组件 | ✅ 必须引入 | import React from 'react'; |
使用 React.memo/React.lazy | ✅ 必须引入 | import React, { memo } from 'react'; |
| 旧项目(React 16 或更早) | ✅ 必须引入 | import React from 'react'; |
6. React 的生命周期方法有哪些?分别在什么阶段调用?
React 的生命周期方法是指在组件的创建、更新和销毁过程中会被调用的一系列钩子函数。React 的生命周期主要分为三个阶段:挂载阶段(Mounting)、更新阶段(Updating)和卸载阶段(Unmounting)。此外,还有错误处理阶段。
挂载阶段(Mounting)
组件实例被创建并插入到 DOM 中时,挂载阶段的生命周期方法按顺序被调用。
| 生命周期方法 | 调用时机 | 用途 |
|---|---|---|
constructor() | 组件初始化时调用 | 初始化 state、绑定方法 |
static getDerivedStateFromProps(props, state) | 在 render 之前调用 | 根据 props 计算 state(很少用) |
render() | 必须实现,返回 JSX | 渲染 UI |
componentDidMount() | 组件挂载后调用(DOM 已插入) | 发起网络请求、订阅事件、操作 DOM |
-
constructor(props)
- 在组件初始化时调用,通常用于初始化状态 (
this.state) 或绑定方法。
- 在组件初始化时调用,通常用于初始化状态 (
-
static getDerivedStateFromProps(props, state)
- 这是一个静态方法,在
render()之前调用,并且在每次更新时都会调用。它允许组件在更新状态之前根据传入的 props 来更新内部的 state。返回一个对象来更新 state,或者返回 null 表示不更新。
- 这是一个静态方法,在
-
render()
- 此方法是必须的,负责返回组件的 JSX 或者
React.createElement的结构,用于描述 UI 内容。这个方法应该是纯函数,不会触发状态更新或与浏览器直接交互。
- 此方法是必须的,负责返回组件的 JSX 或者
-
componentDidMount()
- 在组件被挂载到 DOM 之后立即调用。这里是执行副作用的好地方,比如数据获取、订阅事件或直接与 DOM 交互。
-
示例: 挂载阶段
class MountingComponent extends React.Component {
constructor(props) {
super(props);
this.state = { message: 'Hello' };
}
static getDerivedStateFromProps(nextProps, prevState) {
return null; // 返回null表示不需要更新state
}
componentDidMount() {
console.log('Component has been mounted');
}
render() {
return <div>{this.state.message}</div>;
}
}
更新阶段(Updating)
组件的状态或 props 发生变化时,会进入更新阶段。
| 生命周期方法 | 调用时机 | 用途 |
|---|---|---|
static getDerivedStateFromProps(props, state) | props 或 state 变化时 | 根据新的 props 调整 state |
shouldComponentUpdate(nextProps, nextState) | 在 render 之前调用 | 控制组件是否更新(优化性能) |
render() | 必须调用 | 重新渲染 UI |
getSnapshotBeforeUpdate(prevProps, prevState) | 在 DOM 更新前调用 | 获取 DOM 更新前的快照(如滚动位置) |
componentDidUpdate(prevProps, prevState, snapshot) | 更新完成后调用 | 执行副作用(如网络请求) |
-
static getDerivedStateFromProps(props, state)
- 此方法与挂载阶段相同,会在每次更新时调用,允许组件在状态更新之前对状态进行调整。
-
shouldComponentUpdate(nextProps, nextState)
- 这个方法决定组件是否需要重新渲染。默认返回 true,如果返回 false,则跳过后续的更新流程。可以用来优化性能。
-
render()
- 与挂载阶段相同,在组件更新时会再次调用,以确定组件的 UI。
-
getSnapshotBeforeUpdate(prevProps, prevState)
- 在 DOM 更新之前(在 render 之后)调用,返回的值会作为
componentDidUpdate的第三个参数。通常用于获取 DOM 的状态,比如滚动位置。
- 在 DOM 更新之前(在 render 之后)调用,返回的值会作为
-
componentDidUpdate(prevProps, prevState, snapshot)
- 在组件更新完成并且重新渲染到 DOM 之后调用。可以在这里执行一些操作,例如与 DOM 交互、发起网络请求等。
-
示例: 更新阶段
class UpdatingComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
static getDerivedStateFromProps(nextProps, prevState) {
return null;
}
shouldComponentUpdate(nextProps, nextState) {
return true; // 默认返回true,总是重新渲染
}
getSnapshotBeforeUpdate(prevProps, prevState) {
return null; // 返回null表示没有快照
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('Component has been updated');
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
卸载阶段(Unmounting)
组件从 DOM 中被移除时,会进入卸载阶段。
| 生命周期方法 | 调用时机 | 用途 |
|---|---|---|
componentWillUnmount() | 组件卸载前调用 | 清理定时器、取消订阅、释放资源 |
-
componentWillUnmount()
- 在组件卸载和销毁之前调用。这里可以执行清理操作,比如取消订阅、清除定时器、清除与 DOM 的直接交互等。
-
示例: 卸载阶段
class UnmountingComponent extends React.Component {
componentDidMount() {
this.timerID = setInterval(() => {
console.log('Tick');
}, 1000);
}
componentWillUnmount() {
clearInterval(this.timerID);
console.log('Component will be unmounted');
}
render() {
return <div>Check the console for ticks</div>;
}
}
错误处理阶段
当组件渲染过程中、生命周期方法中或子组件的构造函数中抛出错误时,React 会调用以下方法:
| 生命周期方法 | 调用时机 | 用途 |
|---|---|---|
static getDerivedStateFromError(error) | 子组件抛出错误时 | 返回一个 fallback UI 的 state |
componentDidCatch(error, info) | 错误被捕获后调用 | 记录错误信息 |
-
static getDerivedStateFromError(error)
- 捕获错误并在更新阶段调整状态,通常用于显示错误边界组件。
-
componentDidCatch(error, info)
- 捕获错误信息和组件堆栈,通常用于日志记录或显示备用 UI。
-
示例: 错误处理阶段
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 显示
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
function BuggyComponent() {
throw new Error("I crashed!");
return <h1>I am a buggy component</h1>;
}
function App() {
return (
<div>
<ErrorBoundary>
<BuggyComponent />
</ErrorBoundary>
</div>
);
}
React 16.3+ 废弃的生命周期
由于 React Fiber 架构的引入,以下方法被废弃:
componentWillMount→ 改用constructor或componentDidMountcomponentWillReceiveProps→ 改用static getDerivedStateFromPropscomponentWillUpdate→ 改用getSnapshotBeforeUpdate
函数组件的“生命周期”替代方案(Hooks)
函数组件没有生命周期方法,但可以用 Hooks 模拟类似行为:
| 类组件生命周期 | 函数组件替代方案 |
|---|---|
componentDidMount | useEffect(() => {}, []) |
componentDidUpdate | useEffect(() => {}, [deps]) |
componentWillUnmount | useEffect(() => { return () => { cleanup } }, []) |
shouldComponentUpdate | React.memo 或 useMemo |
getDerivedStateFromProps | 在 useState 或 useEffect 中处理 |
总结
React 的生命周期方法可以帮助开发者控制组件的行为和优化性能。在使用 React Hooks 的函数组件中,可以通过 useEffect 和其他相关 Hooks 实现类似的效果,代替类组件中的生命周期方法。理解和灵活运用这些方法是开发健壮和高效 React 应用的关键。
7. React 的 useEffect 生命周期
useEffect 是 React Hooks 中的一个重要 Hook,用于在函数组件中处理副作用。副作用包括数据获取、订阅事件、手动修改 DOM 等操作、置定时器。useEffect 提供了一种机制来执行这些副作用,并且可以控制它们的执行时机。
-
基本用法
-
副作用代码: 在
useEffect的回调函数中编写需要执行的副作用操作。 -
清理代码: 如果副作用涉及到订阅、定时器等需要清理的操作,可以在返回的函数中编写。
-
依赖数组:
useEffect的第二个参数是依赖数组,用于控制副作用的执行时机。只有当依赖数组中的值发生变化时,副作用才会重新执行。 -
示例: 基本用法
-
import React, { useEffect, useState } from 'react';
function ExampleComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// 副作用代码: 更新文档标题
document.title = `You clicked ${count} times`;
// 清理代码: 取消可能存在的副作用
return () => {
console.log('Cleanup effect');
};
}, [count]); // 依赖数组
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
- 依赖数组使用
- 不传依赖数组:副作用会在每次渲染后执行。
- 类似于类组件的 componentDidMount 和 componentDidUpdate 的组合。
- 注意: 这种方式容易导致无限循环,因为副作用可能会触发组件重新渲染,从而再次执行副作用函数。一般不推荐使用
- 空数组 []:副作用只在组件首次渲染挂载执行一次。
- 指定依赖 [dep1, dep2]:副作用在依赖数组中的值发生变化时重新执行。
- 不传依赖数组:副作用会在每次渲染后执行。
8. useEffect的依赖项可以是哪些
useEffect 是 React 中一个非常重要的 Hook,它允许你在函数组件中执行副作用(side effects),比如数据获取、订阅、手动更改 DOM 等。useEffect 的第二个参数是一个依赖项数组,它决定了副作用函数何时重新运行。
useEffect 依赖项的类型
useEffect 的依赖项可以是任何在组件渲染作用域内可访问的值,包括:
1. Props (组件的属性)
当父组件传递给子组件的 props 发生变化时,如果这些 props 是 useEffect 的依赖项,那么副作用函数就会重新运行。
function UserProfile({ userId }) {
const [userData, setUserData] = useState(null);
useEffect(() => {
// 当 userId 变化时,重新获取用户数据
fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(data => setUserData(data));
}, [userId]); // 依赖项是 userId
return (
<div>
{userData ? <p>User: {userData.name}</p> : <p>Loading...</p>}
</div>
);
}
2. State (组件的状态)
当组件内部的状态发生变化时,如果该状态是 useEffect 的依赖项,副作用函数会重新运行。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
// 当 count 变化时,更新文档标题
document.title = `Count: ${count}`;
}, [count]); // 依赖项是 count
return <button onClick={() => setCount(count + 1)}>Increment</button>;
}
3. 函数 (包括事件处理函数、自定义 Hook 返回的函数等)
如果你的 useEffect 依赖于一个函数(例如,一个事件处理函数),那么当这个函数本身在多次渲染之间发生变化时,useEffect 也会重新运行。
重要提示: 函数在 JavaScript 中是引用类型。如果一个函数在每次渲染时都被重新创建(例如,在组件内部定义的非 useCallback 函数),那么即使它的代码逻辑没变,它的引用也会改变,导致 useEffect 认为依赖项变化而重新运行。为了优化这种情况,通常会使用 useCallback 来记忆(memoize)函数。
import React, { useState, useEffect, useCallback } from 'react';
function MyComponent() {
const [value, setValue] = useState(0);
// 每次渲染都会创建一个新的函数引用,可能导致依赖它的 useEffect 不必要地重新运行
// const handleClick = () => {
// console.log('Clicked', value);
// };
// 使用 useCallback 记忆函数,只有当 value 改变时才重新创建 handleClick
const handleClick = useCallback(() => {
console.log('Clicked', value);
}, [value]); // 依赖 value
useEffect(() => {
// 当 handleClick 的引用改变时,会重新运行
console.log('useEffect for handleClick ran');
const button = document.getElementById('myButton');
button.addEventListener('click', handleClick);
return () => {
button.removeEventListener('click', handleClick);
};
}, [handleClick]); // 依赖项是 handleClick
return (
<button id="myButton" onClick={() => setValue(value + 1)}>
Click me ({value})
</button>
);
}
4. 变量 (在组件作用域内定义的常量或变量)
任何在组件内部定义并被 useEffect 使用的变量,如果它们的值在渲染之间发生变化,也应该被包含在依赖项数组中。
const API_URL = '/api/data'; // 这是一个在组件外部的常量,通常不需要作为依赖项
function DataFetcher() {
const [data, setData] = useState(null);
const limit = 10; // 在组件内部定义的变量
useEffect(() => {
// 当 limit 变化时,重新获取数据
fetch(`${API_URL}?limit=${limit}`)
.then(response => response.json())
.then(result => setData(result));
}, [limit]); // 依赖项是 limit
return (
<div>
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Loading data...</p>}
</div>
);
}
5. 其他 Hooks 返回的值: 例如,useRef 返回的 ref.current、useContext 返回的上下文值等。
import { useRef, useEffect } from 'react';
function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// 第一次渲染后聚焦输入框
inputRef.current.focus();
}, []); // 空数组,只在挂载时运行
return <input ref={inputRef} type="text" />;
}
依赖项的类型和行为
useEffect 对依赖项的比较是浅层比较(shallow comparison)。这意味着:
- 对于基本类型(字符串、数字、布尔值、
null、undefined),它会比较它们的值。 - 对于引用类型(对象、数组、函数),它会比较它们的引用地址。如果引用地址变了(即使内容可能相同),
useEffect也会认为依赖项发生了变化。
引用类型依赖的特殊处理
由于引用类型在每次渲染都会生成新的引用,直接使用会导致 effect 频繁运行:
-
对象依赖:
const user = { id: 1, name: 'John' }; // 错误:每次渲染都会触发 effect useEffect(() => { ... }, [user]); // 正确:使用具体属性 useEffect(() => { ... }, [user.id, user.name]); -
函数依赖:
// 错误:每次渲染都会生成新的函数 const fetchData = () => { ... }; useEffect(() => { ... }, [fetchData]); // 正确:使用 useCallback 记忆函数 const fetchData = useCallback(() => { ... }, []); useEffect(() => { ... }, [fetchData]);
几种特殊的依赖项情况:
- 空数组
[]:表示副作用函数只在组件首次挂载时执行一次,并在组件卸载时执行清理函数(如果返回了清理函数)。这等同于类组件的componentDidMount和componentWillUnmount的组合。 - 不传依赖项数组:表示副作用函数在每次渲染后都会执行。这很少是理想情况,因为很容易导致无限循环或性能问题。
- 包含对象或数组的依赖项:如果你将一个对象或数组作为依赖项,并且这个对象或数组在每次渲染时都被重新创建(即使其内部值没有改变),
useEffect会因为它引用地址的变化而重新运行。为了避免不必要的重新运行,可以考虑使用useMemo来记忆这些对象或数组,或者重构代码避免将复杂对象作为依赖项。
常见问题解决方案
-
无限循环问题:
// 错误:会导致无限循环 const [data, setData] = useState(null); useEffect(() => { fetchData().then(setData); }, [data]); // data 变化会触发 effect,effect 又会修改 data // 正确:移除不必要的依赖 useEffect(() => { fetchData().then(setData); }, []); // 只在挂载时运行 -
依赖函数的问题:
function MyComponent({ fetchData }) { // 错误:如果 fetchData 是 prop,每次父组件渲染都会变化 useEffect(() => { fetchData(); }, [fetchData]); // 解决方案1:父组件用 useCallback 记忆函数 // 解决方案2:将函数移到 effect 内部 useEffect(() => { function fetchData() { ... } fetchData(); }, [/* 其他必要依赖 */]); }
正确管理 useEffect 的依赖项是避免不必要渲染、防止内存泄漏和编写高效 React 应用的关键。
9. 如何获取最新的 state 值?
函数式更新:setState
可以接受一个函数作为参数,这个函数会接收到前一个 state 作为参数,确保你拿到的是最新的 state。
- 总是基于最新的状态值计算
- 可以正确处理连续的多个更新
const [count, setCount] = useState(0);
// 使用函数式更新获取最新值
const increment = () => {
setCount(prevCount => {
const newCount = prevCount + 1;
console.log(newCount); // 这里可以获取最新值
return newCount;
});
};
const [count, setCount] = useState(0);
// 批量更新
const handleClick = () => {
setCount(count + 1);
setCount(count + 1); // 只会增加1
};
// 使用函数式更新解决
const handleClick = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1); // 会增加2
};
优点:
- 直接接收前一个 state 值作为参数
- 确保获取的是最新的 state
- 适用于连续的 state 更新
Q: 函数式更新会导致组件立即渲染吗?会绕过批量更新吗
A: 不会立即渲染,也不会绕过批量更新。函数式更新仍然会参与 React 的批量更新机制:
- 仍然批量处理:多个函数式更新会被批量处理,最终只触发一次渲染
- 不立即执行:更新会被加入队列,等待 React 调度执行
- 计算顺序保证:函数式更新会按顺序执行计算,但渲染仍然是批量的
useEffect监听state变化 (在函数组件中) :
const [count, setCount] = useState(0);
useEffect(() => {
console.log('最新的 count:', count);
}, [count]); // 只有当 count 变化时才触发
适用场景:
- 需要在 state 变化后执行某些操作
- 需要响应 state 的变化
使用 useRef 保存最新值
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // 每次 count 变化时更新 ref
}, [count]);
const logLatestCount = () => {
console.log(countRef.current); // 获取最新值
};
优点:
- 可以在任何地方获取最新值
- 不会触发组件重新渲染
10. React 中的 state 状态 和 props 属性有什么区别?
它们在数据管理和组件通信中扮演着不同角色
- state: 组件内部的状态,由组件自己管理,可变。
- props: 组件的属性,父组件传递给子组件的数据,不可变。
| 特性 | state | props |
|---|---|---|
| 所有权 | 组件内部私有 | 组件属性,从父组件接收(只读) |
| 可变性 | 组件内部可修改(通过 setState) | 不可直接修改(只读属性) |
| 数据流向 | 组件内部管理 | 父组件 → 子组件单向流动 |
理解 state 和 props 的区别是掌握 React 的关键:
- state = 组件的"内存"(可变私有数据)
- props = 组件的"配置参数"(只读外部输入)
11. setState 是同步还是异步的,setState 做了什么
React 18 以前:
-
同步异步取决于具体的调用场景。
- 在 React 的控制范围内(如事件处理函数、生命周期函数等),
setState是异步的。 - 在 React 的控制范围之外(如
Promise、setTimeout/**setInterval**、原生事件监听器等),setState是同步的。
- 在 React 的控制范围内(如事件处理函数、生命周期函数等),
React 18 及以后:
- 使用了
createRootAPI 后,所有setState都是异步批量执行的。 - 实现机制类似于 Vue 的
$ nextTick和浏览器的事件循环机制。 - 每个
setState都会被 React 加入到任务队列,多次对同一个 state 使用setState只会返回最后一次的结果,因为它不是立刻就更新,而是先放在队列中,等时机成熟再执行批量更新。
setState 的具体执行流程
- 将更新加入队列:把状态更新对象放入组件的更新队列
- 检查批量更新模式:判断是否处于批量更新(batching)阶段
- 计算新状态:合并多个更新(Object.assign方式)
- 触发重新渲染:安排一次组件的更新(异步调度)
- 执行渲染:React 决定何时真正应用这些更新
12. 为什么有时react两次(或者10次,道理一样)setState,只执行一次
- 点题收敛
在 React 中,当多次调用 setState 方法时,并不一定会立即触发组件的重新渲染,而是可能会将多次更新合并成一次更新,以提高性能。
- 详细回答
这种合并更新的机制被称为“批量更新”(batching)。React 会将多次更新放入一个更新队列中,等待后续的批量更新。在批量更新期间,React 会尽可能地合并更新,以减少重复渲染的次数。
具体来说,当使用 setState 方法进行状态更新时,React 会将更新放入更新队列中,并不会立即触发组件的重新渲染。如果在同一个事件处理函数、生命周期方法或 useEffect 回调中多次调用 setState,这些更新会被合并成一次更新,并在下一个批量更新期间一起执行。
- 总结收敛
总之,当多次调用 setState 方法时,React 可能会将多次更新合并成一次更新,以提高性能。但是,如果在异步操作中调用 setState 方法,这些更新就不会被合并,而是会立即触发组件的重新渲染。
13. useState要使用数组 而不使用对象
如果useState返回的是数组,那么使用者可以对数组中的元素命名,代码看起来也比较干净
如果useState返回的是对象,在解构对象的时候必须要和useState内部实现返回的对象同名,想要使用多次的话,必须得设置别名才能使用返回值
总结:useState返回的是array而不是object的原因就是为了降低使用的复杂度,返回数组的话可以直接根据顺序解构,而返回对象的话要想使用多次就需要定义别名了
14. react 类组件 render函数中可以通过this.setState 更改状态吗
在 React 的类组件中,不建议在 render 方法中使用 this.setState 来更改状态。这是因为 this.setState 会触发组件的重新渲染,而 render 方法本身就是在每次渲染时调用的。这样做会导致无限循环渲染,进而导致性能问题或程序崩溃。
原因解析
render 函数的设计初衷: render 函数的主要作用是根据组件的当前状态和 props,生成一个虚拟 DOM。它是一个纯函数,应该只负责展示,而不应该修改状态。
setState() 的触发机制: 当调用 setState() 时,React 会将新的状态添加到一个待处理队列中,然后在下一个渲染周期进行批量更新。这个过程是异步的,并且会触发组件的重新渲染。
无限循环的风险: 如果在 render 函数中调用 setState(),就会导致一个无限循环:修改状态 -> 触发重新渲染 -> 再次修改状态 -> 再次触发重新渲染,如此反复。
正确用法
如果你需要在某个条件下改变状态,可以使用其他生命周期方法,例如 componentDidMount 或 componentDidUpdate,或者在事件处理函数中调用 this.setState
在 React 中,useEffect 的依赖项数组(也称为依赖数组)用于控制 useEffect Hook 的执行时机。依赖项数组中的值发生变化时,useEffect 会重新运行。因此,正确地设置依赖项数组对于避免不必要的渲染和副作用至关重要。
15. react有哪几种方式改变state,或者是管理状态
- 类组件中通过 this.setState 更新状态。
- 函数组件中使用 useState 和 useReducer 管理状态。
- 在更复杂的场景下,useReducer 提供了一个更灵活的状态管理方案。
const [state, dispatch] = useReducer(reducer, { count: 0 });
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
default:
return state;
}
}
// 改变state
dispatch({ type: 'increment' });
- useContext 可以用于跨组件共享和更新状态。
const CountContext = createContext();
function App() {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={{ count, setCount }}>
<ConsumerComponent />
</CountContext.Provider>
);
}
function ConsumerComponent() {
const { count, setCount } = useContext(CountContext);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
- forceUpdate 仅作为最后的手段使用,一般不推荐。
- 状态管理库 redux
总结
- 组件内部状态管理:使用useState、useReducer
- 全局状态管理:使用Context API、Redux、MobX等
- 对于中小型应用,可以考虑使用React Context API;对于大型应用,Redux仍然是常用的状态管理工具。
16. react有哪几种创建组件方法
React 提供了多种创建组件的方式,随着 React 的发展,这些方法也在不断演进。以下是 React 中创建组件的主要方法:
1. 类组件 (Class Components)
适合需要使用生命周期方法或更复杂状态管理的场景。
这是 React 早期主要的组件创建方式,使用 ES6 的 class 语法:
import React, { Component } from 'react';
class ClassComponent extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>Increment</button>
</div>
);
}
}
特点:
- 可以使用 state 和生命周期方法
- 需要继承
React.Component - 必须实现
render()方法 - 适合复杂的有状态组件
2. 函数组件 (Function Components)
使用普通 JavaScript 函数创建组件,最初只能作为无状态组件使用,现在通过 Hooks 也能管理状态:
function FunctionComponent(props) {
return <div>Hello, {props.name}!</div>;
}
Hooks 增强后的函数组件:
import { useState } from 'react';
function HookComponent() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
特点:
- 代码更简洁
- 使用 Hooks 可以管理状态和副作用
- React 16.8+ 推荐的主要组件形式
- 性能通常优于类组件
3. 箭头函数组件 (Arrow Function Components)
使用箭头函数语法创建的函数组件:
const ArrowComponent = (props) => {
return <div>Hello, {props.name}!</div>;
};
// 更简洁的写法(隐式返回)
const ShortArrowComponent = props => <div>Hello, {props.name}!</div>;
特点:
- 自动绑定
this(不需要手动绑定) - 语法更简洁
- 适合简单的展示组件
4. 高阶组件 (Higher-Order Components, HOC)
用于增强组件功能和实现代码复用的模式
高阶组件是一个函数,它接收一个组件并返回一个新的增强组件:
function withLogger(WrappedComponent) {
return class extends React.Component {
componentDidMount() {
console.log('Component is mounted');
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
// 使用高阶组件
const EnhancedComponent = withLogger(MyComponent);
特点:
- 用于组件逻辑复用
- 不修改原组件,而是组合新组件
- 常见于第三方库(如 React-Redux 的
connect)
5. Render Props 组件
通过 prop 接收一个返回 React 元素的函数来实现组件复用:
class MouseTracker extends React.Component {
state = { x: 0, y: 0 };
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
});
};
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
);
}
}
// 使用
<MouseTracker render={({ x, y }) => (
<h1>The mouse position is ({x}, {y})</h1>
)} />
特点:
- 另一种组件复用模式
- 动态决定渲染内容
- 适用于需要共享状态的场景
6. 纯组件 (PureComponent)
React.PureComponent 是 React.Component 的优化版本,自动实现了 shouldComponentUpdate 的浅比较:
class PureComp extends React.PureComponent {
render() {
return <div>{this.props.value}</div>;
}
}
特点:
- 自动进行 props 和 state 的浅比较
- 避免不必要的渲染
- 只适用于简单的 props 和 state
7. React.memo (函数组件的 PureComponent)
函数组件版本的性能优化方式,类似于 PureComponent:
const MemoizedComponent = React.memo(function MyComponent(props) {
return <div>{props.value}</div>;
});
特点:
- 记忆组件渲染结果
- 只在 props 变化时重新渲染
- 可以自定义比较函数
17. Hooks解决了什么问题,react函数组件和class组件区别
1、Hooks 解决了什么问题
React Hooks 是 React 16.8 引入的革命性特性,主要解决了以下问题:
1. 状态逻辑复用困难
- 类组件问题:需要使用高阶组件(HOC)或render props等复杂模式实现逻辑复用
- Hooks解决方案:通过自定义Hook轻松提取和复用状态逻辑
// 自定义Hook复用逻辑
function useCounter(initialValue) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
return { count, increment };
}
// 多个组件复用同一逻辑
function ComponentA() {
const { count, increment } = useCounter(0);
// ...
}
function ComponentB() {
const { count, increment } = useCounter(10);
// ...
}
2. 复杂组件难以理解
- 类组件问题:生命周期方法中混杂不相关逻辑(如数据获取+事件监听)
- Hooks解决方案:按功能组织代码,相关逻辑集中在一起
// 类组件中分散的逻辑
class Example extends Component {
componentDidMount() {
// 数据获取
// 事件监听
}
componentWillUnmount() {
// 清除事件
}
}
// 函数组件中使用Hooks
function Example() {
// 数据获取逻辑集中
useEffect(() => { /* 数据获取 */ }, []);
// 事件监听逻辑集中
useEffect(() => {
const handler = () => {};
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
}
3. 性能优化更精细
- 类组件问题:
shouldComponentUpdate优化粒度较粗 - Hooks解决方案:
useMemo和useCallback提供更细粒度的优化
function ExpensiveComponent({ a, b }) {
const result = useMemo(() => computeExpensiveValue(a, b), [a, b]);
return <div>{result}</div>;
}
4. 类组件学习成本高
- 类组件问题:需要理解类、生命周期、
this工作机制、绑定事件处理器、等 - Hooks解决方案:使用更简单的函数和闭包概念
// 类组件中的this问题
class Button extends Component {
constructor() {
super();
this.handleClick = this.handleClick.bind(this); // 需要绑定
}
handleClick() {
console.log(this); // 需要确保this正确
}
}
// 函数组件无this问题
function Button() {
const handleClick = () => {
console.log('无需担心this');
};
}
2、函数组件与类组件的核心区别
| 特性 | 函数组件 | 类组件 |
|---|---|---|
| 状态管理 | 使用useState等Hooks | 使用this.state和this.setState |
| 逻辑复用 | 自定义Hook | HOC或Render Props |
| this绑定 | 无this绑定问题 | 需要处理this绑定 |
| 生命周期 | 使用useEffect模拟生命周期 | 直接使用生命周期方法 |
| 代码组织 | 按功能组织代码 | 按生命周期方法组织代码 |
| 性能优化 | 使用React.memo+useMemo等 | 使用PureComponent或shouldComponentUpdate |
| 学习曲线 | 更简单直观 | 需要理解类、this、生命周期等概念 |
| 未来兼容性 | React团队推荐,未来主流 | 仍支持但不再新增特性 |
18. react中的refs的作用
它能帮助引用一个不需要渲染的值
refs 是 React 提供的一种机制,允许我们在渲染输出的DOM元素或组件实例上附加一个引用。通过这个引用,我们可以直接操作DOM元素或者获取组件实例,从而实现一些在 React 的数据流中难以完成的功能。
refs 的主要用途
1. 获取DOM元素
- 直接操作DOM: 在某些特殊情况下,我们需要直接操作DOM元素,比如设置焦点、触发动画、集成第三方DOM库等。
- 测量DOM: 可以获取DOM元素的尺寸、位置等信息。
2. 获取组件实例
- 调用组件的方法: 如果一个组件暴露了一些方法,可以通过ref来调用这些方法。
- 强制更新组件: 在某些特殊情况下,可能需要强制更新一个子组件。
- 基础方法:
useImperativeHandle+forwardRef
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
// 子组件
const ChildComponent = forwardRef((props, ref) => {
const inputRef = useRef();
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
getValue: () => {
return inputRef.current.value;
}
}));
return <input ref={inputRef} />;
});
// 父组件
function ParentComponent() {
const childRef = useRef();
const handleClick = () => {
childRef.current.focus(); // 调用子组件暴露的方法
console.log(childRef.current.getValue()); // 获取子组件值
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleClick}>操作子组件</button>
</div>
);
}
3. 存储可变值
Refs 可以存储任何可变值,且更新不会触发重新渲染:
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
// 停止计时器的函数可以访问intervalRef
}
使用方式
- 创建ref:
- 函数式组件: 使用 useRef Hook 创建ref。
- 类组件: 使用 React.createRef 创建ref。
- 绑定ref: 将创建的ref绑定到DOM元素或组件上。
- 访问ref: 通过 ref.current 访问ref所指向的DOM元素或组件实例。
19. 受控组件和非受控组件
什么是 React 中的受控和非受控组件?
在 React 中,受控组件和非受控组件是两种不同的方式来管理表单输入值(两种不同的处理表单输入的方式)
- 受控组件是那些使用组件状态state来管理表单数据的组件。表单元素的值由 React 的 state 驱动,并通过事件回调更新。
- 这意味着组件会跟踪输入值的变化,并相应地更新其状态
- 要创建受控组件,您需要使用以下步骤:
-
- 在组件的状态中定义一个属性来存储输入值。
-
- 使用 onChange 事件处理程序来更新组件的状态,当输入值发生变化时。
-
- 使用组件的状态值来设置输入的 value 属性。
这种方式使得表单数据同步到 React 组件的状态中,从而使得表单数据和 React 状态保持一致
- 使用组件的状态值来设置输入的 value 属性。
-
- 非受控组件是那些使用DOM自身来管理表单数据的组件,React 不控制表单元素的值,而是通过 ref 在需要时获取 DOM 元素的值。
- 这意味着组件不会跟踪输入值的变化,也不会更新其状态。要创建非受控组件,您需要使用以下步骤:
-
- 在输入元素上设置 ref 属性。
-
- 使用 ref 属性来访问输入元素。
-
- 从输入元素中获取值
-
- 这意味着组件不会跟踪输入值的变化,也不会更新其状态。要创建非受控组件,您需要使用以下步骤:
优缺点 受控组件与非受控组件的比较
受控组件和非受控组件各有优缺点。
- 受控组件的优点:
- 可以对输入值进行即时验证和格式化。
- 状态和视图保持一致,便于调试和理解。
- 便于实现复杂的表单逻辑和动态表单
- 受控组件的缺点:
- 对于非常大的表单,管理所有输入状态可能会变得繁琐。
- 需要更多的代码来管理状态和事件处理。
- 每次输入都会触发重新渲染
- 非受控组件的优点:
- 代码更简洁,不需要管理状态和事件处理。
- 适用于简单的表单和不需要频繁更新的输入值。
- 非受控组件的缺点:
- 组件无法完全控制输入值。
- 不容易对输入值进行即时验证和格式化。
- 与 React 的单向数据流理念不一致,可能导致状态和视图不一致的问题。
应用场景
一般来说,建议在大多数情况下使用受控组件。 这是因为受控组件提供了对输入值的更多控制,并且更容易验证和处理输入值的更改。但是,如果您的组件需要非常简单,或者您不需要完全控制输入值,那么可以使用非受控组件。
以下是一些建议,说明何时使用受控组件和非受控组件:
- 使用受控组件:
- 需要对输入值进行即时验证或格式化。
- 当然是你需要对输入的值做处理之后设置到表单的时候,或者是你想实时同步状态值到父组件。
- 表单较复杂,包含动态字段或复杂逻辑。
- 需要在表单状态和视图之间保持一致性。
- 需要对输入值进行即时验证或格式化。
- 使用非受控组件:
- 表单较简单,不需要频繁更新输入值。
- 想要更接近于传统的 HTML 表单处理方式。
- 只需要在表单提交时获取输入值。
核心对比
| 特性 | 受控组件 | 非受控组件 |
|---|---|---|
| 数据控制 | React 完全控制 | DOM 控制 |
| 值获取方式 | 从 state 获取 | 通过 ref 从 DOM 获取 |
| 值更新方式 | onChange + setState | DOM 自动更新 |
| 初始值设置 | 通过 value 属性 | 通过 defaultValue/defaultChecked |
| 表单验证 | 实时验证 | 提交时验证 |
| 代码量 | 较多 | 较少 |
| 性能 | 每次输入都触发渲染 | 不触发额外渲染 |
| 推荐度 | ✅ React 官方推荐 | ⚠️ 特定场景使用 |
20. 有状态组件和无状态组件
把获取和管理数据的逻辑放在父组件,也就是有状态组件;把渲染界面的逻辑放在子组件,也就是无状态组件
1、基本概念
1. 有状态组件 (Stateful Components)
有状态组件是指内部维护自身状态的组件,能够管理、存储和改变数据。它们通常使用 state 或 useState 等 Hook 来管理组件内部状态。
2. 无状态组件 (Stateless Components)
无状态组件是指不维护内部状态的组件,所有数据都通过 props 接收,行为完全由传入的 props 决定。它们通常只是接收数据并渲染 UI。
2、核心区别
| 特性 | 有状态组件 | 无状态组件 |
|---|---|---|
| 状态管理 | 有内部状态(state) | 无内部状态 |
| 数据来源 | 可以同时使用props和state | 仅通过props接收数据 |
| 生命周期 | 可以使用生命周期方法/Hook | 无生命周期 |
| 复杂性 | 相对复杂 | 简单纯粹 |
| 复用性 | 相对较低 | 高度可复用 |
| 测试难度 | 较难测试 | 易于测试 |
| 组件类型 | 类组件或使用Hooks的函数组件 | 通常为函数组件 |
| 性能 | 可能影响性能 | 性能更优 |
21. 组件传值
- 父组件向子组件通讯:父组件可以向子组件通过传props的方式,向子组件进行通讯
- 子组件向父组件通讯:props+回调的方式,父组件向子组件传递props进行通讯,此props为作用域为父组件自身的函数,子组件调用该函数,将子组件想要传递的信息,作为参数,传递到父组件的作用域中
- 兄弟组件通信:找到这两个兄弟节点共同的父节点,结合上面两种方式由父节点转发信息进行通信
- 跨层级通信:Context设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言,对于跨越多层的全局数据通过Context通信再适合不过
- 发布订阅模式:发布者发布事件,订阅者监听事件并做出反应,我们可以通过引入event模块进行通信
- 全局状态管理工具:借助Redux或者Mobx等全局状态管理工具进行通信,这种工具会维护一个全局状态中心Store,并根据不同的事件产生新的状态
- URL 参数 / query string / location
22. 什么是React Hooks?常用的Hooks有哪些?
React Hooks 是 React 16.8 引入的革命性特性,它允许你在函数组件中使用 state 和其他 React 特性,而无需编写类组件。Hooks 解决了类组件的多个痛点,提供了更简洁、更灵活的代码组织方式。
Hooks 的核心价值
- 简化组件逻辑:将相关代码聚合在一起,而非分散在不同生命周期中
- 复用状态逻辑:通过自定义 Hook 实现逻辑复用,避免高阶组件嵌套
- 函数式编程:减少类组件的
this绑定问题,代码更简洁 - 渐进采用:可以在不重写现有组件的情况下逐步采用
常用的Hooks包括
- useState:用于声明状态变量
- useEffect:用于处理副作用(如数据获取、订阅等)
- useCallback:缓存回调函数
- useMemo:缓存计算结果
- useRef:用于创建 ref,获取 DOM 元素或保存 mutable 值
- useContext:全局状态管理 createContext
- useReducer:用于复杂状态逻辑的管理 useState替代方案
- useImperativeHandle • 作用:用于在使用 React.forwardRef 时自定义对 ref 的暴露值,避免直接暴露 DOM 元素。 • 使用:useImperativeHandle 接受 ref 和一个创建暴露值的函数,以及依赖项数组。
Hooks 使用规则
-
只在顶层调用:不要在循环、条件或嵌套函数中调用 Hook
-
只在React函数中调用:
- React 函数组件
- 自定义 Hook
Hooks 优势总结
- 代码更简洁:消除类组件的样板代码
- 逻辑更聚合:相关代码组织在一起
- 复用性更强:通过自定义 Hook 共享逻辑
- 学习曲线更低:减少
this和生命周期概念的负担
23. 如何实现自定义Hooks?用途有哪些
自定义 Hooks 是 React 中复用状态逻辑的强大工具,它让你能将组件逻辑提取到可重用的函数中。以下是实现自定义 Hooks 的完整指南:
提取复用的状态逻辑到函数中,返回需要的值和函数
1、自定义 Hook 的基本概念
1. 什么是自定义 Hook?
- 一个以
use开头的 JavaScript 函数 - 可以调用其他 Hook(如
useState,useEffect) - 不包含 UI 渲染逻辑,只包含状态逻辑
2. 命名规范
- 必须以
use开头(如useToggle,useFetch) - 这样 React 才能自动检查 Hook 规则是否被遵守
2、创建自定义 Hook 的步骤
1. 识别可复用的逻辑
观察组件中哪些逻辑可以被多个组件共享,例如:
- 数据获取
- 表单处理
- 定时器管理
- 浏览器API封装(如 localStorage)
2. 提取逻辑到独立函数
将共享逻辑移动到一个新函数中,并确保它遵循 Hook 规则。
3. 返回需要的值和函数
自定义 Hook 可以返回:
- 状态值
- 状态更新函数
- 任何其他需要暴露给使用组件的值
3、实用自定义 Hook 示例
1. useToggle - 切换布尔状态
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
return [value, toggle];
}
// 使用示例
function Component() {
const [isOn, toggleIsOn] = useToggle(false);
return (
<button onClick={toggleIsOn}>
{isOn ? 'ON' : 'OFF'}
</button>
);
}
2. useFetch - 数据获取
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// 使用示例
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
24. React在哪里捕获错误
在 React 应用中,错误可以通过多种方式捕获,具体取决于错误的类型和发生的场景。以下是 React 中捕获错误的主要方式及其适用场景:
1. 错误边界(Error Boundaries)
错误边界是 React 提供的类组件机制,用于捕获子组件树中的 JavaScript 错误,并显示备用 UI,而不是让整个应用崩溃。
捕获范围:
- 渲染期间(
render方法) - 生命周期方法(如
componentDidMount、componentDidUpdate) - 构造函数(
constructor)
无法捕获的情况:
- 事件处理(如
onClick、onChange) - 异步代码(如
setTimeout、fetch) - 服务端渲染(SSR)
- 错误边界自身抛出的错误
实现方式:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 可以在这里记录错误
console.error("Error caught by Error Boundary:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// 使用方式
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
适用场景:
- 防止组件树崩溃,提供优雅降级 UI
- 记录错误日志(如 Sentry、LogRocket)37
2. try/catch 捕获事件和异步错误
由于错误边界无法捕获事件处理和异步代码中的错误,因此需要使用传统的 try/catch 处理。
适用场景:
- 事件处理(如按钮点击、表单提交)
- 异步操作(如
fetch、Promise、setTimeout)
示例:
function Button() {
const handleClick = async () => {
try {
await fetchData();
} catch (error) {
console.error("Fetch error:", error);
alert("请求失败,请重试!");
}
};
return <button onClick={handleClick}>提交</button>;
}
适用场景:
- 防止 UI 因未捕获的异步错误而中断
- 提供用户友好的错误提示
3. 全局错误捕获(window.onerror)
用于捕获未被任何 try/catch 或错误边界处理的 JavaScript 错误。
适用场景:
- 全局未捕获的异常(如第三方库错误)
- 监控前端错误(如 Sentry、Bugsnag)
示例:
// 全局错误监听
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
// 上报错误
});
// Promise 未捕获的异常
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled rejection:', event.reason);
});
适用场景:
- 监控生产环境的未处理错误
- 防止白屏影响用户体验810
总结:React 错误捕获方式对比
| 方式 | 适用场景 | 示例 | 限制 |
|---|---|---|---|
| 错误边界 | 组件渲染/生命周期错误 | <ErrorBoundary> | 不适用于事件/异步代码 |
try/catch | 事件处理、异步代码 | try { fetch() } catch {} | 仅限命令式代码 |
| 全局错误监听 | 未捕获的全局错误 | window.onerror | 无法阻止 React 卸载组件 |
react-error-boundary | 函数组件、更灵活的错误处理 | FallbackComponent | 需要额外安装库 |
25. Context API 是什么?有什么用?
Context API 提供了一种在组件中共享数据的方式,而无需显式地通过 props 层层传递。
Context API 常用于全局状态管理,比如主题切换、语言切换等。
context做的事情就是创建一个上下文对象,并且对外暴露提供者和消费者,在上下文之内的所有子组件,都可以访问这个上下文环境之内的数据,并且不用通过props。简单来说,context的作用就是对它所包含的组件树提供全局共享数据的一种技术
用法
import{createContext}from'react';
**创建 Context**:
export const ThemeContext= createContext('light');
//**Provider 组件**:
functionApp(){
const[theme,setTheme]= useState('light');
// ……
return(
<ThemeContext.Provider value={theme}>
<Page/>
</ThemeContext.Provider>
);
}
//**useContext Hook**
functionButton(){
// ✅ 推荐方式
consttheme= useContext(ThemeContext);
return<buttonclassName={theme}/>;
}
工作原理
-
上下文传递机制:
- React 维护一个上下文栈
- 每个 Provider 将其值推入栈中
- 子组件消费时从栈顶开始查找匹配的上下文
-
更新机制:
- 当 Provider 的 value 发生变化时,所有消费该 context 的组件都会重新渲染
- 使用 Object.is 比较新旧值,所以避免直接修改对象属性
-
性能优化:
- 避免在 Provider 的 value 中传递频繁变化的对象
- 可以将 Context 拆分以避免不必要的更新
最佳实践
-
合理拆分 Context:
- 避免单个 Context 包含过多不相关数据
- 按业务领域或功能拆分 Context
-
性能优化技巧:
- 对不变的上下文值使用 memoization
const value = useMemo(() => ({ user, login, logout }), [user]); -
高阶组件模式:
function withUser(Component) { return function WrappedComponent(props) { const { user } = useContext(UserContext); return <Component {...props} user={user} />; } } -
与 Redux 对比:
- Context 适合低频更新的全局数据(如主题、用户信息)
- Redux 更适合高频更新的复杂状态管理
26. React中的高阶组件(HOC)是什么?
高阶组件是一个函数,接受一个组件并返回一个新的组件,常用于组件逻辑复用,如权限控制、数据获取等。
用途:HOC 用于将组件的逻辑提取到可复用的单元中,避免代码重复。常见的使用场景包括:权限控制、日志记录、处理全局数据(如 Redux 状态)、注入额外的 props、处理生命周期逻辑等。
function withLoading(WrappedComponent) {
return function({ isLoading, ...props }) {
return isLoading
? <div>Loading...</div>
: <WrappedComponent {...props} />;
}
}
const EnhancedComponent = withLoading(MyComponent);
常见用途
-
属性代理 (Props Proxy) :
- 操作 props
- 抽象 state
- 包装组件样式
function withExtraProps(WrappedComponent) { return function(props) { return <WrappedComponent {...props} extraProp="value" />; } } -
继承反转 (Inheritance Inversion) :
- 渲染劫持
- 操作 state
function withLogging(WrappedComponent) { return class extends WrappedComponent { render() { console.log('Rendering:', WrappedComponent.name); return super.render(); } } }
27. 如何在React中实现组件懒加载?
React 提供了几种方法来实现组件的懒加载(按需加载),这可以显著提高应用的初始加载性能。以下是主要的实现方式:
1. 使用 React.lazy 和 Suspense (React 16.6+)
React.lazy 是 React 16.6 引入的一个功能,它允许你通过动态 import() 语法来懒加载组件。
这是 React 官方推荐的懒加载方式:
import React, { Suspense } from 'react';
// 使用 React.lazy 动态导入组件
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function MyComponent() {
return (
<div>
{/* Suspense 提供加载中的回退内容 */}
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
代码解释
React.lazy(() => import('./LazyComponent')):
React.lazy 接受一个函数,该函数通过 import() 动态导入要懒加载的组件。这个函数返回一个 Promise,组件会在需要时才加载。Suspense组件:
Suspense 是一个用于包裹懒加载组件的组件,它接受一个 fallback 属性,表示在懒加载的组件尚未加载完成时显示的内容。这里的 fallback 可以是一个加载指示器(如加载动画或简单的文字 "Loading...")。 一旦懒加载的组件加载完成,React 会自动渲染它并替换 fallback 内容。
特点:
- 需要配合
Suspense使用 - 仅支持默认导出(default export)的组件
- 适用于 React 16.6 及以上版本
- 内置错误边界处理更佳
2. 动态 import() + 自定义加载状态
如果不使用 Suspense,可以这样实现:
import React, { useState, useEffect } from 'react';
function LazyLoader() {
const [Component, setComponent] = useState(null);
useEffect(() => {
import('./LazyComponent').then(module => {
setComponent(module.default); // 获取默认导出
});
}, []);
return Component ? <Component /> : <div>Loading...</div>;
}
3. 路由懒加载 (React Router)
结合 React Router 实现路由级别的懒加载:
import { BrowserRouter as Router, Route, Switch } 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 (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
);
}
4. 高阶组件实现懒加载
可以创建一个通用的懒加载高阶组件:
function lazyLoad(importComponent) {
return function(props) {
const [Component, setComponent] = useState(null);
useEffect(() => {
importComponent().then(module => {
setComponent(module.default);
});
}, []);
return Component ? <Component {...props} /> : <div>Loading...</div>;
};
}
// 使用方式
const LazyComponent = lazyLoad(() => import('./LazyComponent'));
最佳实践
-
预加载策略:
// 鼠标悬停时预加载 <div onMouseEnter={() => import('./LazyComponent')}> <LazyComponent /> </div> -
命名导出组件的懒加载:
const LazyComponent = React.lazy(() => import('./LazyComponent').then(module => ({ default: module.NamedExport })) ); -
错误边界处理:
class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } render() { if (this.state.hasError) { return <h1>加载组件失败</h1>; } return this.props.children; } } // 使用方式 <ErrorBoundary> <Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </Suspense> </ErrorBoundary> -
Webpack 魔法注释 (用于代码分割和预取):
const LazyComponent = React.lazy(() => import(/* webpackChunkName: "lazy-component" */ './LazyComponent') );
React.lazy 基于 import() 语法,而 import() 会自动触发 Webpack 的代码分割功能,生成单独的 JavaScript 文件。这些文件仅在需要时才会被加载,因此可以显著优化应用的性能。
懒加载是优化 React 应用性能的重要手段,合理使用可以显著减少初始包大小,提高页面加载速度。
28. React性能优化的常见方法有哪些?
React 性能优化是一个重要的话题,可以通过多种方式实现:
-
**使用
React.memo(针对函数组件) ** :- 它们会进行浅层比较 (shallow comparison) props 和 state,如果它们没有发生变化,则阻止组件重新渲染。
-
使用
useMemo和useCallbackHooks:useMemo缓存计算结果,避免重复计算。useCallback缓存函数,避免子组件不必要的重新渲染(当子组件使用了React.memo时特别有用)。
-
列表渲染时使用
key:key帮助 React 识别列表中哪些项发生了变化、被添加或被移除,从而高效地更新列表。key应该是唯一的且稳定的。
-
条件渲染和列表虚拟化:
- 条件渲染:只渲染用户当前可见的组件。
- 列表虚拟化 (Windowing) :对于长列表,只渲染当前视口内可见的列表项,大大减少 DOM 元素的数量,提高性能。
-
懒加载 (Lazy Loading) 和代码分割 (Code Splitting) :
- 使用
React.lazy和Suspense按需加载组件或模块,减少初始加载的代码量。
- 使用
-
避免在
render方法或函数组件顶层创建新对象或函数:- 这会导致每次渲染都创建新的引用,可能导致子组件即使 props 没有实际变化也重新渲染。
-
优化图片:使用适当的图片格式、压缩图片、使用图片 CDN。
-
生产环境部署:使用生产模式构建应用,它会移除开发模式特有的代码和优化。
-
性能分析工具:使用 React DevTools 中的 Profiler 来识别性能瓶颈。
29. 虚拟DOM工作原理
概念
Virtual DOM 其实就是一棵以 JavaScript 对象(VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象映射
用途
- 提供与真实节点(Node)对应的虚拟节点(vNode)
- 将新的虚拟节点与旧的虚拟节点进行对比,找出差异,然后更新视图
工作原理
核心目标:最小化DOM操作
- React 组件会返回一个虚拟 DOM 树,该树是一个普通的 JavaScript 对象,描述了界面的结构。
- 每当组件的状态或属性变化时,React 会重新生成一个新的虚拟 DOM 树。
- React 会将新的虚拟 DOM 树与旧的进行比较(diffing),找出差异。
- 根据 diffing 结果,React 以最小的代价更新实际的 DOM,从而提高性能。
优点
-
声明式编程:
- 开发者只需关心状态,不用手动操作DOM
- 自动处理UI与状态的同步
-
开发效率:
- 避免直接DOM操作带来的各种问题
- 组件化开发模式更符合现代前端工程化
-
性能提升:
- 批量DOM更新,减少重绘回流
- 最小化DOM操作,仅更新必要的部分
-
跨平台兼容能力:
- 虚拟DOM抽象使React Native成为可能
- 可以渲染到不同目标(如PDF、Canvas)
30. React 的 diff 算法原理?
1 背景
- 每次组件更新时,React 会构建一棵新的 Virtual DOM 树(虚拟 DOM)。
- 为了找出新旧 DOM 的差异,React 使用 Diff 算法(也叫 Reconciliation 协调过程)。
- 然而,完整比较两棵 DOM 树的时间复杂度是 O(n³),性能极低。
2 核心优化策略(React 的“启发式算法”)
React 使用了一些 优化假设(Heuristics) ,将算法复杂度降低到 O(n) :
1 同级比较原则(只比较同一层级的节点)
- React 不会跨层比较元素,只比较同一层级上的节点。
2 不同类型元素直接替换
- 如果新旧节点的
type不同,React 会直接删除旧节点,创建新节点。
<div>变成 <span> → 直接删除 div,添加 span
3 相同类型元素,比较 props 和 children
- 如果类型相同,React 会继续比较属性(props)和子元素(children),并递归执行 Diff。
4 对列表使用 key 提升效率
- key 是 React 判断哪些列表项变化的关键。
- 相同 key 的元素会复用,不同的会被创建或删除。
31. React 中的 key 的作用是什么?
- key 应该是元素的一个唯一的标识,可以是索引、ID 或者其他唯一值。
- key 帮助 React 识别列表哪些元素改变了,相同 key 的元素会复用,不同的会被添加或删除、替换、移动。差异更新,减少不必要的渲染
- key 的核心价值:
- 身份标识:唯一识别列表元素
- 性能优化:最小化 DOM 操作
- 状态保持:确保组件实例正确复用
- 注意事项:
- key值一定要和具体的元素—一对应;
- 尽量不要用数组的index去作为key;
- 不要在render的时候用随机数或者其他操作给元素加上不稳定的key,这样造成的性能开销比不加key的情况下更糟糕。
32. React中的事件处理机制
React 的事件处理机制与原生 DOM 事件略有不同,它实现了一套自己的合成事件系统(Synthetic Event System),以确保跨浏览器的一致性和性能优化。理解这套机制对于编写健壮的 React 应用至关重要。
1. 合成事件 (SyntheticEvent)
React 不会直接将事件监听器附加到真实的 DOM 节点上。相反,它实现了一个合成事件系统:
当你使用 React 绑定事件(例如 onClick、onChange)时,你传入的事件处理函数接收到的并不是原生的浏览器事件对象,而是一个 合成事件SyntheticEvent 实例。
- 跨浏览器兼容性:
合成事件是 React 对原生事件的封装。它抹平了不同浏览器在事件实现上的差异,确保你的事件处理逻辑在所有支持的浏览器中都能保持一致的行为。 - 标准接口:
合成事件提供了与原生 DOM 事件对象相同的标准接口,例如event.preventDefault()、event.stopPropagation()、event.target、event.currentTarget等,让你能够像处理原生事件一样进行操作。 - 事件池(已移除大部分) : 在 React 16 及以前版本,
SyntheticEvent对象是池化的,意味着事件对象会被重用以提高性能。在事件回调函数执行完毕后,事件对象的属性会被清空。如果你需要在异步操作中访问事件属性,需要调用event.persist()。 重要提示: 从 React 17 开始,事件池已被移除。SyntheticEvent对象不再被池化,你可以在异步代码中安全地访问事件属性,无需调用event.persist()。这是 React 17 的一个重要简化,避免了常见的“空事件对象”问题。
2. 事件代理/委托 (Event Delegation)
React 的事件处理系统利用了事件委托的原理来优化性能。
- 统一监听: React 并不会为每个 DOM 元素单独注册事件监听器。相反,它会在文档的根部(或者,从 React 17 开始,在
ReactDOM.createRoot()挂载的那个容器 DOM 节点上)统一注册一个事件监听器。 - 事件冒泡: 当真实 DOM 上的事件触发时,它们会按照浏览器的事件冒泡机制,从触发元素向上传播,直到到达 React 注册的根监听器。
- React 处理: 一旦事件到达根监听器,React 就会捕获它,将其封装成
SyntheticEvent对象,然后根据事件的类型和目标元素,将事件“分发”给对应的 React 组件中的事件处理函数。
这种机制的优势在于:
- 性能提升: 大量减少了真实 DOM 上的事件监听器数量,降低了内存消耗和初始化开销。
- 动态元素处理: 即使组件在运行时被添加或移除,事件处理也能正常工作,无需手动添加或移除监听器。
与传统事件代理的对比
| 特性 | React 事件代理 | 传统 DOM 事件代理 |
|---|---|---|
| 绑定层级 | 根容器/document 级别 | 手动选择的父元素级别 |
| 事件对象 | SyntheticEvent(跨浏览器封装) | 原生 Event 对象 |
| 性能优化 | 自动重用事件对象 | 需手动优化 |
| 事件类型支持 | 大部分常用 DOM 事件 | 取决于手动实现 |
| 事件冒泡处理 | 自动处理 | 需手动处理 event.target |
3. 事件流阶段
React 的事件流类似于原生 DOM 事件流,但有一些关键区别:
-
捕获阶段:
- 从最外层元素向内传播
- 使用
onClickCapture等属性捕获事件
-
目标阶段:
- 到达目标元素
- 触发目标元素上的事件处理程序
-
冒泡阶段:
- 从目标元素向外传播
- 使用
onClick等属性处理冒泡事件
4. 事件绑定
在 React 中,你通常会像绑定普通 HTML 属性一样,将事件处理函数作为组件的 props 传递:
function MyButton() {
function handleClick(event) {
// event 是一个合成事件对象
console.log('Button clicked!', event);
event.preventDefault(); // 阻止默认行为
}
return (
<button onClick={handleClick}>
点击我
</button>
);
}
5. this 的绑定问题(针对类组件)
在类组件中,事件处理函数中的 this 默认情况下是 undefined。这是因为 JavaScript 中函数的作用域规则决定的,方法在被调用时其 this 值取决于调用它的上下文。为了确保 this 正确指向组件实例,你需要进行绑定。
有几种常见的绑定方式:
a. 在 constructor 中绑定 (推荐)
这是最常见的也是官方推荐的方式,因为它只会在组件创建时执行一次绑定操作。
class MyClassComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
// 在构造函数中绑定 'this'
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prevState => ({
count: prevState.count + 1
}));
}
render() {
return (
<button onClick={this.handleClick}>
Click count: {this.state.count}
</button>
);
}
}
b. 使用箭头函数作为类属性 (Class Properties / Property Initializers)
这种方式在 Babel 配置允许的情况下很流行,它利用了箭头函数不绑定自己的 this 的特性,this 会在其定义时(即组件实例创建时)自动绑定到组件实例。
class MyClassComponent extends React.Component {
state = { count: 0 }; // class property syntax
// 使用箭头函数定义事件处理器
handleClick = () => {
this.setState(prevState => ({
count: prevState.count + 1
}));
}
render() {
return (
<button onClick={this.handleClick}>
Click count: {this.state.count}
</button>
);
}
}
c. 在 render 方法中使用箭头函数
这种方式虽然简单,但每次组件渲染时都会创建一个新的函数实例。对于简单的组件来说影响不大,但对于频繁渲染的组件或子组件使用了 React.memo(或 PureComponent)的情况,可能导致不必要的重新渲染,从而影响性能。因此,通常不推荐用于性能敏感的场景。
class MyClassComponent extends React.Component {
state = { count: 0 };
handleClick() {
this.setState(prevState => ({
count: prevState.count + 1
}));
}
render() {
return (
// 每次 render 都会创建一个新的匿名函数
<button onClick={() => this.handleClick()}>
Click count: {this.state.count}
</button>
);
}
}
6. 向事件处理函数传递参数
当你需要向事件处理函数传递额外参数时,也有几种方式:
a. 使用箭头函数(推荐)
这是最常用且简洁的方式。你可以在箭头函数内部调用事件处理函数,并传递参数。
function ItemList({ items }) {
const handleDelete = (id, event) => {
console.log(`Deleting item with ID: ${id}`, event);
// ... 执行删除操作
};
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={(event) => handleDelete(item.id, event)}>
删除
</button>
</li>
))}
</ul>
);
}
注意: 如果你的事件处理函数不需要 event 对象,或者 event 是最后一个参数,你可以省略它:onClick={() => handleDelete(item.id)}。
b. 使用 bind 方法
与箭头函数类似,bind 也可以用于传递参数。但通常不如箭头函数直观。
function ItemList({ items }) {
const handleDelete = (id, event) => { // id 作为第一个参数,event 作为第二个参数
console.log(`Deleting item with ID: ${id}`, event);
};
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
{/* bind 的第一个参数是 this,之后是传递给函数的额外参数 */}
<button onClick={handleDelete.bind(this, item.id)}>
删除
</button>
</li>
))}
</ul>
);
}
特性
它是对原生事件的封装,具有以下特点:
| 特性 | 描述 |
|---|---|
| 跨浏览器统一 | 兼容所有浏览器的事件行为(如阻止默认行为) |
| 性能优化 | 使用事件委托机制 |
| API 统一 | 合成事件拥有原生事件几乎所有属性和方法 |
| 自动回收机制 | 事件对象是池化(pooling)的,生命周期短 |
| 可以阻止默认行为 | 通过 event.preventDefault() 实现 |
| 可以阻止冒泡 | 通过 event.stopPropagation() 实现 |
总结
React 的事件处理机制通过合成事件系统提供了跨浏览器的一致性和性能优化。核心要点包括:
- 合成事件对象提供统一接口。
- 事件委托到根节点。
- React 17 移除了事件池,可以直接在异步代码中访问事件属性。
- 类组件中
this的绑定是关键,推荐在构造函数中绑定或使用类属性的箭头函数。 - 传递参数时,箭头函数是简洁且常用的方式。
掌握这些机制,能够帮助你更好地控制 React 应用中的用户交互。
33. React的事件和普通的HTML事件有什么不同?
| 特性 | React 事件 | 原生 DOM 事件 |
|---|---|---|
| 事件命名 | 驼峰式 (onClick) | 全小写 (onclick) |
| 事件绑定 | JSX 属性 | addEventListener |
| 事件对象 | 合成事件 (SyntheticEvent) | 原生事件对象 |
| 事件传播 | 捕获 → 目标 → 冒泡,冒泡阶段执行 | 捕获 → 目标 → 冒泡,可指定捕获/冒泡阶段 |
| 默认行为阻止 | e.preventDefault() | 相同 |
| 事件移除 | 自动处理 | 需手动 removeEventListener |
区别:
虽然合成事件不是原生DOM事件,但它包含了原生DOM事件的引用,可以通过e.nativeEvent访问
事件传播机制 都是 捕获-》目标-》冒泡
- 对于事件名称命名方式,原生事件为全小写,react事件采用小驼峰;
- 对于事件函数处理语法,原生事件为字符串,react事件为函数
- react事件不能采用return false的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefauLt()来阻止默认行为。
- 合成事件是react模拟原生DOM事件所有能力的一个事件对象,其优点如下
- 兼容所有浏览器,更好的跨平台;
- 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)
方便react统一管理和事务机制
- 兼容所有浏览器,更好的跨平台;
- 事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到document上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document上合成事件才会执行。
34. React Fiber架构,Fiber解决了什么问题
React Fiber 是 React 16 中引入的一个对核心协调算法(Reconciler)的彻底重写。它不是一个直接面向开发者使用的功能,而是 React 内部的底层实现细节,旨在解决传统栈式协调器的局限性,并为 React 18 及未来版本的并发模式 (Concurrent Mode) 和其他高级特性(如时间切片、Suspense)奠定基础。
一个能够进行异步、可中断、高优先级调度的渲染引擎。
1. 为什么需要 Fiber?传统协调器的局限性
在 React Fiber 之前,React 的协调过程是基于栈式 (Stack) 递归的。当组件状态发生变化时,React 会从根组件开始,同步地递归遍历整个组件树,执行 Diff 算法并计算出需要更新的 DOM 变化。
这种同步的、不可中断的递归过程带来了几个问题:
- 阻塞主线程 (Blocking Main Thread) :当组件树非常庞大或更新非常频繁时,整个协调过程可能需要很长时间。由于 JavaScript 是单线程的,这个长时间的计算会阻塞浏览器的主线程,导致页面无响应、动画卡顿、用户输入延迟,严重影响用户体验。
- 无法暂停/恢复:传统的栈式协调器一旦开始就必须一口气执行完,无法在中间暂停或将控制权交还给浏览器。
- 优先级管理困难:所有更新都被视为同等优先级,无法区分紧急的用户输入(如打字)和非紧急的数据加载。
Fiber 架构就是为了解决这些问题而诞生的。
2. Fiber 是什么?
从概念上讲,Fiber 是对 React 渲染单元的重新定义,React 内部管理和调度更新工作的核心机制,React的执行引擎。
- 在 Fiber 架构中,每个 React 元素(如一个
div、一个组件实例)在内部都会对应一个 Fiber 节点。 - 每个 Fiber 节点代表一个工作单元。
- 这些 Fiber 节点之间通过链表的形式连接起来,构成了一个 Fiber 树。
你可以把 Fiber 想象成一个更细粒度、更灵活的“任务单位”,React 可以对这些任务单位进行暂停、恢复、优先级排序和重用。
Fiber 实现增量处理:
- 将整个树的递归遍历拆分为单个 Fiber 节点的处理
- 通过
requestIdleCallback实现时间切片(Time Slicing)
3. Fiber 架构的核心思想:可中断、可恢复的异步渲染
Fiber 架构的核心思想是实现增量渲染 (Incremental Rendering)和任务优先级调度 ,也就是将一个大的渲染任务分解成多个小的、可中断的工作单元。
它的关键在于将协调过程分为两个主要阶段:
阶段一:渲染/协调阶段 (Render/Reconciliation Phase) - 可中断
这个阶段的主要任务是构建 Fiber 树并找出差异,计算变化(虚拟 DOM 比较)。
- 创建虚拟DOM树:在这个阶段,React 会调用组件的
render方法(对于函数组件就是执行函数本身),生成新的虚拟DOM树。 - 构建 Fiber 树 (Work in Progress Tree) :协调器基于新的虚拟DOM树,构建/更新Fiber树,Fiber 树的节点(Fiber 节点)会引用虚拟 DOM 节点,并添加调度和状态管理的元数据。React 会从根 Fiber 节点开始,遍历当前组件树,并为每个组件创建一个新的 Fiber 节点(或者复用旧的 Fiber 节点)。这些新的 Fiber 节点会构成一个“工作中的树”(Work in in Progress tree)。
- 增量Diff 算法:React 会将新虚拟DOM与当前 Fiber 节点的旧虚拟DOM进行 Diff 比较,标记出哪些节点需要更新、添加或删除(这些标记称为
effectTag)。 - 可中断性:这个阶段是可以被暂停的。React 会检查当前是否有更高优先级的任务需要执行(例如,浏览器有绘制任务,或者用户有输入事件)。如果存在,React 会将当前工作单元暂停,将控制权交还给浏览器,让浏览器处理高优先级任务。当浏览器空闲时,React 会从上次暂停的地方继续执行。
- 不执行 DOM 操作:在这个阶段,React 不会进行任何实际的 DOM 操作。所有的更新都只发生在内存中的 Fiber 树上。
阶段二:提交阶段 (Commit Phase) - 不可中断
当渲染阶段完成后,所有的更新都被计算出来并标记在 Fiber 树上。提交阶段的任务就是将这些变化应用到真实 DOM 上。
- 应用 DOM 更新:React 会遍历带有
effectTag的 Fiber 节点,并执行相应的 DOM 操作(添加、更新、删除等)。 - 调用生命周期方法/Hooks 副作用:在这个阶段,React 会执行那些需要在 DOM 更新后才执行的生命周期方法(如
componentDidMount、componentDidUpdate)和useEffect的副作用函数。 - 不可中断性:提交阶段是同步的、不可中断的。一旦开始,就必须一口气执行完。这是因为 DOM 操作会引起浏览器重绘,为了避免 UI 闪烁或不一致,必须保证 DOM 操作的原子性。
Fiber 的双缓冲技术
工作原理
实际应用示例
-
首次渲染:
- 构建全新的 WorkInProgress 树
- 提交后成为 Current 树
-
更新时:
- 基于 Current 树克隆出 WorkInProgress 树
- 在内存中完成更新后切换指针
fiber的执行流程
React Fiber 是 React 的一种新的协调引擎,其执行流程如下:
任务调度阶段: React Fiber 通过优先级调度器(Priority Scheduler)来决定当前要执行的任务单元。每个任务单元都是一个 Fiber 节点,记录了该任务的类型、状态、子节点等信息。React Fiber 将任务单元按照优先级分为多个批次(Batch),每个批次包含一组任务单元。React Fiber 首先会执行高优先级的批次,然后执行低优先级的批次。
协调过程: 在每个任务单元执行前,React Fiber 会检查该任务单元是否需要更新。如果需要更新,则会进入协调阶段,根据当前节点和子节点的状态来决定该任务单元的执行方式。协调阶段分为两个步骤:Reconciliation 和 Commit。
Reconciliation 阶段: React Fiber 通过遍历 Fiber 树来比较新旧虚拟 DOM 的差异,找出需要更新的节点,并标记这些节点为“脏节点”。React Fiber 通过 diff 算法来比较新旧虚拟 DOM 的差异,尽可能地重用已有的节点,从而减少 DOM 操作的次数。
Commit 阶段: 在 Reconciliation 阶段完成后,React Fiber 会进入 Commit 阶段,将所有“脏节点”更新到真实的 DOM 上。在 Commit 阶段,React Fiber 会调用各个生命周期方法,如 componentDidMount、componentDidUpdate 等,以完成组件的更新和挂载。
完成阶段: 在 Commit 阶段完成后,React Fiber 将当前的工作进度标记为完成,并将结果返回给调用者。如果还有未完成的任务单元,React Fiber 会继续执行任务调度和协调阶段,直到所有任务都完成。
4. Fiber特性/解决了哪些问题?
React Fiber 是 React 的一种新的协调引擎,它具有改进协调算法、支持增量渲染、可中断和恢复、支持并发模式、向后兼容和支持错误边界等特点,从而提高了 React 应用的性能、可扩展性和健壮性。
改进了协调算法: React Fiber 使用的是基于优先级的协调算法,将任务分解为多个小任务,并根据任务的优先级来调度任务的执行,从而提高了应用的性能。
支持增量渲染: React Fiber 支持增量渲染,即在处理任务时可以中断任务,并优先处理更高优先级的任务,从而使得用户能够更快地看到页面的变化。
可中断和恢复: React Fiber 允许开发者在组件渲染的过程中对任务进行中断和恢复,从而支持更细粒度的控制,提高了应用的性能和响应速度。
支持并发模式: React Fiber 的设计是支持并发模式的,可以在多个线程中同时执行任务,从而提高了应用的性能。
向后兼容: React Fiber 的设计是向后兼容的,可以在不影响旧的应用的情况下逐步升级到新的版本,从而使得 React 应用的升级变得更加容易。
支持错误边界: React Fiber 支持错误边界机制,当组件发生错误时,可以通过错误边界机制来捕获错误并展示友好的错误信息,从而提高了应用的健壮性和用户体验
5. Fiber 树的结构
Fiber 是对虚拟 DOM 的重新实现,每个 React 元素对应一个 Fiber 节点
每个 Fiber 节点大致包含以下核心字段:
type:对应 React 元素的类型(例如div、span、MyComponent)。key:和 React 元素的key属性相同,用于 Diff 算法。props:传递给组件的属性。stateNode:指向组件实例(对于类组件)或 DOM 节点(对于宿主组件,如div)。return:指向父 Fiber 节点。child:指向第一个子 Fiber 节点。sibling:指向下一个兄弟 Fiber 节点。pendingProps/memoizedProps:用于存储新的和旧的 props。pendingState/memoizedState:用于存储新的和旧的 state。effectTag:一个数字标志,表示该 Fiber 节点需要执行的副作用(如插入、更新、删除)。nextEffect:指向下一个有副作用的 Fiber 节点,形成一个副作用链表 (effect list)。
通过 child、sibling 和 return 指针,Fiber 节点形成了一个链表结构的树,可以方便地进行深度优先遍历,并且可以随时暂停和恢复遍历过程。
interface Fiber {
tag: WorkTag; // 组件类型(函数组件/类组件/Host组件等)
key: string | null; // 同 React Element 的 key
elementType: any; // 创建元素的函数/类/标签名
type: any; // 同 elementType(处理懒加载后可能不同)
stateNode: any; // 关联的 DOM 节点或组件实例
// 链表结构
return: Fiber | null; // 父节点
child: Fiber | null; // 第一个子节点
sibling: Fiber | null; // 下一个兄弟节点
index: number; // 在父节点中的索引
// 副作用相关
flags: Flags; // 标记需要进行的操作(插入/更新/删除等)
subtreeFlags: Flags; // 子树中的副作用标记
deletions: Fiber[] | null; // 待删除的子节点
// 状态和 props
pendingProps: any; // 新 props
memoizedProps: any; // 上次渲染使用的 props
memoizedState: any; // 上次渲染使用的 state
// 调度相关
lanes: Lanes; // 优先级车道
childLanes: Lanes; // 子节点的优先级
// 双缓存技术
alternate: Fiber | null; // 当前树和 workInProgress 树之间的链接
}
6. 总结
React Fiber 架构是 React 发展中的一个里程碑,它将 React 从一个同步的渲染引擎转变为一个能够进行异步、可中断、高优先级调度的引擎。尽管作为开发者我们通常不需要直接操作 Fiber 节点,但理解其基本原理有助于我们更深入地理解 React 的性能优化机制,并更好地利用 useTransition、useDeferredValue 等并发 API 来构建更流畅、响应更快的用户界面。
35. React Hooks工作原理
React Hooks 的本质是一套在函数组件中管理状态、副作用和生命周期的机制。它们看起来简单,但背后涉及到 React 内部对Fiber 树的状态管理、链表结构、状态队列和执行顺序的精密控制。
React Hooks 是通过“以调用顺序为定位标识”,在 Fiber 上维护一条 Hook 链,来记录和更新每一个 Hook 的状态。
通过调用顺序 + 链表顺序来“对号入座”
1. Hooks 的核心原理:链表与“记忆”
Hooks 的实现原理主要依赖于 React 内部对组件的渲染和更新机制。最关键的一点是,React 是如何知道哪个 useState 对应哪个状态,哪个 useEffect 对应哪个副作用的。答案是:基于调用顺序的链表结构和对状态的“记忆” 。
a. 内部数据结构:链表
当 React 渲染一个函数组件时,它会在内部维护一个与该组件实例关联的链表(或者更准确地说,是一个数组,但行为类似链表)。每次调用 Hooks(如 useState、useEffect、useRef 等)时,React 都会在当前组件对应的链表中查找或创建相应的 Hook 对象。
-
useState原理:- 当你第一次调用
useState()时,React 会初始化一个状态值,并将其存储在当前组件对应的 Hook 链表的第一个位置。 - 每次组件重新渲染时,当你再次调用
useState(),React 会按照相同的顺序从链表中取出对应位置的状态值。 setState函数会触发组件的重新渲染,并更新 Hook 链表中对应位置的状态值。
- 当你第一次调用
-
useEffect原理:- 第一次调用
useEffect()时,React 会记录下你传入的副作用函数和依赖项数组。 - 每次组件重新渲染时,React 会比较新的依赖项数组和上一次的依赖项数组。
- 如果依赖项发生变化,或者没有依赖项数组(每次都运行),React 会执行副作用函数。
- 副作用函数返回的清理函数(如果有)也会被记录,在下次副作用执行前或组件卸载时调用。
- 第一次调用
b. 严格的调用顺序
正是因为 React 依赖于 Hooks 的调用顺序来查找对应的状态和副作用,所以 Hooks 必须遵守以下规则:
- 只能在 React 函数组件或自定义 Hook 中调用 Hooks:不能在普通的 JavaScript 函数中调用。
- 不能在循环、条件语句或嵌套函数中调用 Hooks:必须总是在组件的顶层调用 Hooks,确保每次渲染时的调用顺序是稳定且一致的。
// 错误示例:在条件语句中调用 Hook
function MyComponent(props) {
if (props.isLoggedIn) {
// 🚩 错误:Hooks 必须在顶层调用
const [userName, setUserName] = useState('');
}
// ...
}
// 正确示例
function MyComponent(props) {
const [userName, setUserName] = useState(''); // ✅ 在顶层调用
useEffect(() => { // ✅ 在顶层调用
if (props.isLoggedIn) {
// ... 逻辑
}
}, [props.isLoggedIn]);
// ...
}
如果违反了这些规则,Hooks 的链表顺序就会被打乱,导致 React 无法正确匹配状态和副作用,从而引发难以调试的错误。
c. 对值进行“记忆” (Memoization)
Hooks 也能“记忆”一些值,以优化性能:
useMemo:记忆计算结果。它会在依赖项不变的情况下,返回上一次计算的结果,避免不必要的重复计算。useCallback:记忆函数。它会在依赖项不变的情况下,返回上一次创建的函数实例,避免子组件(尤其是使用了React.memo的子组件)不必要的重新渲染。useRef:创建一个可变的ref对象,并在组件的整个生命周期中保持引用不变。它可以在多次渲染之间存储任何可变值,而不会触发重新渲染。
2. 常见 Hooks 的工作原理简述
-
useState:- 第一次渲染:初始化 state,将其存储在内部链表中。返回
[state, setState]。 - 后续渲染:按照顺序从内部链表中读取 state 值。
setState调用会触发组件重新渲染,并在内部更新链表中的 state 值。
- 第一次渲染:初始化 state,将其存储在内部链表中。返回
-
useEffect:- 第一次渲染:记录副作用函数和依赖项数组。在 DOM 更新后执行副作用函数。
-
每个
useEffect会被收集到一个 effect list 中。 -
在 commit 阶段,React 会按顺序执行:
- 先清除旧的副作用(如果有
return); - 然后执行新的副作用函数。
- 先清除旧的副作用(如果有
-
- 后续渲染:比较新的依赖项数组和上一次的。如果依赖项变化,则先执行上一次的清理函数(如果有),然后执行新的副作用函数。
- 组件卸载:执行最后一次的清理函数。
- 第一次渲染:记录副作用函数和依赖项数组。在 DOM 更新后执行副作用函数。
-
useContext:- 允许组件订阅最近的 Context Provider 提供的值。当 Context 值变化时,订阅的组件会重新渲染。
-
useRef:- 在组件的整个生命周期中返回并保持同一个可变对象引用。它不会触发组件重新渲染。
3. 示例:多个 useState 会生成怎样的 Hook 链?
function Counter() {
const [count, setCount] = useState(0); // Hook1
const [name, setName] = useState('Tom'); // Hook2
return <div>{count} - {name}</div>;
}
这会在对应 Fiber 节点上生成:
Fiber.memoizedState → Hook1 → Hook2 → null
具体结构:
Hook1 = {
memoizedState: 0, // count 的值
queue: [...], // setCount 的更新队列
next: Hook2
}
Hook2 = {
memoizedState: 'Tom', // name 的值
queue: [...], // setName 的更新队列
next: null
}
Hooks 链表在更新时怎么用?
- 每次组件重新渲染,React 会把当前 Fiber 节点的
memoizedState指针重置为第一个 Hook 节点。 - 然后,每执行一个 Hook(如
useState()),就沿着链表往下读取下一个 Hook 节点,取出对应的状态。
这样就可以确保:
- 第 1 个
useState始终取到第 1 个值; - 第 2 个
useState始终取到第 2 个值; - 不靠变量名,只靠“顺序”。
4. Hooks 对 React 内部的影响
Hooks 的引入,并没有改变 React 的虚拟 DOM、Diff 算法或 Fiber 架构。它们依然是 React 高效更新 UI 的核心。Hooks 只是提供了一种新的、更灵活的方式来组织和管理组件内部的逻辑和状态,让函数组件能够更全面地利用 React 的能力。
通过调用顺序 + 链表顺序来“对号入座”
总结来说,React Hooks 的工作原理是基于其内部的链表数据结构,通过严格的调用顺序来“记忆”和管理函数组件的状态和副作用。这使得函数组件能够变得“有状态”,并且通过 useCallback 和 useMemo 等 Hooks 提供了更细粒度的性能优化控制,从而大大提升了 React 开发的效率和代码的可读性。
5. Hooks和闭包
state值不是在fiber hooks链表上读取的吗,为什么说是闭包呢?
这是一个非常好的问题,它触及了 React Hooks 内部实现和 JavaScript 闭包机制之间深层次的联系。你提出的观点很正确:state 值确实是在 Fiber 节点上的 Hooks 链表上读取的。
那么,为什么我们还会说 Hooks 和闭包有关系呢?
澄清:Fiber Hooks 链表是“存储机制”,闭包是“访问机制”
要理解这一点,我们需要区分 数据存储 和 数据访问。
-
Fiber 上的 Hooks 链表(存储机制)。当 React 渲染一个函数组件时:
- 第一次渲染:
useState会在当前组件对应的 Fiber 节点上创建一个新的 Hook 对象,其中包含初始的state值,并将其添加到 Fiber 内部维护的 Hooks 链表的第一个位置。 - 后续渲染:React 会再次执行这个函数组件。当它遇到
useState调用时,它会沿着 Fiber 节点上的 Hooks 链表,根据调用顺序找到对应的 Hook 对象,并从那里读取state的当前值。setState函数也会被保存在这个 Hook 对象上,当它被调用时,会通知 React 更新 Fiber 链表上对应 Hook 的state值,并触发重新渲染。
所以,Fiber 上的 Hooks 链表是
state值被 React 内部存储和管理的地方。 - 第一次渲染:
-
闭包(访问机制) 闭包在这里的作用是,确保当你在组件内部定义任何函数(包括事件处理函数、
useEffect的副作用函数、甚至其他普通的辅助函数)时,这些函数能够正确地**“捕获”或“记住”它们在定义时所处的那个特定渲染周期中的props和state值。
为什么说闭包在其中起作用?
虽然 state 本身存储在 Fiber 链表上,但组件函数每次渲染都会重新执行,其内部的局部变量(包括 state 变量)和函数都会被重新创建。闭包确保了这些重新创建的函数能够访问到“正确版本”的 state。
我们来看一个例子:
function Counter() {
// 每次 Counter 重新渲染,useState 都会从 Fiber 链表上获取当前的 count 值
const [count, setCount] = React.useState(0);
// 这是一个在每次渲染时都会被重新创建的函数
const handleClick = () => {
// 这里的 `count` 是一个闭包变量
// 它“记住”了当这个 handleClick 函数被创建时,`count` 的值是多少
setCount(count + 1); // 如果你直接使用 count,它会捕获当前渲染的 count
};
// 如果你使用函数式更新,闭包的作用更明显,它捕获的是 setCount 本身
// const handleClick = () => {
// setCount(prevCount => prevCount + 1); // prevCount 总是最新的,因为这是 React 内部传递的参数
// };
React.useEffect(() => {
// 这里的 `count` 也是一个闭包变量
// 这个 effect 函数在每次渲染时都会被重新创建,并捕获当前渲染周期中的 count 值
console.log('Current count in effect:', count);
return () => {
// 清理函数也会捕获它创建时的 count 值
console.log('Cleanup for count:', count);
};
}, [count]); // 依赖项是 count,所以当 count 变化时,effect 会重新运行
return (
<button onClick={handleClick}>
Count: {count}
</button>
);
}
深入理解闭包的作用:
- “捕获”特定渲染周期的值: 每次
Counter组件函数执行时,它会创建一个全新的执行上下文。useState会从 Fiber 链表上拉取count的当前值,并作为局部变量赋给count。 当handleClick或useEffect内部的函数被定义时,它们就通过闭包捕获了这个特定执行上下文中的count变量。这意味着,如果你不把count列为useEffect的依赖,那么useEffect的副作用函数将永远访问到第一次渲染时捕获的count值(这就是所谓的 “陈旧闭包”或“Stale Closure” )。 - 隔离不同渲染周期: 由于每个渲染周期都会创建新的闭包,这实际上帮助我们隔离了不同渲染周期的数据。每个
handleClick实例都知道它自己所属的那个渲染周期的count值。这对于 React 的并发模式至关重要,因为它可以同时处理多个渲染,而不会让它们的状态混淆。 setCount函数的稳定性:useState返回的setCount函数的引用是稳定的,它在组件的生命周期中不会改变。这是因为setCount本身在内部通过闭包捕获了对当前 Fiber 节点和其对应的 Hook 对象的引用。这样,无论你何时调用setCount,它都知道去哪个 Fiber 上的哪个 Hook 对象更新状态。
总结
state 值确实存储在 Fiber 节点内部的 Hooks 链表上,这是 React 内部高效管理的机制。
而闭包则是一种 JavaScript 特性,它确保了在函数组件每次渲染过程中创建的那些局部函数(如事件处理函数、useEffect 回调、memo/callback 的依赖等)能够**“记住”并访问到它们定义时所处的特定渲染周期的 props 和 state 值**。
这两者协同工作,Fiber提供了状态的存储和更新机制,而闭包则提供了状态的正确访问和隔离。理解这种协同关系,能让你更深入地掌握 Hooks 的精髓,并避免常见的开发陷阱。
36. React Router工作原理
React Router 通过监听 URL 变化,匹配对应的组件树,动态渲染页面,而不是刷新整个页面。
SPA
即单页面应用(Single Page Application)。所谓单页 Web 应用,就是只有一张 Web 页面的应用。单页应用程序 (SPA) 是加载单个 HTML 页面并在用户与应用程序交互时动态更新该页面的 Web 应用程序。浏览器一开始会加载必需的 HTML 、 CSS 和 JavaScript ,所有的操作都在这张页面上完成,都由 JavaScript 来控制
前端路由
在 Web 前端单页应用 SPA(Single Page Application)中,路由描述的是 URL 与 UI (组件)之间的映射关系,这种映射是单向的,即 URL 变化引起 UI(组件) 更新(无需刷新页面)
如何实现前端路由
- 如何改变URL却不引起整个页面刷新,局部刷新,只刷新URL对应的UI组件
- 如何检测URL改变
背景
-
spa 但页面应用,只有一个index.html文件,怎么访问
http://localhost:8080/home,返回index.html文件呢,稍微复杂一点的 SPA ,都需要用到路由 -
在传统的hash模式中(
http://localhost:8080#home),即使不需要配置,静态服务器始终会去寻找index.html并返回给我们,然后react-router会获取#后面的字符作为参数,对前端页面进行变换。 -
类比一下,在history模式中,我们所想要的情况就是:输入http://localhost:8080/home,但最终返回的也是index.html,然后react-router会获取home作为参数,对前端页面进行变换。那么在nginx中,谁能做到这件事呢?答案就是try_files
- 大意就是它会按照try_files后面的参数依次去匹配root中对应的文件或文件夹。如果匹配到的是一个文件,那么将返回这个文件;如果匹配到的是一个文件夹,那么将返回这个文件夹中index指令指定的文件。最后一个uri参数将作为前面没有匹配到的fallback。(注意try_files指令至少需要两个参数)
为什么需要前端路由
- 早期:一个页面对应一个路由,路由跳转导致页面刷新,用户体验差
- ajax的出现使得不刷新页面也可以更新页面内容,出现了SPA(单页应用)。SPA不能记住用户操作,只有一个页面对URL做映射,SEO不友好
- 前端路由帮助我们在仅有一个页面时记住用户进行了哪些操作
前端路由解决了什么问题
- 当用户刷新页面,浏览器会根据当前URL对资源进行重定向(发起请求)
- 单页面对服务端来说就是一套资源,怎么做到不同的URL映射不同的视图内容
- 拦截用户的刷新操作,避免不必要的资源请求;感知URL的变化
- React Router 是一个用于构建单页面应用的路由库。
- 它通过监听 URL 的变化,来渲染不同的组件。
- React Router 的核心概念是路由器、路由和组件。
路由模式
hash
概念
实际就是 URL 中#后面的东西 它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面
原理
通过 hashchange 事件监听 URL 的变化,改变 URL 的方式只有这几种
- 通过浏览器前进后退改变 URL
- 通过
<a>标签改变 URL - 通过
window.location改变URL
特点
兼容性好但是不美观
history
概念
提供了 pushState 和 replaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新
这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
原理
通过监听popstate事件和拦截 <a>标签的点击事件来检测 URL 变化
- 通过浏览器前进后退改变 URL 时会触发 popstate 事件
- 通过
pushState/replaceState或<a>标签改变 URL 不会触发 popstate 事件。 好在我们可以拦截 pushState/replaceState的调用和<a>标签的点击事件来检测 URL 变化 - 通过js 调用history的back,go,forward方法课触发该事件
nginx
在rf-blog-web这个目录中,没有子目录,只有一个index.html和一些压缩后的名称是hash值的.js文件。当我们请求http://localhost:8080/home这个地址时,首先查找有无home这个文件,没有;再查找有无home目录,也没有。所以最终会定位到第三个参数从而返回index.html,按照这个规则,所有路由里的url路径最后都会定位到index.html。vue-router再获取参数进行前端页面的变换,至此,我们已经可以通过http://localhost:8080/home这个地址进行成功地访问了。
而uri/在这个例子中并没有多大用,实际上是可以去掉的。
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files \$uri \$uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
特点
虽然美观,但是刷新会出现 404 需要后端进行配置
主要的核心组件
-
<BrowserRouter>(或<HashRouter>):这是应用程序的路由容器,它负责监听 URL 的变化。BrowserRouter使用 History API,适用于大多数现代 Web 应用;HashRouter使用 URL 的哈希部分(如#/about),适用于一些旧浏览器或需要兼容静态文件服务器的场景。 -
<Routes>:一个容器,用于包裹所有的<Route>组件。它会遍历其子<Route>,并渲染第一个匹配当前 URL 的<Route>。 -
<Route>:定义了一个路由规则。它有两个主要属性:path:要匹配的 URL 路径。element:当路径匹配时要渲染的 React 元素。
-
<Link>:用于在应用内部导航到不同的路径。它会阻止浏览器默认的链接跳转行为,而是通过 History API 更新 URL 并触发 React Router 的路由匹配。 -
useNavigate()Hook:一个 Hook,用于在函数组件中进行编程式导航(例如,表单提交后跳转到另一个页面)。
路由匹配与渲染
当 URL 发生变化时(无论是通过 Link 点击、编程式导航还是浏览器前进/后退):
<BrowserRouter>监听到 URL 变化。- 它会通知其内部的
<Routes>组件。 <Routes>会遍历其子<Route>组件,根据它们的path属性与当前的 URL 进行模式匹配。- 第一个匹配成功的
<Route>会将其element属性中定义的组件渲染到 DOM 中。
React Router 工作流程图(简化)
用户点击 <Link>
↓
React Router 更改URL 调用 history.pushState()/history.replaceState(),或则hash方式
↓
监听 URL 改变(popstate或则hashchange)
↓
匹配 Route 配置 对应path的组件
↓
更新相应组件树并重新渲染
37. Redux工作原理
Redux 是一个可预测的状态管理容器,它通过严格的单向数据流来管理应用状态。以下是 Redux 的核心工作原理:
三大核心原则
使用单例模式实现
-
单一数据源:
- 整个应用的状态存储在一个单一的 store 对象树中
- 状态树是只读的,只能通过触发 action 来修改
-
状态是只读的:
- 唯一改变状态的方法是触发 action(一个描述发生了什么的对象)
- 状态不会被直接修改,而是通过纯函数生成新状态
-
使用纯函数执行修改:
- Reducer 是纯函数,接收旧 state 和 action,返回新 state
- 必须返回全新的对象,而不是修改原 state
Redux核心概念
-
Store (存储) :整个应用的状态树。它是唯一的,并且所有状态都存储在其中。
-
Action (动作) :一个普通的 JavaScript 对象,用于描述“发生了什么”。它必须有一个
type属性,通常还包含payload。 -
Reducer (纯函数) :接收当前状态 (state) 和一个动作 (action) 作为参数,然后返回一个新的状态。Reducer 必须是纯函数,即不修改传入的参数,不产生副作用,给定相同的输入总是返回相同的输出。
-
Dispatcher (分发器) :用于发送 Action 到 Store 的方法。当一个 Action 被 dispatch 时,Redux Store 会调用对应的 Reducer 来计算出新的状态。
-
View: UI 组件,通过订阅 Store 来获取状态更新,并渲染相应的视图。
-
Selector(选择器) Selector 是一个函数,用于从 Store 中提取所需的状态数据。它有助于将状态的结构与使用者分离,方便状态管理和代码维护。
const selectCount = (state) => state.count;
Redux 的工作流程
- Dispatch Action: 当用户与应用交互时,会触发一个 action。
- 调用Reducer: Reducer 函数会接收当前的 state 和 action,并返回一个新的 state。
- Store 更新: Store 会将新的 state 保存起来。
- View 更新: 所有订阅了 Store 的组件都会重新渲染,以反映状态的改变。
React 和 Redux 的结合
React-Redux 的主要作用是提供一种机制,让 React 组件能够:
- 获取 Redux Store 中的状态 (State) :让组件能够“看到”Store 中的数据,使用
<Provider>包裹你的应用。 - 分发 Action (Dispatch Actions) :让组件能够“通知”Redux Store 状态需要改变。
它通过两个主要的 API 来实现这些功能:Provider 组件 和 connect 函数 (用于类组件) 或 Hooks ( useSelector, useDispatch ) (用于函数组件)。在现代 React 开发中,Hooks 是首选。
创建 Redux Store
// store.js
import { createStore } from 'redux';
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
}
const store = createStore(counterReducer);
export default store;
连接 React 应用
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
在组件中使用
// App.js (一个简单的计数器组件)
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
// 使用 useSelector 从 Redux Store 中获取 count 状态
const count = useSelector(state => state.count);
// 获取 dispatch 函数的引用
const dispatch = useDispatch();
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
Increment
</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>
Decrement
</button>
</div>
);
}
export default Counter;
38. SSR (服务端渲染) 的原理是什么?
SSR 是在服务器端将 React 组件渲染成 HTML 字符串,然后将 HTML 发送给浏览器。
什么是 SSR?
SSR(Server-Side Rendering)即服务端渲染。传统的前端应用通常是客户端渲染(CSR),即浏览器下载 HTML、CSS、JavaScript 文件后,在浏览器中动态生成 DOM 并渲染页面。而 SSR 则是在服务器端直接生成完整的 HTML,然后将生成的 HTML 发送给浏览器
SSR 工作流程
1. 客服端请求:当客户端发起请求时,服务器接收到请求并开始处理。
2. 服务器渲染:服务器运行 React 组件,将它们渲染为 HTML 字符串。
3. 返回 HTML:服务器将生成的 HTML 发送回客户端。
4. 客户端挂载:客户端加载 React 的 JavaScript 文件,客户端代码会将现有的 HTML 与 React 组件进行挂载,从而使页面变得可交互。
SSR 的实现
React SSR 可以通过 Node.js 环境下的 react-dom/server 模块实现。react-dom/server 提供了两个主要的 API 来支持服务端渲染:
- renderToString:将 React 组件渲染为 HTML 字符串。
- renderToNodeStream:将 React 组件渲染为 HTML 字符串流,支持更高效的流式渲染。
SSR 的优势和挑战
优势
- SEO 友好:
- 搜索引擎可以直接抓取完整的 HTML 内容
- 解决 SPA 应用 SEO 不佳的问题
-
更快的首屏渲染:
- 用户立即看到完整页面,无需等待 JS 加载执行
- 特别适合低端设备和慢速网络环境
-
更好的用户体验:
- 减少白屏时间
- 内容立即可见,提高用户留存率
挑战
- 服务器负载增加:每个请求都需要服务器渲染整个页面,会增加服务器的负担,可能需要更多的计算资源。
- 复杂性增加:SSR 需要处理更多的问题,如数据预取、路由处理、状态同步等,使得开发和调试更加复杂。
- Hydration 问题:如果服务器渲染的 HTML 和客户端渲染的 HTML 不一致,可能会导致 hydration 过程中的问题。
SSR 与静态生成
除了 SSR,还有一种类似的预渲染技术称为静态生成(Static Generation,简称 SSG)。与 SSR 不同,SSG 是在构建时预先生成所有页面的 HTML 文件,然后在服务器上直接提供这些文件。SSG 非常适合内容基本静态、更新不频繁的页面,如博客、文档等。Next.js 是一个流行的 React 框架,它支持同时使用 SSR 和 SSG。
使用 Next.js 实现 SSR
Next.js 是一个 React 框架,它内置了对服务端渲染的支持。使用 Next.js,开发者可以轻松地实现 SSR,而不必手动设置 Express 服务器和配置复杂的 SSR 逻辑。
Next.js 的核心概念
-
页面文件系统路由
Next.js 使用文件系统作为路由的基础。任何在 pages 目录中的 .js、.jsx、.ts 或 .tsx 文件都会自动成为一个路由。比如,pages/index.js 对应网站的首页 (/),pages/about.js 对应 /about 路径。 -
getServerSideProps 函数
getServerSideProps 是 Next.js 提供的一个特殊函数,它可以让你在服务器端渲染 (SSR) 时预取数据。在每次请求时,Next.js 都会在服务器上调用 getServerSideProps,然后将返回的数据作为 props 传递给页面组件。
// pages/user.js
export async function getServerSideProps(context) {
const { params, req, res } = context;
const user = await getUser(params.id);
return {
props: { user } // 将作为 props 传递给页面组件
};
}
export default function UserPage({ user }) {
return <div>{user.name}</div>;
}
Next.js SSR 的工作原理
2.1. 请求处理
当客户端发送请求到一个通过 getServerSideProps 配置的页面时,Next.js 在服务器上执行以下步骤:
-
服务器接收请求:Next.js 服务器接收到客户端的 HTTP 请求。
-
调用 getServerSideProps:Next.js 会运行页面组件中定义的 getServerSideProps 函数来获取需要渲染的数据。这个函数是异步的,可以从数据库、API 或其他外部资源中获取数据。
-
服务器渲染:拿到 getServerSideProps 返回的数据后,Next.js 在服务器上使用这些数据渲染 React 组件,将其转换为 HTML 字符串。
-
返回HTML:服务器将渲染后的 HTML 以及页面所需的 JavaScript 文件发送给客户端。
-
客户端挂载:客户端接收到 HTML 后,Next.js 的客户端代码会将现有的 HTML 与 React 组件进行挂载(即 Hydration),从而使页面变得可交互。