一.JSX

1.认识JSX

JSX 是 JavaScript 的语法扩展,让你能在 JS 中写类似 HTML 的代码。

本质React.createElement() 的语法糖

我们对比一下jsx和原生js

// 用 JSX(清晰)
const page = (
  <div>
    <h1>标题</h1>
    <ul>
      {list.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  </div>
);

// 不用 JSX(噩梦,千万别这样写)
const page = React.createElement(
  'div',
  null,
  React.createElement('h1', null, '标题'),
  React.createElement(
    'ul',
    null,
    list.map(item => React.createElement('li', { key: item.id }, item.name))
  )
);

优势
说明
声明式
一眼看出 UI 结构,像 HTML 一样直观
开发效率
不用手写嵌套的 createElement,少写很多代码
错误提示
编译时就能发现语法错误(比如标签没闭合)

2.JSX的核心规则

2.1.只能返回一个根元素

// ❌ 错误
return (
  <h1>标题</h1>
  <p>内容</p>
)

// ✅ 正确:用 div 包裹
return (
  <div>
    <h1>标题</h1>
    <p>内容</p>
  </div>
)

// ✅ 正确:用 Fragment(不产生额外 DOM 节点)
return (
  <>
    <h1>标题</h1>
    <p>内容</p>
  </>
)

2.2所有标签必须闭合

// ❌ 错误
<img src="xxx">
<input type="text">

// ✅ 正确
<img src="xxx" />
<input type="text" />
<div></div>  // 双标签也要闭合

2.3. 属性名用驼峰命名

2.4

HTML 写法

JSX 写法

class

className

tabindex

tabIndex

onclick

onClick

2.4. 嵌入 JS 表达式用 {}

const name = '小明';
const isLogin = true;

return (
  <div>
    <h1>你好,{name}</h1>           {/* 变量 */}
    <p>{isLogin ? '已登录' : '未登录'}</p>  {/* 三元表达式 */}
    <p>{1 + 2 + 3}</p>              {/* 计算 */}
  </div>
);

{} 里能放什么样的值?

任何 JS 表达式:变量、函数调用、计算、三元、map 等(有返回值)。不能放 if/for/while 等语句。

3.为什么 JSX 里要用 className 而不是 class

因为 JSX 本质是 JS,class 是 JS 的保留关键字(定义类的),所以 React 团队选了 className

二.组件

2.1组件是什么?

组件 = 一个函数或类,接收输入(props),返回 JSX(描述 UI)。

2.2组件两种形式

2.2.1函数组件

// 函数组件:就是一个返回 JSX 的函数
function Greeting(props) {
  return <h1>你好,{props.name}</h1>;
}

// 箭头函数写法
const Greeting = (props) => {
  return <h1>你好,{props.name}</h1>;
};

除此之外还有类组件

面试一句话总结

函数组件更轻量、更函数式,配合 Hooks 能实现类组件的全部能力。React 官方推荐新项目使用函数组件 + Hooks。

2.3组件核心概念

2.3.1Props(父传子,只读)

// 父组件
function Parent() {
  return <Child name="小明" age={18} />;
}

// 子组件接收 props
function Child(props) {
  return <div>{props.name},{props.age}岁</div>;
}

// 解构写法(推荐)
function Child({ name, age }) {
  return <div>{name},{age}岁</div>;
}

Props 核心规则:

  • 只读:子组件不能修改 props

  • 任何类型都能传:字符串、数字、对象、数组、函数、JSX

2.3.2. State(组件内部数据,可变)

// 函数组件用 useState
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

2.3.3.Children(组件嵌套)

function Card({ children, title }) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <div>{children}</div>
    </div>
  );
}

// 使用
<Card title="温馨提示">
  <p>这是一段内容</p>
  <button>确定</button>
</Card>

2.4.组件的生命周期(理解核心,面试常考)

React 在每个阶段都提供了一些“钩子”,让你可以插入自己的代码。这些钩子就叫生命周期方法

2.4.1为什么需要生命周期?

因为你想在特定时刻做特定的事:

  • 出生时:发请求拿数据、启动定时器、连接 websocket

  • 更新时:根据新的 props 重新计算某些值、打印日志

  • 死亡时:清理定时器、取消请求、断开连接(防止内存泄漏)

如果没有生命周期,你很难控制这些时机。

