React 核心概念与基础语法
本页关键词:JSX/TSX、useState、useEffect、组件即函数、UI = f(state)、依赖数组、StrictMode
一、React 核心思想
React 的核心公式:
UI = f(state)
界面是状态的函数。与 Vue 依赖模板语法和响应式追踪不同,React 更直接——一切都是 JS。
| 对比项 | Vue | React |
|---|---|---|
| 模板 | .vue 单文件模板语法 | JSX(本质是 JS) |
| 响应式 | 自动追踪依赖 | state 变化 → 函数重新执行 → diff |
| 更新粒度 | 组件级精确更新 | 父组件 re-render → 子组件全部 re-render |
面试要点:React 没有"模板响应式追踪",它的更新机制是:重新执行组件函数 → 生成新的虚拟 DOM → diff 比较 → 最小化更新真实 DOM。
二、JSX 与 TSX
JSX 本质
JSX 不是模板,它是 React.createElement 的语法糖:
// JSX 写法
<h1>Hello React</h1>
// 编译后等价于
React.createElement("h1", null, "Hello React")JSX 与 TSX 的区别
.jsx= JavaScript + JSX 语法.tsx= TypeScript + JSX 语法
TSX 只是多了类型能力,其他完全一样:
// jsx
export default function App() {
return <h1>Hello React</h1>
}
// tsx(可以标注返回类型)
export default function App(): JSX.Element {
return <h1>Hello React</h1>
}JSX 不是 React 专属,它只是语法扩展。但当前前端岗位默认使用 React + TypeScript,所以统一用 .tsx。
JSX 表达式插槽 {}
JSX 中的 {} 类似 Vue 模板的 ,可以放任何 JS 表达式:
| 表达式结果 | 渲染行为 |
|---|---|
| 字符串 / 数字 | 渲染为文本 |
| JSX 元素 | 渲染组件/标签 |
| JSX 元素数组 | 依次渲染每个元素 |
null / undefined / false | 不渲染 |
条件渲染与列表渲染
React 没有 v-if、v-for,全部用原生 JS 代替:
// 条件渲染:三元表达式(对应 Vue 的 v-if)
{todos.length === 0 ? <p>暂无数据</p> : <ul>...</ul>}
// 列表渲染:map()(对应 Vue 的 v-for)
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}map 是 JavaScript 原生数组方法,不是 React 的 API。它遍历数组,对每个元素执行回调,返回一个新数组。在 JSX 中,map 返回的 JSX 元素数组会被 React 依次渲染。
面试要点:JSX 的
{}里放的是 JS 表达式,不是模板语法。React 用三元表达式替代v-if,用map替代v-for,用&&替代v-show。
三、useState —— 状态管理基础
useState 是 React 最核心的 Hook,用于在函数组件中声明响应式状态。
import { useState } from "react"
export default function App() {
const [count, setCount] = useState(0)
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}核心机制
- state 改变 → 组件函数重新执行(不是局部更新,是整个函数重新跑一遍)
- 组件就是函数,每次 render 都是函数重新执行
- React 通过 diff 算法只更新真正变化的 DOM 部分
验证方式:在组件函数体内(return 之前)加 console.log("render"),每次 state 变化都会打印。
与 Vue 的对比
// Vue
const input = ref("")
input.value = "你好" // 直接通过 .value 修改
// React
const [input, setInput] = useState("")
setInput("你好") // 必须通过 setter 函数修改,不能直接赋值不可变更新原则
React 要求不能直接修改 state,必须创建新值:
// ✗ 错误:直接 push
todos.push("新任务")
// ✓ 正确:创建新数组
setTodos([...todos, "新任务"])面试要点:React 的 state 更新是不可变的(immutable),必须通过 setter 函数传入新值。直接修改原对象/数组不会触发 re-render。
四、useEffect —— 副作用处理
useEffect 用于处理副作用(数据请求、DOM 操作、订阅等),在 render 之后执行。
import { useState, useEffect } from "react"
export default function App() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log("count changed:", count)
}, [count])
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
)
}依赖数组的三种形态
| 写法 | 执行时机 | 等价含义 |
|---|---|---|
useEffect(fn) | 每次 render 后都执行 | 无限制 |
useEffect(fn, []) | 仅首次 render 后执行一次 | 组件挂载(mounted) |
useEffect(fn, [count]) | 首次 + count 变化时执行 | 监听特定依赖 |
依赖数组写错会导致无限循环,这是 React 开发中最常见的坑之一。
面试要点:
useEffect在 render 之后执行,依赖数组控制触发时机。空数组[]等价于onMounted,不传数组等价于每次 render 后都执行。
五、受控组件
React 中表单元素的值由 state 控制,通过 onChange 更新,称为受控组件:
const [input, setInput] = useState("")
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="输入内容"
/>这和 Vue 的 v-model 是同一个概念,但 React 需要手动绑定 value 和 onChange。
面试要点:受控组件的值完全由 React state 驱动,每次输入都会触发
onChange→setState→ re-render。与之对应的是非受控组件(通过ref直接访问 DOM)。
六、key 的作用
列表渲染时,每个元素必须有 key,帮助 React 在 diff 时精确识别元素:
没有 key:React 按顺序对比,删除中间元素时会错误地更新后续元素的内容。
有 key:React 通过 key 精确匹配,直接删除目标 DOM 节点,其他不动。
为什么不建议用 index 做 key
当列表发生增删时,index 会重新分配,导致 key 与元素的对应关系变化,React 无法正确复用 DOM。
正确做法是使用唯一且稳定的 id:
const [todos, setTodos] = useState<{ id: number; text: string }[]>([])
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}面试要点:key 帮助 React 在 diff 时识别哪些元素变了。用 index 做 key 在列表增删时会导致错误复用,应使用稳定唯一的 id。
七、StrictMode 双重渲染
React 18 的 <StrictMode> 在开发模式下会故意对组件进行双重渲染,用于检测:
- 不纯的渲染逻辑 —— 组件函数应该是纯函数
- 遗漏的副作用清理 —— 如
useEffect中忘记 return 清理函数
// main.tsx
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)生产构建不会双重渲染,不影响性能。 开发时看到 console.log 打印两次是正常现象。
八、闭包与 React
在 React 中,组件函数内部的所有函数都是闭包。
function createGreeter(name) {
return function() {
console.log("你好," + name)
}
}
const greetZhang = createGreeter("张三")
greetZhang() // "你好,张三"
// createGreeter 已执行完毕,但返回的函数仍能访问 name闭包的定义:函数会"记住"它被创建时所在作用域的变量,即使那个作用域已经执行完毕。
在 map 循环中,每次迭代创建的箭头函数都通过闭包捕获了当时的 index 值:
{todos.map((todo, index) => (
<li key={index} onClick={() => removeTodo(index)}>
{/* 每个函数捕获了不同的 index */}
{todo}
</li>
))}面试要点:闭包在 React 中的实际影响——函数捕获的是创建时的值,不是最新的值。这就是为什么有时需要
useCallback和函数式更新(setState(prev => ...))。