web自学6——React
前文记录了 npm 和 Vite。npm 解决第三方依赖管理问题,Vite 解决开发服务器、模块解析和生产构建问题。
但是即使用了 Vite,页面内部的交互逻辑仍然可以是原生 JavaScript 和 DOM API。比如 TodoList 中仍然可以写:
1 | const li = document.createElement("li"); |
当页面非常简单时,这样写完全够用。但是当页面状态越来越复杂,就会遇到新的问题,这就是 React 要解决的部分。
1. 从 DOM 操作到状态同步
如果 TodoList 只有添加和删除任务,原生 DOM 操作很直接。但是继续增加功能以后,例如编辑、筛选、统计、保存、批量操作,问题会逐渐出现:
- 同一份数据可能影响页面中的多个位置,例如列表、未完成数量、筛选结果;
- 增删改查以后,需要手动同步很多 DOM 节点;
- 表单状态、列表状态、筛选状态交织在一起;
- 页面拆成多个区域后,数据传递和事件处理容易混乱。
也就是说,原生 DOM 操作的问题不在于“不能做”,而是状态多了以后,程序员需要自己保证数据和页面始终同步。
React 解决的正是这个困难:它把手工维护 DOM 的逻辑,改成用组件和状态描述 UI。
2. 基本思想
原生 JavaScript 的思路通常是:
1 | 数据变化 -> 手动修改 DOM |
React 的思路更接近:
1 | state -> UI = f(state) -> React 更新 DOM |
也就是说,程序主要维护 state,然后描述当前 state 下页面应该长什么样。至于怎样把真实 DOM 更新到这个状态,由 React 负责。
例如 TodoList 中有一个数组:
1 | const todos = [ |
React 中可以直接把这个数组映射成 UI:
1 | <ul> |
这里的意思是:todos 数组里有什么,页面就显示什么。以后 todos 改变,React 会重新计算 UI,并同步到真实 DOM。
3. JSX
JSX 是什么
JSX 是 JavaScript 的语法扩展,用类似 HTML 的写法描述 UI。例如:
1 | <button onClick={addTodo}>添加</button> |
这段代码描述了一个按钮,其中显示“添加”,点击时调用 addTodo。它不是一段 HTML 字符串,而是一个 JavaScript 表达式,其结果用于描述一个 React 元素。
从概念上看,它大致相当于:
1 | React.createElement("button", { onClick: addTodo }, "添加"); |
React 并不强制使用 JSX,也可以直接调用 JavaScript API 创建元素。不过 UI 结构复杂以后,JSX 通常更容易阅读和编写。
基本写法
JSX 可以像 HTML 一样嵌套标签:
1 | <main> |
普通文本直接写在标签中;需要使用 JavaScript 数据时,则把表达式放进 {}:
1 | <h1>{title}</h1> |
这里的 {title} 用来显示变量,{todos.length} 用来显示计算结果,{todos.map(...)} 则根据数组生成一组元素。{} 中只能放表达式,不能直接放 if、for 等语句。
JSX 和 HTML 还有几个常见区别:
- JSX 属性使用 JavaScript 的命名方式,例如
class写成className,onclick写成onClick; - 字符串属性可以写成
type="button",JavaScript 表达式则写在{}中,例如disabled={todos.length === 0}; - 标签必须闭合,例如
<input />; - 一段 JSX 包含多个并列元素时,需要用同一个父元素或
<>...</>包起来。
JSX 如何运行
JSX 不是浏览器原生支持的 JavaScript 语法,不能直接交给浏览器执行。包含 JSX 的文件通常使用 .jsx 扩展名,例如 App.jsx。在使用 Vite 的项目中,Vite 会负责把 JSX 转换成普通 JavaScript,再交给浏览器运行。
4. 组件、props 和 state
理解 JSX 后,再来看 React 中的几个基本概念。
组件
组件是一块可以组合的 UI,最简单的组件就是一个返回 JSX 的 JavaScript 函数:
1 | function TodoItem() { |
组件可以继续组合成更大的组件:
1 | function App() { |
props
组件通常需要接受外部传入的数据:
1 | function TodoItem({ todo }) { |
使用组件时传入:
1 | <TodoItem todo={todo} /> |
这里传入的 todo 就是 prop。可以暂时把组件理解成:
1 | Component(props) -> UI |
state
有些数据会随用户操作变化,例如输入框内容、Todo 列表、当前过滤条件等,这些内容适合放到 state 中。
1 | const [todos, setTodos] = useState([]); |
增加、删除和修改任务时,不直接操作 DOM,而是更新 state:
1 | setTodos([...todos, newTodo]); |
这时页面如何变化,不再靠 document.createElement(...)、element.remove() 或 element.classList.add(...) 手动完成,而是由 React 根据新的 state 重新计算。
5. 创建项目
可以使用 Vite 创建 React 项目:
1 | npm create vite@latest todo-react |
这里选择普通的 JavaScript 模板,不选择 React Compiler。项目创建完成后,目录结构大致如下:
1 | todo-react/ |
其中大部分内容与普通 Vite 项目相同,React 相关的入口主要在 src/ 中:
main.jsx是 JavaScript 入口,负责启动 React;App.jsx定义页面最外层的App组件;index.css保存整个页面的基础样式;App.css保存默认App组件的样式;assets/中的图片只是默认演示页面使用的资源。
此外,package.json 中已经安装了 react 和 react-dom。前者提供组件、state 等 React API,后者负责把 React 页面渲染到浏览器 DOM。
vite.config.js 是 Vite 的配置文件,这里是内容是启用了 Vite 的 React 插件,让 Vite 能够处理 React 项目中的 JSX 文件:
1 | import { defineConfig } from "vite"; |
默认的入口 index.html 非常简单,通过模块脚本引入 src/main.jsx:
1 |
|
默认的 main.jsx 内容如下:
1 | import { StrictMode } from 'react' |
解释一下:document.getElementById("root") 找到 HTML 中的根节点,createRoot(...) 为它创建 React 根节点,render(<App />) 再把 App 组件渲染到页面中。StrictMode 用于在开发阶段帮助发现潜在问题,不会生成额外的页面元素。
这里的 App 来自 App.jsx。默认模板中的 App 是一个计数器演示组件,同时导入了 App.css 和 assets/ 中的图片。
这里涉及到的各个阶段依次是:
- 浏览器仍然从 HTML
index.html开始加载 - Vite 负责处理模块和 JSX
- React 从
main.jsx开始接管#root内部的页面
其中的 index.html 和 main.jsx 都模板化了,内容简单且固定,通常都不需要修改。
6. TodoList 示例
现在把默认计数器改成 TodoList。可以删除 src/assets/ 和 src/App.css,保留不需要修改的 main.jsx,然后重写 App.jsx 和 index.css:
1 | src/ |
src/App.jsx 负责 TodoList 的状态、事件和页面结构:
1 | import { useState } from "react"; |
src/index.css 可以写成:
1 | * { |
使用 React 的版本相比之前直接写的做法,核心变化是:程序不再直接创建和删除 DOM,而是修改 text 和 todos 两个 state。页面长什么样由 JSX 描述,React 负责把 state 的变化同步到 DOM。