2.4.2类组件的生命周期(最经典)但是是早期类组件时代概念,我们这里用函数组件,所以不进行赘述

2.4.3函数组件中生命周期被useEffect模拟

2.4.3.1. 组件第一次出现时(挂载)

useEffect(() => {
  console.log('组件出现了,只执行一次');
  // 在这里做初始化工作
}, []);

那个 [] 就是“空数组”,意思是不依赖任何数据,所以只执行一次。

2.4.3.2 组件因为数据变化重新显示时(更新)

useEffect(() => {
  console.log('messages 变了,组件更新了');
  // 滚动到底部
  scrollToBottom();
}, [messages]);  // 依赖 messages,只有 messages 变化才执行

2.4.3.3 组件从页面上消失时(卸载)

useEffect(() => {
  const timer = setInterval(() => {}, 1000);
  
  // 这个函数会在组件消失前执行
  return () => {
    clearInterval(timer);
    console.log('组件消失了,清理工作');
  };
}, []);

关键: return 一个函数,这个函数就是“清理函数”,在组件消失前自动调用。

总结

你想做什么

写法

组件出现时做一次

useEffect(() => { ... }, [])

某个数据变化时做

useEffect(() => { ... }, [那个数据])

组件消失前做清理

useEffect(() => { return () => { ... } }, [])

三.State

State 是组件内部的数据,当 state 变化时,组件会重新渲染(函数重新执行,UI 更新)。

3.1更新State的两种方式

3.1.1直接传值

setCount(count + 1);

3.1.2传入函数—— 当新值依赖于旧值时推荐

setCount(prevCount => prevCount + 1);

为什么要用函数式更新?
如果连续调用多次,直接传值可能会因为异步批量更新导致覆盖。函数式更新能确保拿到的是上一次最新的值

// ❌ 点击一次,count 只增加 1
setCount(count + 1);
setCount(count + 1);

// ✅ 点击一次,count 增加 2
setCount(prev => prev + 1);
setCount(prev => prev + 1);

3.2.State异步更新

在 React 事件处理函数中,setState 是异步的,不会立刻更新 state,而是会批量处理

function handleClick() {
  setCount(count + 1);
  console.log(count);  // 还是旧值,不会立刻变成新值
}

React 为了性能,会把多个 setState 合并成一次更新,避免频繁渲染。

面试回答:setState 在事件处理中是异步的,如果你需要在更新后立即获取新值,可以用 useEffect 监听那个 state。

3.3对象类型State

const [user, setUser] = useState({ name: '小明', age: 18 });

// 更新时:必须替换整个对象,不能直接修改
setUser({ ...user, age: 19 });   // ✅ 正确:展开旧对象,覆盖 age
// 错误:setUser({ age: 19 })   // ❌ 会丢失 name

核心规则

  • 不能直接修改原对象user.age = 19 无效,React 不会渲染)

  • 必须创建一个新对象传给 setter

3.4数组类型State

const [list, setList] = useState(['a', 'b']);

// 添加
setList([...list, 'c']);

// 删除(filter)
setList(list.filter(item => item !== 'b'));

// 修改某个元素(map)
setList(list.map(item => item === 'a' ? 'A' : item));

永远不要用 pushpopsplice 直接修改原数组,要用 [...]filtermap 返回新数组。

3.5.State的不可变原则

一句话:永远不要直接修改 state 的值,必须用 setter 函数传入一个新的值

// ❌ 错误
state.count = 1;
setState(state);   // 不会触发渲染

// ✅ 正确
setState({ ...state, count: 1 });

为什么?
React 通过浅比较判断 state 是否变化。如果直接修改原对象,引用不变,React 认为没变,就不渲染。

3.6.State提升(Lifting State Up)

当两个组件需要共享同一份 state 时,把 state 放到它们的共同父组件中,通过 props 传递下去。

function Parent() {
  const [value, setValue] = useState('');
  return (
    <>
      <ChildA value={value} setValue={setValue} />
      <ChildB value={value} />
    </>
  );
}

四.Props

Props 是父组件传递给子组件的数据,全称 Properties

  • 单向数据流:父 → 子,只能从上往下传

  • 只读:子组件不能修改 props(改了也没用,父组件传什么就是什么)

// 父组件传递
<Child name="小明" age={18} />

// 子组件接收
function Child({ name, age }) {
  return <div>{name}, {age}岁</div>;
}

4.1.Props传递方式

