Part 1 React 基础
约 2204 字大约 7 分钟
2025-06-30
1 React 项目
1.1 创建一个 React 项目
这里我们使用 Vite 创建 React 项目。
npm create vite@latest my-app -- --template react
或者参考官网的其他创建方式。
1.2 React 项目结构
React 项目结构与 Vue 项目结构类似:
public
vite.svg
...
src
App.tsx
main.tsx
...
index.html
package.json
vite.config.ts
...
类似于 Vue 的单文件组件,React 的组件都写在jsx
或tsx
文件中。
项目的入口是main.tsx
文件,默认状态下,它是这样的:
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import "./index.css"
import App from "./App.tsx"
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
)
在main.tsx
中引入了两个关键的函数createRoot
和StrictMode
。createRoot
创建了一个根组件实例,并调用该实例的render
函数进行根组件的渲染。
<StrictMode>
代表启用了 React 严格模式。
1.3 函数式组件
Vite 默认提供了一个App.tsx
,我们以此分析 React 的函数式组件。
import { useState } from "react"
import reactLogo from "./assets/react.svg"
import viteLogo from "/vite.svg"
import "./App.css"
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount(count => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App
通读代码,发现整个App.tsx
实质上只导出了一个函数,这个函数的返回值是一段 HTML 代码(事实上是 JSX 代码)。这段 JSX 代码就定义了一个 React 组件。
组件中定义了一个变量count
和一个函数setCount
。他们使用useState()
定义。useState()
是 React 提供的一个方法,其用处我们将在后面讲解。
阅读返回值,注意这一段:
<button onClick={() => setCount(count => count + 1)}>count is {count}</button>
显然,在这个按钮上,onClick
定义了一个点击事件,使得每次点击按钮都会调用setCount
方法。
而变量count
使用单大括号包裹,实时响应数据。
2 JSX/TSX 语法
SX/TSX 使用函数定义组件,极大地提高了项目的灵活性。JSX/TSX 的本质是语法糖,编译后会被转成React.createElement()
调用。
2.1 插值语法
正如 Vue 中的插值语法,React 插值语法也是绑定变量和 HTML 的重要工具。
在 JSX/TSX 中,使用{}
可以嵌入任何 JavaScript 表达式。
function App() {
const title: string = "Hello React"
const description: string = "这是一个 React 应用"
return (
<div>
<h1>{title}</h1>
<p>{description}</p>
</div>
)
}
export default App
插值语法也可以应用在标签属性上。
function App() {
const imgURL: string = "https://reactjs.org/logo-og.png"
return (
<div>
<img src={imgURL} alt="React Logo" />
</div>
)
}
export default App
需要注意的是,在{}
中只能写 JS 表达式,不能写for
if
等语句。如果想控制元素的渲染,应该使用
2.2 标签语法
2.2.1 标签必须闭合
return <input> // ERROR: JSX 元素“input”没有相应的结束标记。
return <input />
2.2.2 空标签
函数返回的 JSX/TSX 表达式只能包含一个元素。下边的写法是不行的:
return (
<h1>{title}</h1>
<p>{description}</p>
) // ERROR: JSX 表达式必须具有一个父元素。
为解决这个问题,React 提供了空标签<>
。
return (
<>
<h1>{title}</h1>
<p>{description}</p>
</>
)
2.2.3 CSS 类名
由于class
是 JavaScript/TypeScript 的保留字,React 使用className
来指定 CSS 类名。
return (
<>
<h1 className="title">{title}</h1>
<p className="description">{description}</p>
</>
)
2.2.4 内联样式
内联样式应写为对象形式,使用双大括号包裹。外层大括号表明这是插值语法,内部是一个 JS 表达式。内层大括号为一个 JS 对象字面量。
return <div style={{ color: "red", fontSize: "16px" }}>Styled Text</div>
3 数据渲染
渲染并展示数据是前端开发的本职工作。这其中包括带有可见性的条件渲染和带有顺序的列表渲染。
3.1 条件渲染
我们可以使用多种方式控制元素的可见性。例如对于一项数据:
const me: {
name: string
age: number
} = {
name: "YOAKE",
age: 18,
}
和两个元素:
<div>{me.name} 已经合法了</div>
<div>{me.name} 还不合法</div>
借助 JS/TS 语句,可以很轻松地完成这个效果:
return (
<div>
{me.name} {me.age >= 18 ? "已经合法了" : "还不合法"}
</div>
)
这是使用三元运算符实现的。还可以使用if
语句进行实现:
if (me.age >= 18) {
return <div>{me.name} 已经合法了</div>
}
return <div>{me.name} 还不合法</div>
以及短路运算符:
return (
<div>
{me.name}
{me.age >= 18 && "已经合法了"}
{me.age < 18 && "还不合法"}
</div>
)
此处即可窥得 JSX/TSX 的高度灵活性。
3.2 列表渲染
对于一组数据:
const list: {
id: number
name: string
}[] = [
{ id: 1, name: "YOAKE" },
{ id: 2, name: "AJohn" },
{ id: 3, name: "Zephyr" },
]
通常使用map
进行遍历:
return (
<ul>
{list.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
li
标签上的key
通常使用唯一的专属 ID,如果实在没有,可以使用index
:
return (
<ul>
{list.map((item, index) => (
<li key={index}>{item.name}</li>
))}
</ul>
)
当然也不止一种书写方式:
const content = list.map(item => {
return <li key={item.id}>{item.name}</li>
})
return <ul>{content}</ul>
4 事件
4.1 监听事件
React 要监听某一事件非常简单。
function App() {
const handleClick = (): void => {
alert("按钮被点击了")
}
return <button onClick={handleClick}>点击我</button>
}
事件处理函数默认接收 React 事件作为第一个参数。React 使用SyntheticEvent
对浏览器原生事件进行封装,形成统一接口。要访问原生事件,只需要访问nativeEvent
属性。
function App() {
const handleClick = (e: React.SyntheticEvent): void => {
console.log(e) // SyntheticBaseEvent
console.log(e.nativeEvent) // PointerEvent
}
return <button onClick={handleClick}>点击我</button>
}
4.2 控制事件传播
React 提供了和 DOM 元素类似的方法控制事件传播。
function App() {
const handleClick = (e: React.SyntheticEvent): void => {
e.preventDefault() // 阻止默认行为
e.stopPropagation() // 阻止事件冒泡
}
return (
<a href="https://www.google.com" onClick={handleClick}>
点击不会跳转
</a>
)
}
4.3 事件类型
React 支持的事件类型包括但不限于:
鼠标事件:
onClick
onDoubleClick
onMouseEnter
onMouseLeave
表单事件:
onChange
onSubmit
onFocus
onBlur
键盘事件:
onKeyDown
onKeyUp
onKeyPress
触摸事件:
onTouchStart
onTouchMove
onTouchEnd
拖拽事件:
onDrag
onDragOver
onDrop
其他:
onWheel
onScroll
onInput
5 状态
React 是函数式编程,而由于函数式编程无副作用的特点,React 组件是无法保存状态的。
例如:
function App() {
let title: string = "示例标题"
const handleClick = () => {
title = "标题已更改"
}
return (
<>
<h3>{title}</h3>
<button onClick={handleClick}>点击更改标题</button>
</>
)
}
预期中组件的行为应该是:点击按钮,然后标题被更改。而当我们真正点击按钮时却什么都没有发生,即使按钮中的语句确实被执行了。
这是由于 React 并没有提供像 Vue 一样的响应式机制,声明的title
只是一个普通变量,并不会影响到 DOM。
为了解决这个问题,React 提供了useState
方法。
useState
是一个函数,它接收一个 JS 字面量作为参数,也是数据的初始内容。useState
返回一个数组,第一个元素是对初始内容的引用,或者称为state
,第二个元素是一个修改内容的函数,或者称为setState
。
5.1 简单数据类型状态
setState
可以接收一个 JS 字面量,直接赋值给 state。
function App() {
const [title, setTitle] = useState("初始标题")
const handleClick = () => {
setTitle("标题已更改")
}
return (
<>
<h3>{title}</h3>
<button onClick={handleClick}>点击更改标题</button>
</>
)
}
也可以接收一个函数,用于实现更复杂的逻辑。这个函数默认接收“前一个 state”作为第一个参数。
function App() {
const [count, setCount] = useState(1)
const handleClick = () => {
setCount(prevCount => prevCount + 1)
}
return (
<>
<h3>{count}</h3>
<button onClick={handleClick}>Click</button>
</>
)
}
5.2 引用数据类型状态
5.2.1 对象
对于一个引用数据类型而言,修改其状态容易陷入这样的陷阱:
function App() {
const [info, setInfo] = useState({
name: "YOAKE",
age: 18,
})
const handleClick = () => {
setInfo(prevInfo => ({
age: prevInfo.age + 1,
}))
} // 事实上,在 TSX 中,这一行连编译都过不去
return (
<>
<div>
{info.name}今年{info.age}岁了
</div>
<button onClick={handleClick}>年龄+1</button>
</>
)
}
当我们点击按钮时,会发现 DOM 中的info.name
消失了。这说明setState
的直接赋值方式对于引用数据类型是整体替换,而非我们预期中的更新。
要实现预期行为,需要重写所有属性。当然我们可以借助展开运算符,以避免重写所有属性:
setInfo(prevInfo => ({
...prevInfo,
age: prevInfo.age + 1,
}))
5.2.2 数组
对于数组而言,展开运算符也经常出现:
function App() {
const [list, setList] = useState([
{ id: 1, name: "YOAKE" },
{ id: 2, name: "AJohn" },
{ id: 3, name: "Zephyr" },
])
const handleClick = () => {
setList([...list, { id: 4, name: "清歌" }])
}
return (
<>
<ul>
{list.map(item => (
<li key={item.id}>
{item.id} - {item.name}
</li>
))}
</ul>
<button onClick={handleClick}>点击加一个人</button>
</>
)
}
当然可以调整位置,将其添加到列表首位:
const handleClick = () => {
setList([{ id: 4, name: "清歌" }, ...list])
}
结合数组方法,我们可以添加进任何位置,虽然略显麻烦就是了。