React 基础
react 是前端三大框架之一
- 【函数式组件 + hooks】方式进行讲解
1) 环境准备
创建项目
首先,通过 react 脚手架创建项目
npx create-react-app client --template typescript
- client 是项目名
- 目前 react 版本是 18.x
运行项目
cd client
npm start
- 会自动打开浏览器,默认监听 3000 端口
修改端口
在项目根目录下新建文件 .env.development,它可以定义开发环境下的环境变量
PORT=7070
重启项目,端口就变成了 7070
浏览器插件
插件地址 New React Developer Tools – React Blog (reactjs.org)
VSCode
推荐安装 Prettier 代码格式化插件
2) 入门案例
Hello
编写一个 src/pages/Hello.tsx 组件
export default function Hello() {
return <h3>Hello, World!</h3>
}
- 组件中使用了 jsx 语法,即在 js 中直接使用 html 标签或组件标签
- 函数式组件必须返回标签片段
在 index.js 引入组件
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import reportWebVitals from './reportWebVitals'
// 1. 引入组件
import Hello from './pages/Hello'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
<React.StrictMode>
{/* 2. 将原来的 <App/> 改为 <Hello></Hello> */}
<Hello></Hello>
</React.StrictMode>
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()
将欢迎词作为属性传递给组件
<Hello msg='你好'></Hello>
- 字符串值,可以直接使用双引号赋值
- 其它类型的值,用
{值}
而组件修改为
export default function Hello(props: { msg: string }) {
return <h3>{props.msg}</h3>
}
jsx 原理
export default function Hello(props: { msg: string }) {
return <h3>{props.msg}</h3>
}
在 v17 之前,其实相当于
import { createElement } from "react";
export default function Hello(props: {msg: string}) {
return createElement('h3', null, `${props.msg}`)
}
3) 人物卡片案例
样式已经准备好 /src/css/P1.css
#root {
display: flex;
width: 100vw;
height: 100vh;
justify-content: center;
align-items: center;
}
div.student {
flex-shrink: 0;
flex-grow: 0;
position: relative;
width: 128px;
height: 330px;
/* font-family: '华文行楷'; */
font-size: 14px;
text-align: center;
margin: 20px;
display: flex;
justify-content: flex-start;
background-color: #7591AD;
align-items: center;
flex-direction: column;
border-radius: 5px;
box-shadow: 0 0 8px #2c2c2c;
color: #e8f6fd;
}
.photo {
position: absolute;
width: 100%;
height: 100%;
top: 0;
border-radius: 0%;
overflow: hidden;
transition: 0.3s;
border-radius: 5px;
}
.photo img {
width: 100%;
height: 100%;
/* object-fit: scale-down; */
object-fit: cover;
}
.photo::before {
position: absolute;
content: '';
width: 100%;
height: 100%;
background-image: linear-gradient(to top, #333, transparent);
}
div.student h2 {
position: absolute;
font-size: 20px;
width: 100%;
height: 68px;
font-weight: normal;
text-align: center;
margin: 0;
line-height: 68px;
visibility: hidden;
}
h2::before {
position: absolute;
top: 0;
left: 0;
content: '';
width: 100%;
height: 68px;
background-color: rgba(0, 0, 0, 0.3);
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
div.student h1 {
position: absolute;
top: 250px;
font-size: 22px;
margin: 0;
transition: 0.3s;
font-weight: normal;
}
div.student p {
margin-top: 300px;
width: 80%;
font-weight: normal;
text-align: center;
padding-bottom: 5px;
border-bottom: 1px solid #8ea2b8;
}
.student:hover .photo::before {
display: none;
}
.student:hover .photo {
width: 90px;
height: 90px;
top: 90px;
border-radius: 50%;
box-shadow: 0 0 15px #111;
}
.student:hover img {
object-position: 50% 0%;
}
.student:hover h1 {
position: absolute;
top: 190px;
width: 40px;
}
div.student:hover h2 {
visibility: visible;
}
类型 /src/model/Student.ts
export interface Student {
id: number,
name: string,
sex: string,
age: number,
photo: string
}
组件 /src/pages/P1.tsx
import { Student } from '../model/Student'
import '../css/P1.css'
export default function P1(props: { student: Student }) {
return (
<div className='student'>
<div className='photo'>
<img src={props.student.photo}/>
</div>
<h1>{props.student.name}</h1>
<h2>{props.student.id}</h2>
<p>性别 {props.student.sex} 年龄 {props.student.age}</p>
</div>
)
}
使用组件
const stu1 = { id: 1, name: '风清扬', sex: '男', age: 99, photo: '/imgs/1.png' }
const stu2 = { id: 2, name: '玮哥', sex: '男', age: 20, photo: '/imgs/2.png' }
const stu3 = { id: 3, name: '长宏', sex: '男', age: 30, photo: '/imgs/3.png'}
<P1 student={stu1}></P1>
<P1 student={stu2}></P1>
<P1 student={stu3}></P1>
路径
- src 下的资源,要用相对路径引入
- public 下的资源,记得 / 代表路径的起点
标签命名
- 组件标签必须用大驼峰命名
- 普通 html 标签必须用小写命名
事件处理
import { Student } from '../model/Student'
import '../css/P1.css'
export default function P1(props: { student: Student }) {
function handleClick(e : React.MouseEvent){
console.log(student)
console.log(e)
}
return (
<div className='student'>
<div className='photo' onClick={handleClick}>
<img src={props.student.photo}/>
</div>
<h1>{props.student.name}</h1>
<h2>{props.student.id}</h2>
<p>性别 {props.student.sex} 年龄 {props.student.age}</p>
</div>
)
}
- 事件以小驼峰命名
- 事件处理函数可以有一个事件对象参数,可以获取事件相关信息
列表 & Key
import { Student } from '../model/Student'
import P1 from './P1'
export default function P2(props: { students: Student[] }) {
return (
<>
{props.students.map((s) => ( <P1 student={s} key={s.id}></P1> ))}
</>
)
}
- key 在循环时是必须的,否则会有 warning
也可以这么做
import { Student } from '../model/Student'
import P1 from './P1'
export default function P2(props: { students: Student[] }) {
const list = props.students.map((s) => <P1 student={s} key={s.id}></P1>)
return <>{list}</>
}
使用组件
const stu1 = { id: 1, name: '风清扬', sex: '男', age: 99, photo: '/1.png' }
const stu2 = { id: 2, name: '玮哥', sex: '男', age: 45, photo: '/2.png' }
const stu3 = { id: 3, name: '长宏', sex: '男', age: 45, photo: '/3.png'}
<P2 students={[stu1,stu2,stu3]}></P2>
条件渲染
P1 修改为
import { Student } from '../model/Student'
import '../css/P1.css'
export default function P1(props: { student: Student; hideAge?: boolean }) {
function handleClick() {
console.log(props.student)
}
const ageFragment = !props.hideAge && <span>年龄 {props.student.age}</span>
return (
<div className='student'>
<div className='photo' onClick={handleClick}>
<img src={props.student.photo} />
</div>
<h1>{props.student.name}</h1>
<h2>{props.student.id}</h2>
<p>
性别 {props.student.sex} {ageFragment}
</p>
</div>
)
}
- 子元素如果是布尔值,nullish,不会渲染
P2 修改为
import { Student } from '../model/Student'
import P1 from './P1'
export default function P2(props: { students: Student[]; hideAge?: boolean }) {
const list = props.students.map((s) => (
<P1 student={s} hideAge={props.hideAge} key={s.id}></P1>
))
return <>{list}</>
}
使用组件
const stu1 = { id: 1, name: '风清扬', sex: '男', age: 99, photo: '/1.png' }
const stu2 = { id: 2, name: '玮哥', sex: '男', age: 45, photo: '/2.png' }
const stu3 = { id: 3, name: '长宏', sex: '男', age: 45, photo: '/3.png'}
<P2 students={[stu1,stu2,stu3]} hideAge={true}></P2>
参数解构
以 P1 组件为例
import { Student } from '../model/Student'
import '../css/P1.css'
export default function P1
({ student, hideAge = false }: { student: Student, hideAge?: boolean }) {
function handleClick() {
console.log(student)
}
const ageFragment = !hideAge && <span>年龄 {student.age}</span>
return (
<div className='student'>
<div className='photo' onClick={handleClick}>
<img src={student.photo} />
</div>
<h1>{student.name}</h1>
<h2>{student.id}</h2>
<p>
性别 {student.sex} {ageFragment}
</p>
</div>
)
}
- 可以利用解构赋值语句,让 props 的使用更为简单
- 对象解构赋值还有一个额外的好处,给属性赋默认值
使用组件
const stu1 = { id: 1, name: '风清扬', sex: '男', age: 99, photo: '/1.png' }
<P1 student={stu1}></P1>
4) 处理变化的数据
入门案例侧重的是数据展示,并未涉及到数据的变动,接下来我们开始学习 react 如何处理数据变化
axios
首先来学习 axios,作用是发送请求、接收响应,从服务器获取真实数据
安装
npm install axios
定义组件
import axios from 'axios'
export default function P4({ id }: { id: number }) {
async function updateStudent() {
const resp = await axios.get(`http://localhost:8080/api/students/${id}`)
console.log(resp.data.data)
}
updateStudent()
return <></>
}
- 其中 /api/students/${id} 是提前准备好的后端服务 api,会延迟 2s 返回结果
使用组件
<P4 id={1}></P4>
在控制台上打印
{
"id": 1,
"name": "宋远桥",
"sex": "男",
"age": 40
}
当属性变化时,会重新触发 P4 组件执行,例如将 id 从 1 修改为 2
执行流程
- 首次调用函数组件,返回的 jsx 代码会被渲染成【虚拟 dom 节点】(也称 Fiber 节点)
- 根据【虚拟 dom 节点】会生成【真实 dom 节点】,由浏览器显示出来
- 当函数组件的 props 或 state 发生变化时,才会重新调用函数组件,返回 jsx
- jsx 与上次的【虚拟 dom 节点】对比
- 如果没变化,复用上次的节点
- 有变化,创建新的【虚拟 dom 节点】替换掉上次的节点
- jsx 与上次的【虚拟 dom 节点】对比
- 由于严格模式会触发两次渲染,为了避免干扰,请先注释掉 index.tsx 中的
<React.StrictMode>
状态
先来看一个例子,能否把服务器返回的数据显示在页面上
import axios from 'axios'
let count = 0
export default function P5(props: { id: number }) {
function getTime() {
const d = new Date()
return d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds()
}
async function updateStudent() {
const resp = await axios.get(
`http://localhost:8080/api/students/${props.id}`
)
Object.assign(student, resp.data.data)
console.log(current, student, getTime())
}
const current = count++
let student = { name: 'xx' }
console.log(current, student, getTime())
updateStudent()
return <h3>姓名是:{student.name}</h3>
}
- count 是一个全局变量,记录 P5 函数第几次被调用
执行效果,控制台上
0 {name: 'xx'} '16:22:16'
0 {id: 1, name: '宋远桥', sex: '男', age: 40} '16:22:18'
此时页面仍显示 姓名是:xx
那么修改一下 props 的 id 呢?进入开发工具把 id 从 1 修改为 2,控制台上
1 {name: 'xx'} '16:24:0'
1 {id: 2, name: '俞莲舟', sex: '男', age: 38} '16:24:2'
此时页面仍显示 姓名是:xx
为什么页面没有显示两秒后更新的值?
- 第一次,页面显示的是 P5 函数的返回结果,这时 student.name 还未被更新成宋远桥,页面显示 xx
- 虽然 2s 后数据更新了,但此时并未触发函数执行,页面不变
- 第二次,虽然 props 修改触发了函数重新执行,但既然函数重新执行,函数内的 student 又被赋值为
{ name: 'xx' }
,页面还是显示 xx- 2s 后数据更新,跟第一次一样,并未重新触发函数执行,页面不变
结论:
- 函数是无状态的,执行完毕后,它内部用的数据都不会保存下来
- 要想让函数有状态,就需要使用 useState 把数据保存在函数之外的地方,这些数据,称之为状态
useState
import axios from 'axios'
import { useReducer, useState } from 'react'
import { Student } from '../model/Student'
let count = 0
export default function P5(props: { id: number }) {
// ...
async function updateStudent() {
const resp = await axios.get(
`http://localhost:8080/api/students/${props.id}`
)
Object.assign(student, resp.data.data)
console.log(current, student, getTime())
}
const current = count++
let [student] = useState<Student>({ name: 'xx' })
console.log(current, student, getTime())
updateStudent()
return <h3>姓名是:{student.name}</h3>
}
接下来使用 setXXX 方法更新 State
import axios from 'axios'
import { useState } from 'react'
import { Student } from '../model/Student'
export default
function P5(props: { id: number }) {
async function updateStudent() {
const resp = await axios.get(`/api/students/${props.id}`)
setStudent(resp.data.data)
}
let [student, setStudent] = useState<Student>({ name: 'xx' })
updateStudent()
return <h3>姓名是:{student.name}</h3>
}
工作流程如下
首次使用 useState,用它的参数初始化 State
2s 后数据更新,setStudent 函数会更新 State 数据,并会触发下一次渲染(P5 的调用)
再次调用 useState,这时返回更新后的数据
这时再返回 jsx,内容就是 姓名是:宋远桥
了
P.S.
使用了 useState 之后,会执行两次 xhr 请求,后一次请求是 react 开发工具发送的,不用理会
问题还未结束,第二次 P5 调用时,updateStudent 还会执行,结果会导致 2s 后响应返回继续调用 setStudent,这会导致每隔 2s 调用一次 P5 函数(渲染一次)
如何让 updateStudent 只执行一次呢?一种土办法是再设置一个布尔 State
接下来数据更新
第二次进入 P5 函数时,由于 fetch 条件不成立,因此不会再执行两个 setXXX 方法
函数式组件的工作流程
- 首次调用函数组件,返回的 jsx 代码会被渲染成【虚拟 dom 节点】(也称 Fiber 节点)
- 此时使用 useState 会将组件工作过程中需要数据绑定到【虚拟 dom 节点】上
- 根据【虚拟 dom 节点】会生成【真实 dom 节点】,由浏览器显示出来
- 当函数组件的 props 或 state 发生变化时,才会重新调用函数组件,返回 jsx
- props 变化由父组件决定,state 变化由组件自身决定
- jsx 与上次的【虚拟 dom 节点】对比
- 如果没变化,复用上次的节点
- 有变化,创建新的【虚拟 dom 节点】替换掉上次的节点
useEffect
Effect 称之为副作用(没有贬义),函数组件的主要目的,是为了渲染生成 html 元素,除了这个主要功能以外,管理状态,fetch 数据 ... 等等之外的功能,都可以称之为副作用。
useXXX 打头的一系列方法,都是为副作用而生的,在 react 中把它们称为 Hooks
useEffect 三种用法
import axios from "axios"
import { useEffect, useState } from "react"
/*
useEffect
参数1:箭头函数, 在真正渲染 html 之前会执行它
参数2:
情况1:没有, 代表每次执行组件函数时, 都会执行副作用函数
情况2:[], 代表副作用函数只会执行一次
情况3:[依赖项], 依赖项变化时,副作用函数会执行
*/
export default function P6({ id, age }: { id: number, age: number }) {
console.log('1.主要功能')
// useEffect(() => console.log('3.副作用功能'))
// useEffect(() => console.log('3.副作用功能'), [])
useEffect(() => console.log('3.副作用功能'), [id])
console.log('2.主要功能')
return <h3>{id}</h3>
}
用它改写 P5 案例
import axios from "axios"
import { useEffect, useState } from "react"
export default function P6({ id, age }: { id: number, age: number }) {
const [student, setStudent] = useState({name:'xx'})
useEffect(()=>{
async function updateStudent() {
const resp = await axios.get(`http://localhost:8080/api/students/${id}`)
setStudent(resp.data.data)
}
updateStudent()
}, [id])
return <h3>{student.name}</h3>
}
useContext
import axios from 'axios'
import { createContext, useContext, useEffect, useState } from 'react'
import { R, Student } from '../model/Student'
/*
createContext 创建上下文对象
useContext 读取上下文对象的值
<上下文对象.Provider> 修改上下文对象的值
*/
const HiddenContext = createContext(false)
// 给以下组件提供数据,控制年龄隐藏、显示
export default function P7() {
const [students, setStudents] = useState<Student[]>([])
const [hidden, setHidden] = useState(false)
useEffect(()=>{
async function updateStudents() {
const resp = await axios.get<R<Student[]>>("http://localhost:8080/api/students")
setStudents(resp.data.data)
}
updateStudents()
}, [])
function hideOrShow() {
// 参数:上一次状态值,旧值
// 返回值:要更新的新值
setHidden((old)=>{
return !old
})
}
return <HiddenContext.Provider value={hidden}>
<input type="button" value={hidden?'显示':'隐藏'} onClick={hideOrShow}/>
<P71 students={students}></P71>
</HiddenContext.Provider>
}
// 负责处理学生集合
function P71({ students }: { students: Student[] }) {
const list = students.map(s=><P72 student={s} key={s.id}></P72>)
return <>{list}</>
}
// 负责显示单个学生
function P72({ student }: { student: Student }) {
const hidden = useContext(HiddenContext)
const jsx = !hidden && <span>{student.age}</span>
return <div>{student.name} {jsx}</div>
}
- 如果组件分散在多个文件中,HiddenContext 应该 export 导出,用到它的组件 import 导入
- React 中因修改触发的组件重新渲染,都应当是自上而下的
- setHidden 方法如果更新的是对象,那么要返回一个新对象,而不是在旧对象上做修改
表单
import axios from 'axios'
import React, { useState } from 'react'
import '../css/P8.css'
export default function P8() {
const [student, setStudent] = useState({name:'', sex:'男', age:18})
const [message, setMessage] = useState('')
const options = ['男', '女']
const jsx = options.map(e => <option key={e}>{e}</option>)
// e 事件对象, e.target 事件源
function onChange(e : React.ChangeEvent<HTMLInputElement|HTMLSelectElement>) {
setStudent((old)=>{
// 返回的新值,不能与旧值是同一个对象
return {...old, [e.target.name]:e.target.value}
})
}
async function onClick() {
const resp = await axios.post('http://localhost:8080/api/students', student)
setMessage(resp.data.data)
}
const messageJsx = message && <div className='success'>{message}</div>
return (
<form>
<div>
<label>姓名</label>
<input type="text" value={student.name} onChange={onChange} name='name'/>
</div>
<div>
<label>性别</label>
<select value={student.sex} onChange={onChange} name='sex'>
{jsx}
</select>
</div>
<div>
<label>年龄</label>
<input type="text" value={student.age} onChange={onChange} name='age' />
</div>
<div>
<input type='button' value='新增' onClick={onClick}/>
</div>
{messageJsx}
</form>
)
}