4.1.1. 传递各种数据类型

<Child 
  name="字符串" 
  age={18}               // 数字(要花括号)
  isLogin={true}         // 布尔
  user={{ id: 1 }}       // 对象(双花括号)
  onEvent={() => {}}     // 函数
  list={[1, 2, 3]}       // 数组
/>

4.1.2. 传递 JSX(children 特殊 prop)

function Card({ children }) {
  return <div className="card">{children}</div>;
}

<Card>
  <h3>标题</h3>
  <p>内容</p>
</Card>

这里的 <h3> 和 <p> 会作为 children 传递给 Card 组件。

4.2.子组件接收 Props 的几种写法

// 写法1:直接接收 props 对象
function Child(props) {
  return <div>{props.name}</div>;
}

// 写法2:解构(最推荐,清晰)
function Child({ name, age }) {
  return <div>{name}, {age}</div>;
}

// 写法3:解构 + 默认值
function Child({ name = '匿名', age = 0 }) {
  return <div>{name}, {age}</div>;
}

4.3.Props不可变

function Child({ count, setCount }) {
  // ❌ 错误:不能这样改
  count = count + 1;
  
  // ✅ 正确:如果父组件传了 setCount 函数,子组件可以调用它
  // 这叫“子传父”模式:父传函数给子,子调用函数并传参数
  return <button onClick={() => setCount(count + 1)}>+1</button>;
}

面试回答:Props 是只读的,React 要求组件像纯函数一样,不能修改入参。如果要改变数据,应该通过父组件传递的更新函数来实现,即“状态提升”。

4.4子传父

因为数据只能父传子,子想传数据给父,需要通过父传给子的函数来实现。

function Parent() {
  const [msg, setMsg] = useState('');
  
  const handleChildMsg = (childData) => {
    setMsg(childData);
  };
  
  return (
    <div>
      <Child onSend={handleChildMsg} />
      <p>来自子组件:{msg}</p>
    </div>
  );
}

function Child({ onSend }) {
  return (
    <button onClick={() => onSend('Hello 父组件')}>
      发送给父
    </button>
  );
}

流程:父组件定义 handleChildMsg → 传给 Child 的 onSend prop → 子组件点击时调用 onSend('数据') → 父组件的 setMsg 执行 → 数据更新,UI 变化。

4.5Props 和 State 的区别

维度PropsState
数据来源父组件传入组件内部自己定义
可变性只读,不能修改可用 setter 修改
谁控制父组件控制组件自己控制
变化后子组件重新渲染组件重新渲染
使用场景组件配置、父传子数据组件内部动态数据

一句话:Props 是外面给的,State 是自己管的。

五.渲染流程

5.1什么是虚拟 DOM?

虚拟 DOM = 一个普通的 JS 对象,用来描述真实 DOM 的结构。

例如,这段 JSX:

<div className="box" id="main">
  <span>Hello</span>
</div>

对应的虚拟 DOM(简化版)长这样

{
  type: 'div',
  props: {
    className: 'box',
    id: 'main',
    children: {
      type: 'span',
      props: { children: 'Hello' }
    }
  }
}

特点

  • 轻量(没有真实 DOM 几百个属性)

  • 可以快速创建、销毁、比较

  • 跨平台(因为只是 JS 对象)

5.2完整渲染流程

当组件的 state 或 props 发生变化时,React 会执行以下流程:

步骤名称做什么是否操作真实 DOM
1触发state/props 变化,标记组件需要更新
2渲染(Render)执行组件函数,生成新的虚拟 DOM 树
3比对(Diff)比较新旧两棵虚拟 DOM 树,找出差异(变更集)
4提交(Commit)将差异一次性更新到真实 DOM

关键点:前三步都不碰真实 DOM,只在最后一步批量更新。这就是 React 性能优化的基础。

5.3Diff算法

Diff 算法的任务:快速找出新旧两棵虚拟 DOM 树的不同之处

React 用了三个简单的假设来让 Diff 变得很快:

  1. 不同标签名 = 不同结构(比如 div 变成 p,直接删掉旧的,建新的)

  2. 同一层级比较,不会跨层移动(只比较父节点下的同一层子节点)

  3. 列表元素靠 key 区分(告诉 React 哪个节点是同一个)

