React 的简单介绍
- 特色 - 组件化:像搭乐高一样,把页面拆分成一个个独立、可复用的"零件"(组件),然后组合起来。
- 特色 - 虚拟DOM:虚拟DOM带来两个好处:
- 性能卓越: 每次数据变更,只会更新真正产生变更的地方
- 跨平台:虚拟DOM把HTML的DOM节点抽象成了一个JS的对象,所以React理论可以运行在所有支持js的环境中,如:nodejs(nextjs)、移动APP(React Native)、PC平台(electron)
a. 使用在线编辑器:访问 codesandbox.io ,选择 "React" 模板
b. 本地开发(推荐最终方式):使用 Create React App
-
这是React官方推荐的脚手架工具,能一键生成项目环境。
-
只需要一行命令 (确保已安装Node.js):
bashnpx create-react-app react-demo cd react-demo npm start
-
执行后,一个React开发环境就在
http://localhost:3000
运行起来了!
1. JSX (JavaScript XML) - 在JS里写"HTML"
1.1 什么是JSX?
JavaScript的语法扩展,允许我们在JavaScript代码中直接编写类似HTML的结构。在我的理解中JSX就是一个描述UI结构的字符串。
-
示例:
js// js生成HTML片段版本 const name = 'World' const time = '2025-09-01' const element = '<div>'+ '<h1>Hello, ' + name + '!</h1>' + '<p>当前时间:' + time + '</p>' + '</div>';
js// 使用ES6语法优化版本 const name = 'World' const time = '2025-09-01' const element = ` <div> <h1>Hello, ${ name }!</h1> <p>当前时间:${ time }</p> </div> `;
jsx// React jsx 版本 const [ name, nameSet ] = useState('World'); const [ time, timeSet ] = useState('2025-09-01'); const element = ( <div> <h1>Hello, { name }!</h1> <input onChange={ change }/> <p>当前时间:{ time }</p> </div> )
1.2 在JSX中嵌入表达式
使用花括号 {}
可以嵌入任何有效的JavaScript表达式,如:if
语句、 for
循环或者三目运算等
jsx
const [ logged, loggedSet ] = useState(false);
const [ name, nameSet ] = useState('World');
const [ time, timeSet ] = useState('2025-09-01');
const [ devices, devicesSet ] = useState([
{ time: '2025-01-01', device: 'Android', address: '北京市朝阳区' },
{ time: '2025-05-01', device: 'IPhone 16 Pro', address: '深圳市福田区' },
{ time: '2025-09-01', device: 'Chrome 139.0.0.0', address: '深圳市福田区' },
]);
const list = (
{/*
JSX表达式必须有一个父元素包裹,通常使用 `<div>` 或 React Fragment `<></>`, 在React Native中一般是View; 使用React Fragment`<></>`,不会产生额外的DOM元素
*/}
<>
<h3>登录设备:</h3>
{
devices.map(item => (
<div>
<div>{ item.time }</div>
<div>{ item.device }</div>
<div>{ item.address }</div>
</div>
))
}
</>
)
const element = (
{/* 使用 `className` 而不是 `class`(因为class是JS的保留字)。 */}
<div className={ logged ? '' : 'disabled' }>
<h1>Hello, { name }!</h1>
{/* 所有标签都必须闭合,如 `<img />` 或 `<input />`。 */}
<img src={ user.avatarUrl }></img>
<input onChange={ change }/>
<p>当前时间:{ time }</p>
{ list }
</div>
)
2. 组件 - 应用的基石
使用组件可以将UI拆分为独立、可复用的代码片段。在React中组件有两种书写方式:函数组件和类组件。
2.1 函数组件 (推荐) 最简单的定义组件的方式是编写一个JavaScript函数。
jsx
// 定义一个组件,这里注意有两个关键元素:
// 1. 组件名称: Welcome (函数名必须大写)
// 2. 外部状态: props (必须是一个对象)
function Welcome(props) {
// 定义组件内部状态, 这里的name对应java中数据的get;nameSet对应数据的set(Model)
const [ name, nameSet ] = useState('World');
// 定义组件内部行为 (Control)
const change = (event) => {
const value = event.target.value;
// 更新state,组件会重新渲染
nameSet(value);
}
// 另一个useEffect,模拟componentDidMount
useEffect(() => {
console.log('仅挂载时执行一次');
// 相当于 componentWillUnmount
return () => {
console.log('组件即将卸载');
};
}, []); // 空依赖数组表示只在挂载和卸载时执行
// 相当于 componentDidMount 和 componentDidUpdate
useEffect(() => {
console.log('组件已挂载或count已经更新更新');
}, [name]); // 依赖数组,只有count变化时才执行
// 这个组件返回一段JSX(View)
return (
<div>
<h1>Hello, { name }!</h1>
<input onChange={ change }/>
<p>当前时间:{ props.time }</p>
</div>
);
}
// 3. 像使用HTML标签一样使用它
function App() {
return (
<div>
<Welcome time="2025-09-01"/> {/* 这就是我们自定义的组件 */}
</div>
);
}
2.2 类组件 (Class Components) ES6的Class也可以用来定义组件,但现在主要用于需要生命周期函数的旧项目。
jsx
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {
name: ''
};
}
componentDidMount() {
console.log('类组件: 组件已挂载');
}
componentDidUpdate(prevProps, prevState) {
console.log('类组件: 组件已更新');
}
componentWillUnmount() {
console.log('类组件: 组件即将卸载');
}
shouldComponentUpdate(nextProps, nextState) {
console.log('类组件: 判断是否应该更新');
// 只有当count变化时才更新
return nextState.name !== this.state.name
}
change(event) {
this.state.name = event.target.value
}
render() {
const { name } = this.state;
const { time } = this.props;
return (
<div>
<h1>Hello, { name }!</h1>
<input onChange={ this.change }/>
<p>当前时间:{ time }</p>
</div>
);
}
}
3. 组件通信
React 的数据流是"单向"的,自上而下通过 props 传递,但根据通信方向与距离的不同,我们采用不同的方式。
3.1 父组件向子组件传递信息:Props
这是最基础、最常见的通信方式。父组件通过子组件的属性(props) 将数据传递下去。
- 方式:在子组件标签上写入属性。
- 子组件接收 :通过函数的参数
props
或解构赋值获取。
示例:
tsx
// 父组件 Parent.js
import Child from './Child';
function Parent() {
const parentData = "Data from Parent";
const sayHello = () => { alert('Hello from Parent!'); };
return (
<div>
{/* 传递字符串和数据函数 */}
<Child
message={parentData}
onSayHello={sayHello}
extraContent={<span>This is JSX from Parent</span>}
/>
</div>
);
}
// 子组件 Child.js
interface IChildProps {
message: string;
extraContent: string;
onSayHello?: () => void;
}
function Child(props: IChildProps) {
// 也可以使用解构: function Child({ message, onSayHello, extraContent }) { ... }
return (
<div>
<p>收到来自父组件的信息:{props.message}</p>
<button onClick={props.onSayHello}>点击我</button>
{props.extraContent}
</div>
);
}
export default Child;
3.2 子组件向父组件传递信息:回调函数
子组件不能直接修改父组件的 props。如果需要通知父组件某些事件(如表单提交、按钮点击)或传递数据,父组件可以通过 props 传递一个回调函数给子组件,子组件在适当时机调用此函数。
- 方式:父组件定义函数,通过 prop 传递给子组件 -> 子组件调用该函数并传入参数。
示例:
jsx
// 父组件 Parent.js
import { useState } from 'react';
import Child from './Child';
function Parent() {
const [messageFromChild, setMessageFromChild] = useState('');
// 定义回调函数,用于接收子组件的数据
const handleChange = (data) => {
console.log("父组件收到用户的输入数据:", data);
};
// 定义回调函数,用于接收子组件的数据
const handleDataFromChild = (data) => {
setMessageFromChild(data);
console.log("父组件收到:", data);
};
return (
<div>
<p>子组件对我说:{messageFromChild}</p>
{/* 将回调函数传递给子组件 */}
<Child onSendData={handleDataFromChild} onChange={handleChange}/>
</div>
);
}
// 子组件 Child.js
function Child({ onChange, onSendData }) {
const [count, countSet] = useState(0);
const handleChange = (event) => {
const value = event.target.value;
countSet(value)
// 调用父组件传来的函数,并传递数据
onChange(value)
};
const handleClick = () => {
// 调用父组件传来的函数,并传递数据
onSendData("Hello Parent! - From your child");
};
return (
<>
<input onChange={handleChange}/>
<button onClick={handleClick}>发送信息给父组件</button>
<>
);
}
export default Child;
3.3 多级组件跨越式传递信息:
当需要在远房组件(如孙组件、曾孙组件)之间传递数据,层层手动传递 props(称为"Prop Drilling")会非常繁琐。此时有两种主流解决方案:
3.3.1 上下文 (Context) - (React 官方推荐)
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。它创建了一个"全局"的数据域,其下的任意组件都能订阅这个数据。
- 适用场景:React组件中的多级组件之间的通信,如果涉及组件和普通js函数之间的通信用这种方式就不适合
示例:
jsx
// 1. 创建 Context (例如:ThemeContext.js)
import { createContext } from 'react';
export const ThemeContext = createContext('light'); // 'light' 为默认值
// 2. 在顶层组件提供数据 (App.js)
import { ThemeContext } from './ThemeContext';
import Toolbar from './Toolbar';
function App() {
const [theme, setTheme] = useState('dark');
return (
// 使用 Provider 提供 value,包裹需要接收数据的子组件树
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar /> {/* Toolbar 及其所有子组件都能订阅这个 Context */}
</ThemeContext.Provider>
);
}
// 3. 在深层子组件中消费数据 (ThemedButton.js)
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemedButton() {
// 使用 useContext Hook 来订阅 Context 的变化
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
style={{
background: theme === 'dark' ? '#333' : '#CCC',
color: theme === 'dark' ? 'white' : 'black'
}}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
My theme is {theme}
</button>
);
}
3.3.2 全局事件中心 (Event Bus / PubSub)
这是一种传统的设计模式,创建一个全局的中央事件管理器。组件可以"发布(Publish)"事件到中心,也可以"订阅(Subscribe)"中心的事件。彼此之间不直接通信,而是通过事件中心间接联系。
- 适用场景:非父子组件、关系非常遥远的组件间通信。在现代React开发中,通常优先考虑 Context API 或状态管理库(如 Redux),但在某些特定场景或简单应用中仍可使用。
概念性示例:
javascript
// eventBus.js (一个简单的实现)
const events = {};
const eventBus = {
// 订阅事件
on(eventName, callback) {
if (!events[eventName]) {
events[eventName] = [];
}
events[eventName].push(callback);
},
// 取消订阅
off(eventName, callback) {
if (!events[eventName]) return;
events[eventName] = events[eventName].filter(cb => cb !== callback);
},
// 发布事件
emit(eventName, data) {
if (!events[eventName]) return;
events[eventName].forEach(callback => {
callback(data);
});
}
};
export default eventBus;
jsx
// Component.js (订阅者,可以是任意位置的组件)
import { useEffect } from 'react';
import eventBus from './eventBus';
function Component() {
useEffect(() => {
// 组件挂载时订阅事件
const handleEvent = (data) => {
console.log('收到事件和数据:', data);
};
eventBus.on('myEvent', handleEvent);
// 组件卸载时取消订阅,防止内存泄漏
return () => {
eventBus.off('myEvent', handleEvent);
};
}, []);
return <div>Listening for events...</div>;
}
// Socket.js
function socket() {
const msg = {sub: "ticker", id: "123456", type:"add"}
const socket = new WebSocket('wss://somesocket.com');
socket.onmessage = (e: MessageEvent) => {
eventBus.emit('myEvent', { data: e.data });
};
socket.send(msg)
}
通信方式总结
通信方向 | 使用方式 | 适用场景 |
---|---|---|
父 -> 子 | Props | 最常用,直接的父子关系 |
子 -> 父 | 回调函数 (通过 Props) | 子组件通知父组件,传递数据 |
任意组件 | Context API | 跨多级组件传递"全局"数据(React 首选) |
任意组件、函数 | 全局事件中心 (Event Bus) | 非父子组件、极度松散的通信 |
任意组件 | 状态管理库 (Redux, Zustand) | 复杂的应用状态管理,可视为Context的增强版(我们项目中没有用到) |
4. Hooks
Hooks 是 React 16.8 版本引入的一项革命性特性。它允许你在函数组件中使用 state 以及其他 React 特性(如生命周期),从而摆脱了必须使用 class 组件的限制。
核心优势:
- 逻辑复用:解决了 Class 组件中高阶组件(HOC)和渲染属性(Render Props)带来的"嵌套地狱"问题(后续第五部分会单独讲讲),使状态逻辑的复用变得非常简单。
- 代码组织:允许将组件中相互关联的逻辑拆分成更小的函数(自定义 Hook)。
- 易于理解 :函数组件更简洁,没有复杂的
this
绑定问题。
4.1 内置 Hooks
a. useState
- 状态钩子
用于在函数组件中添加和管理局部状态。
-
用法 :
const [state, setState] = useState(initialState);
-
参数 :
initialState
是状态的初始值(可以是任意类型,函数也行)。 -
返回值: 一个包含两个元素的数组:
state
: 当前的状态值。setState
: 用于更新状态的函数,调用它会触发组件重新渲染。
-
示例:
jsximport React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); // 初始值为 0 return ( <div> <p>你点击了 {count} 次</p> <button onClick={() => setCount(count + 1)}> 点击我 </button> {/* 函数式更新,解决异步更新依赖问题 */} <button onClick={() => setCount(prevCount => prevCount - 1)}> 减少 </button> </div> ); }
b. useEffect
- 副作用钩子
用于在函数组件中执行副作用操作(数据获取、订阅、手动修改 DOM 等)。它可以看作是 componentDidMount
, componentDidUpdate
, 和 componentWillUnmount
的组合。
-
用法 :
useEffect(effectFunction, dependencyArray?)
-
参数:
effectFunction
: 包含副作用逻辑的函数。此函数可以返回一个清理函数(cleanup),用于在组件卸载或执行下一次 effect 前清除副作用(如取消订阅、清除定时器)。dependencyArray
(可选): 依赖项数组。React 会根据这个数组来决定是否重新执行 effect。
-
执行时机:
- 无依赖数组 (
useEffect(effect)
): 每次组件渲染后都会执行。 - 空依赖数组 (
useEffect(effect, [])
): 只在组件首次挂载后执行一次(类似于componentDidMount
)。 - 有依赖项 (
useEffect(effect, [state1, state2])
): 只在依赖项(state1
,state2
)发生变化时执行。
- 无依赖数组 (
-
示例:
jsximport React, { useState, useEffect } from 'react'; function UserProfile({ userId }) { const [user, setUser] = useState(null); // effect 依赖于 userId prop useEffect(() => { // 异步获取数据 fetch(`/api/user/${userId}`) .then(response => response.json()) .then(data => setUser(data)); // 返回清理函数(可选) return () => { // 这里可以取消未完成的请求(例如使用 AbortController) console.log('Cleanup for userId:', userId); }; }, [userId]); // 只有当 userId 变化时,才会重新执行 return <div>{user ? user.name : 'Loading...'}</div>; }
c. useContext
- 上下文钩子
用于接收一个 Context 对象(由 React.createContext
创建)并返回该 Context 的当前值。让你无需组件嵌套即可订阅 React 的 Context。
-
用法 :
const value = useContext(MyContext);
-
示例 :
jsx// 1. 创建 Context const ThemeContext = React.createContext('light'); function App() { // 2. 使用 Provider 提供值 return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } function Toolbar() { // 中间的组件无需再传递 theme prop return <ThemedButton />; } function ThemedButton() { // 3. 在子组件中使用 useContext 获取值 const theme = useContext(ThemeContext); return <button className={theme}>我是 {theme} 主题的按钮</button>; }
d. useCallback
& useMemo
- 性能优化钩子
用于避免不必要的重复渲染和计算,优化性能。
-
useCallback
: 缓存一个函数本身。- 问题: 父组件重新渲染时,其内部定义的函数会重新创建,导致接收该函数作为 prop 的子组件不必要的重渲染。
- 解决 :
useCallback(fn, deps)
返回一个记忆化的回调函数,只在依赖项deps
变化时才会更新。
jsxconst memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]); // 只有当 a 或 b 变化时,函数才会重新创建
-
useMemo
: 缓存一个计算值。- 问题: 每次渲染时都要进行昂贵的计算(如过滤大型数组)。
- 解决 :
useMemo(() => computeExpensiveValue(a, b), [a, b])
返回一个记忆化的值,只在依赖项变化时重新计算。
jsxconst expensiveValue = useMemo(() => { return someExpensiveCalculation(a, b); }, [a, b]); // 只有当 a 或 b 变化时,计算才会重新执行
e. useRef
用于储存一个不会随着函数组件刷新而重新定义、赋值的数据
4.2 自定义 Hooks
自定义 Hook 是一个以 use
开头的 JavaScript 函数,它内部可以调用其他的 Hook。它的目的是将组件逻辑提取到可重用的函数中,所以当你发现一些逻辑在多个组件中重复出现时,就可以考虑将其提取为自定义 Hook。
核心规则:
- 函数名必须以
use
开头。 - 自定义 Hook 内部可以调用其他 Hook。
- 两个不同的组件使用相同的自定义 Hook 不会共享 state。每次调用都有自己的独立状态。
示例 :我们去看下React官方的例子:zh-hans.react.dev/learn/reusi...
5. 拓展
这部分咱们主要说说为什么推荐函数组件,而不是类组件,主要有以下几个原因:
5.1 更好的逻辑复用
5.2 无需理解 JavaScript 的 this 机制
在JS中this
和其他语言有很大的不同:this
的值取决于函数被调用的方式,而不是定义的方式. 而类组件严重依赖this
,这就带来了几个经典难题:
- a. 事件处理函数的绑定问题
- b.
this
在生命周期方法中的不一致性
jsx
// 示例地址: https://codesandbox.io/p/sandbox/stoic-grass-8c9sfm?file=%2Fsrc%2FApp.js%3A6%2C25-7%2C22
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
// 解决: 在构造函数中手动绑定
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 如果不绑定,这里的 `this` 将是 undefined,导致 Cannot read properties of undefined (reading 'setState') 的错误;
this.setState(prevState => ({
count: prevState.count + 1
}));
}
componentDidMount() {
// 这里的 `this`指向组件实例,
console.log('this', this);
setTimeout(function() {
console.log('setTimeout this', this);
}, 1000)
}
render() {
return (
<button onClick={this.handleClick}>
Count: {this.state.count}
</button>
);
}
}
而在函数组件中根本就没有this
所以天然就不会有上面说的问题
jsx
function MyComponent() {
const [count, setCount] = useState(0);
// 直接声明一个函数。没有 `this`,没有绑定。
const handleClick = () => {
// 直接使用 `setCount`,它来自闭包,永远指向正确的函数
setCount(prevCount => prevCount + 1);
};
return (
<button onClick={handleClick}>
Count: {count}
</button>
);
}