跳到主要内容

React 基础

react 是前端三大框架之一

  • 【函数式组件 + hooks】方式进行讲解

1) 环境准备

创建项目

首先,通过 react 脚手架创建项目

npx create-react-app client --template typescript
  • client 是项目名
  • 目前 react 版本是 18.x

运行项目

cd client
npm start
  • 会自动打开浏览器,默认监听 3000 端口

image-20221001110328233

修改端口

在项目根目录下新建文件 .env.development,它可以定义开发环境下的环境变量

PORT=7070

重启项目,端口就变成了 7070

浏览器插件

插件地址 New React Developer Tools – React Blog (reactjs.org)

image-20221004105110150

VSCode

推荐安装 Prettier 代码格式化插件

image-20221004090816142

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

image-20221005160308705

执行流程

  • 首次调用函数组件,返回的 jsx 代码会被渲染成【虚拟 dom 节点】(也称 Fiber 节点)
    • 根据【虚拟 dom 节点】会生成【真实 dom 节点】,由浏览器显示出来
  • 当函数组件的 props 或 state 发生变化时,才会重新调用函数组件,返回 jsx
    • jsx 与上次的【虚拟 dom 节点】对比
      • 如果没变化,复用上次的节点
      • 有变化,创建新的【虚拟 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

image-20221005173958351

2s 后数据更新,setStudent 函数会更新 State 数据,并会触发下一次渲染(P5 的调用)

image-20221005174347981

再次调用 useState,这时返回更新后的数据

image-20221005174739593

这时再返回 jsx,内容就是 姓名是:宋远桥

P.S.

使用了 useState 之后,会执行两次 xhr 请求,后一次请求是 react 开发工具发送的,不用理会

问题还未结束,第二次 P5 调用时,updateStudent 还会执行,结果会导致 2s 后响应返回继续调用 setStudent,这会导致每隔 2s 调用一次 P5 函数(渲染一次)

image-20221005175440228

如何让 updateStudent 只执行一次呢?一种土办法是再设置一个布尔 State

image-20221005181042078

接下来数据更新

image-20221005181428984

第二次进入 P5 函数时,由于 fetch 条件不成立,因此不会再执行两个 setXXX 方法

image-20221005182505908

函数式组件的工作流程

  • 首次调用函数组件,返回的 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>
)
}