具体规则(背这三句就够了):

  • 根节点标签变了:整个组件销毁重建(包括它的所有子节点)。

  • 根节点标签没变:只更新变化的属性(比如 className),然后继续比较子节点。

  • 子节点是列表:React 按顺序比较,但有 key 的话,可以准确匹配新旧节点,避免不必要的重建。

5.4渲染、虚拟 DOM、Diff 的关系

state / props 变化
      ↓
【触发】标记需要更新
      ↓
【渲染】执行组件 → 生成新虚拟 DOM 树
      ↓
【Diff】比较新树和旧树(同层比较 + key) → 得到变更集合
      ↓
【提交】将变更集合应用到真实 DOM

一句话总结

虚拟 DOM 是用 JS 对象模拟真实 DOM;React 通过 Diff 算法比较新旧虚拟 DOM 树,找出最小变更集,最后批量更新真实 DOM,从而提升性能。

为什么虚拟 DOM + Diff 能提升性能?

  • 把多次变化合并成一次真实 DOM 更新。

  • 尽可能复用已有的 DOM 节点,只更新必要部分。

  • 开发者不用手动优化 DOM 操作,React 帮你做。

六.Hooks

6.1useState(高频,但容易答漏,前面已经总结过)

Q1:setCount 是同步还是异步?

在 React 事件处理中是异步(批量更新);在 setTimeout、原生事件中同步(但不要依赖这种差异,统一当作异步写)。

Q2:函数式更新是什么?为什么需要?

setCount(prev => prev + 1) 可以拿到上一次的最新值,避免因批量更新导致覆盖。

Q3:state 是对象/数组时如何正确更新?

必须创建新对象/新数组,不能直接修改原对象。
setUser({ ...user, age: 19 })setList([...list, newItem])

Q4:多个 state 还是单个 state 对象?

经常一起更新用对象;独立更新用多个 useState

6.2useEffect(前面已说)

6.3useRef

这是什么?

你可以把 useRef 想象成一个抽屉

  • 这个抽屉里可以放任何东西(数字、字符串、DOM 元素、对象……)

  • 关键特点:你往抽屉里换东西,React 不会重新刷页面(不会重新渲染)

  • 而 useState 里的东西一变,页面就会重新渲染

什么时候用?

  1. 想操作真实的 DOM 元素(比如让输入框自动获得焦点、播放视频、测量高度)

  2. 想存一个值,但它的变化不需要让页面重新刷新(比如定时器的 id、上一次的滚动位置)

怎么用?(代码示例)

案例1:让输入框自动获得焦点
import { useRef, useEffect } from 'react';

function MyInput() {
  const inputRef = useRef(null);   // 创建一个抽屉,起名叫 inputRef

  useEffect(() => {
    // 组件第一次显示后,让输入框获得焦点
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} />;  // 把这个输入框和抽屉关联起来
}

解释:ref={inputRef} 把真实 DOM 元素放进抽屉,之后就可以用 inputRef.current 操作它。

案例2:存定时器 id,避免重新渲染
function Timer() {
  const timerRef = useRef(null);   // 抽屉专门放定时器id
  const [count, setCount] = useState(0);

  function start() {
    timerRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  }

  function stop() {
    clearInterval(timerRef.current);
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </div>
  );
}

解释:timerRef 里的 id 变了,页面不会重新渲染,这正好符合需求(我们只想让 count 变化时渲染)。

和 useState 的区别一句话

useState 变了 → 页面刷新;useRef 变了 → 页面不刷新。

6.4.useContext

这是什么?

解决传东西传得很累的问题。

假设你有一个爷爷组件,他想给曾孙组件一个玩具。如果用 props:

  • 爷爷 → 爸爸 → 儿子 → 孙子 → 曾孙
    每一层都要手动传递,中间的人即使不需要这个玩具也要帮忙传。很烦。

useContext 相当于一个公共的快递柜

  • 爷爷把玩具放进快递柜(Provider)

  • 曾孙直接去快递柜取(useContext),不需要经过中间的人

什么时候用?

当很多层都要用同一个数据(比如主题颜色、当前登录的用户、语言设置),而且中间很多组件不需要知道这个数据时。

怎么用?(三步)

  1. 创建快递柜(Context)

  2. 在爷爷那里提供数据(用 Provider 包裹)

  3. 在曾孙那里取数据(调用 useContext)

完整示例:主题切换
import { createContext, useContext, useState } from 'react';

// 1. 创建快递柜(Context)
const ThemeContext = createContext();

function Grandpa() {
  const [theme, setTheme] = useState('light');

  // 2. 把数据和改数据的方法放进快递柜,包裹下面的所有组件
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Father />
    </ThemeContext.Provider>
  );
}

function Father() {
  return <Son />;
}

function Son() {
  return <GrandSon />;
}

function GrandSon() {
  // 3. 曾孙直接从快递柜拿数据,不需要爷爷一层层传
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <div style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
      <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
        切换主题
      </button>
    </div>
  );
}

解释:爷爷的 Provider 把 { theme, setTheme } 放进公共区域,曾孙直接用 useContext 取出来用。中间的父亲、儿子完全不需要知道这件事。

注意

  • 如果快递柜里的值变了(比如 theme 从 light 变 dark),所有取数据的组件都会重新渲染。

  • 适用于中等规模、不频繁变化的数据。非常频繁变化的大应用建议用 Zustand/Redux。

6.5useReducer

这是什么?

useReducer 是 useState 的升级版,用来管理比较复杂的状态

什么时候觉得 useState 麻烦?

  • 你有好几个相关的状态,比如一个表单有姓名、年龄、邮箱,而且更新规则复杂(比如年龄不能小于0,邮箱要校验)

  • 你的状态更新依赖上一次的状态,而且有多个不同的更新“动作”

useReducer 就是把所有更新规则集中到一个小管家(reducer 函数)里,你只需要告诉小管家“我想做什么动作”,小管家帮你算出新状态。

生活类比

想象你有一个银行账户(状态):

  • 直接操作(useState):你自己记着余额,每次取钱存钱自己算,容易出错。

  • useReducer:你雇了一个会计。你只跟会计说“我要存100”或“我要取50”,会计按照规则算出新余额,告诉你。你不需要知道怎么算的。

怎么用?(代码示例:计数器升级版)

普通 useState 写法(简单情况):

const [count, setCount] = useState(0);
setCount(count + 1);

如果规则多一点:比如允许 +1、-1、重置、加任意数字。用 useState 就要写多个 setter,逻辑分散。

用 useReducer

import { useReducer } from 'react';

// 1. 写一个小管家(reducer 函数)
// 它接收当前状态和一个动作,返回新状态
function countReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    case 'add':
      return { count: state.count + action.payload };
    default:
      return state;
  }
}

function Counter() {
  // 2. 使用 useReducer,传入管家和初始状态
  const [state, dispatch] = useReducer(countReducer, { count: 0 });

  return (
    <div>
      <p>{state.count}</p>
      {/* 3. 派发动作,让管家干活 */}
      <button onClick={() => dispatch({ type: 'increment' })}>+1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
      <button onClick={() => dispatch({ type: 'reset' })}>重置</button>
      <button onClick={() => dispatch({ type: 'add', payload: 5 })}>+5</button>
    </div>
  );
}

解释:

  • dispatch({ type: 'increment' }) 就是告诉会计“我要加1”

  • 会计(reducer)根据规则计算新状态,React 自动更新页面

什么时候真的需要用?

  • 状态更新逻辑超过 3 种不同操作

  • 下一个状态依赖前一个状态,而且计算比较复杂

  • 比如购物车(添加、删除、改数量、清空)、表单(多个字段校验)

useReducer vs useState 选择

  • 简单情况:用 useState(80% 的情况)

  • 复杂情况:用 useReducer(让代码更清晰、可预测)

总结

Hook

一句话解释(通俗)

useState

让组件记住一个会变的值(比如按钮点了几下),值变了页面自动刷新。

useEffect

等组件渲染完后去做一件事(比如发请求、改标题、开定时器),还能在组件消失前收拾干净。

useRef

放东西的抽屉,换东西页面不刷新;常用来拿 DOM 或存定时器 id。 ✅(你已写)

useContext

公共快递柜,避免 props 层层传递;适合主题、用户等全局数据。 ✅(你已写)

useReducer

雇一个会计管理复杂状态;你只告诉它动作,它按规则算新状态。 ✅(你已写)

useMemo

把计算结果缓存起来,依赖没变就不重新算(省力气)。

useCallback

把函数缓存起来,依赖没变就不重新创建函数(让子组件少渲染)。

自定义 Hook

把自己重复用的逻辑(比如发请求、拿窗口宽度)包成一个以 use 开头的函数,到处用。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