跳到主要内容

React入门笔记

后端程序员的React入门笔记

参考来源:

https://doc.happy-learning.cn/react/react基础

相关介绍

概览

  1. React 英文文档 https://react.dev/
  2. React 中文文档 https://zh-hans.react.dev/

特性

  1. 声明式设计 — React 采用声明范式,可以轻松描述应用。
  2. 高效, 通过对 DOM 的模拟(虚拟 dom), 最大限度地减少与 DOM 的交互。
  3. 灵活 — React 可以与已知的库或框架很好地配合。
  4. JSX — JSX 是 JavaScript 语法的扩展
  5. 组件 — 通过 React 构建组件,使得代码更加容易得到复用,能够很好的应用在顶目的开发中。
  6. 单向响应的数据流 — React 实现了单向响应的数据流

虚拟 DOM

  • DOM 操作非常昂贵
  • 在前端开发中,性能消耗最大的就是 DOM 操作,而且这部分代码会让整体项目的代码变得难以维护
  • React 把真实 DOM 树转换成 JavaScript 对象树,也就是 Virtual DOM

优点

  1. 系出名门, facebook 开发
  2. 一个专注于构建用户界面的 JavaScript 框架,和 vue 和 angular 并称前端三大框架
  3. React 的出现让创建交互式 UI 变得轻而易举
  4. 函数和类就是 UI 的载体, 将数据传入 React 的类和函数中,返回的就是 UI 界面
  5. React 应用中,一切皆组件
  6. React 还具有跨平台能力, Node 进行服务器渲染,还可以用 React Native 进行原生移动应用的开发
  7. 小厂喜欢 Vue, 大厂喜欢 React

搭建

VSCode 可安装 Error Lens 插件,美化错误提示

配置环境

# 安装脚手架
npm install -g create-react-app
# 生成项目
create-react-app react-demo
# 运行(浏览器查看本地3000端口)
cd react-demo; npm run start

包管理

yarn与npm都是包管理器,其lock文件用于锁定项目依赖版本

# 安装项目依赖
npm i
# 向项目中添加新依赖
npm i 库名
# 向项目中添加dev新依赖
npm i 库名 --save-dev
# 从项目中删除依赖
npm uninstall 库名 --save
# 升级项目依赖
npm update --save

# 全局安装新依赖(慎用)
npm install 库名 -g

demo

在src目录下新建index.js文件:

import ReactDOM from "react-dom/client"
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
    <h1>
        欢迎
    </h1>
)

语法

jsx

表达式

可以使用的表达式

  1. 字符串、数值、布尔值、null、undefined、object( [] /
  2. 1 + 2、‘abc’.split(”)、[‘a’, ‘b’].join(’-‘)
  3. fn()

函数的返回值有两种书写方式

  1. 花括号+return 返回值: function getAge=()=>{return 18}
  2. 圆括号+返回值:function getAge=()=>(18)
import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(document.getElementById("root"));
// 定义一个用户名
const username = "张三";
// 定义一个函数, 用来计算年龄
const getAge = () => {
return 30;
};
root.render(
<h1>
你好, 我叫{username} , 今年 {getAge()} 岁了!!!
</h1>
);

列表

import ReactDOM from "react-dom/client"
const root = ReactDOM.createRoot(document.getElementById("root"))
const username = "Cheems"
const flag = true
const getAge = () => {
    return 18
}

const songs = [
    { id: 1, name: "粉红高跟鞋" },
    { id: 2, name: "伤心太平洋" },
    { id: 3, name: "雨一直下" },
  ];
root.render(
    <div>
        <h1>
            欢迎: {username}, 年龄: {getAge()}<br />
        </h1>
        <h2>{flag ? "心情不错" : "emo..."}</h2>
        <div>
            <h3>Songs</h3>
            <ul>
                {
                    songs.map((song)=>(
                        <li key={song.id}>{song.name}</li>
                    ))
                }
            </ul>
        </div>
    </div>
)

条件

// 三元运算符
root.render(<h1>{flag ? "好棒棒!!!" : "真菜..."}</h1>);

// 使用单独函数处理条件
const getFlag = () => {
if (type === 1) {
return <h1>this is h1</h1>;
}
if (type === 2) {
return <h2>this is h2</h2>;
}
if (type === 3) {
return <h3>this is h3</h3>;
}
};
root.render(<div>{getFlag()}</div>);

样式

ES6对象属性简写

  • const color = "blue";
  • const styleObj = { color: color }; // 传统写法
  • const styleObj = { color }; // ES6 简写写法(效果完全等同)
// 使用两个大括号
<h1 style={{ color: "blue", backgroundColor: "pink" }}>

// 1. 对象名与属性名相同时
// 方法1:使用变量存储
const color = "blue"
const backgroundColor = "pink"
<h1 style={{ color, backgroundColor }}>
// 方法2:使用对象
const titleStyle = {
    color,
    backgroundColor
}
<h1 style={{ titleStyle }}>

// 2.对象名于属性名不同时
const songColor = "pink"
const songBgColor = "lightgreen"
<li style={{color:songColor, backgroundColor:songBgColor}} key={song.id}>{song.name}</li>

// 3.使用className
const showTitle = true
.tittle{
    color: red;
    font-size: 20px;
    font-weight: bold;
}
<h2 className="tittle">Nice day!</h2>
<h2 className={showTitle?"title":""}>Nice day!</h2>

组件

组件分类

react组件

函数组件

  1. 组件的名称必须首字母大写(大驼峰的写法),react 内部会根据这个来判断是组件还是普通的 HTML 标签
  2. 函数组件必须有返回值,表示该组件的 UI 结构;
  3. 如果不需要渲染任何内容,则返回 null
  4. 组件就像 HTML 标签一样可以被渲染到页面中。
  5. 组件表示的是一段结构内容,对于函数组件来说,渲染的内容是函数的返回值
  6. 使用函数名称作为组件标签名称,可以是双标签也可以是单标签
function HelloFunctionComponenet(){
  return <h1>hello world.</h1>
}

root.render(
  <HelloFunctionComponenet/>
);

类组件

  1. 类名称也必须以大写字母开头, 大驼峰写法
  2. 类组件应该继承 React.Component 父类,从而使用父类中提供的方法或属性
  3. 类组件必须提供 render 方法
  4. render 方法必须有返回值,表示该组件的 UI 结构
class HelloClassComponent extends React.Component {
render() {
return <div>Class Component!</div>;
}
}

root.render(
<div>
<HelloClassComponent />
</div>
);

事件

事件绑定

// 1. 函数组件绑定
function HelloFunctionComponenet() {
const clickHandler = () =>{
alert("Function Clicked")
}
return (
<button onClick={clickHandler}>函数点击</button>
)
}

// 2.类组件绑定
class HelloClassComponent extends React.Component {
clickHandler = () =>{ // 无需声明const
alert("Class Clicked")
console.log(this); // HelloClassComponent
}
render() {
return <button onClick={this.clickHandler}>Class点击</button>
}
}

获取事件参数

class JumpIndexComponent extends React.Component {
indexUrlClick =(e) =>{
e.preventDefault();
alert("即将跳转")
}
render(){
return <a onClick={this.indexUrlClick} href='https://baidu.com'>
跳转主页</a>
}
}
// 传递额外参数
class VolumeChooseComponent extends React.Component{
volumeList = [
{name:"Slepping",weight:1},
{name:"Driving",weight:3},
{name:"Party",weight:5},
]

sceneHandler =(e,weight)=>{
e.preventDefault()
alert("volume turn to" + weight)
}

render(){
return (
<div>
<h3>Choose a Volume: </h3>
<ul>
{
this.volumeList.map((song)=>(
<button key={song.name} onClick={
(e)=>{
this.sceneHandler(e, song.weight)
}
}>{song.name}</button>
))
}
</ul>
</div>
)
}
}

组件状态

  • 思想: 数据驱动视图,只要修改数据状态,页面就会自动刷新dom
  • setState 方法作用: 修改 state 中的数据状态,更新 UI

定义和修改状态:

class Counter extends React.Component {
// 定义状态
state = {
count: 0,
};
this.setState({
count: this.state.count + 1
list: [...this.state.list, 4],
person: {
...this.state.person,
// 覆盖原来的属性 就可以达到修改对象中属性的目的
name: 'rose'
}
})
}

示例:

class CountComponent extends React.Component {
// 初始化状态
state = {
sugarCount: 0,
iceCount: 0
}
countClickHandler = (i) => {
if (i == 'sugar') {
// 修改状态
this.setState({
sugarCount: this.state.sugarCount + 1
})
}
if (i == 'ice') {
this.setState({
iceCount: this.state.iceCount + 1
})
}
alert(i + " added!")
}
render() {
// 读取状态
return (
<div>
<hr></hr>
<div>{this.state.sugarCount}分糖,{this.state.iceCount}分冰</div>
<button id="sugar" onClick={(e) => (
this.countClickHandler("sugar")
)}>➕🍬</button>
<button id="ice" onClick={(e) => (
this.countClickHandler("ice")
)}>➕🧊</button>
</div>
)
}
}

class IngredientsComponent extends React.Component {
state = {
ingredients: ["奶茶"]
}
addIngredientHandler = (name) =>{
this.setState({
// 新增状态
ingredients: [...this.state.ingredients, name]
})
}

render() {
return (
<div>
<hr></hr>
<h5>菜单:</h5>
<button onClick={()=>this.addIngredientHandler("珍珠")}>珍珠</button>
<button onClick={()=>this.addIngredientHandler("芋泥")}>芋泥</button>
<button onClick={()=>this.addIngredientHandler("芝士")}>芝士</button>
<h5>订单:</h5>
<ul>
{
this.state.ingredients.map((m)=>(
<li key={m}>{m}</li>
))
}
</ul>
</div>
)
}
}


root.render(
<div>
<CountComponent />
<IngredientsComponent/>
</div>
);

表单处理

受控表单处理

class FormComponent extends React.Component {

state = {
text: 'this is a msg.'
}

inputChange = (e) => {
const value = e.target.value;
console.log(value)
this.setState = ({
text: value
})
}

render() {
return (
<div>
<hr />
<h5>{this.state.text}</h5>
<div>
<input type="text" value={this.state.text} onChange={this.inputChange} />
</div>
</div>
)
}
}

非受控表单组件

非受控组件就是通过手动操作 dom 的方式获取文本框的值,文本框的状态不受 react 组件的 state 中的状态控制,直接通过原生 dom 获取输入框的值

class InputDomComponent extends React.Component {
// 使用 createRef 函数创建ref对象
msgRef = createRef();
clickHandler = () => {
// 获取ref对象所在的dom元素的值
console.log(this.msgRef.current.value);
}

render() {
return (
<>
<input ref={this.msgRef} />
<button onClick={this.clickHandler}>hello</button>
</>
)
}
}

组件通信

组件是独立且封闭的单元,默认情况下组件只能使用自己的数据(state) 组件化开发的过程中,完整的功能会拆分多个组件,在这个过程中不可避免的需要互相传递一些数据,让各组件之间进行互相沟通、数据传递的过程就是组件通信

父传子

  • 父组件: 引入子组件时,给组件添加自定义属性
  • 类子组件: 通过 this.props 取值
  • 函数子组件: 通过入参取值
class SonClassComponent extends React.Component {
render() {
return (
<div>我是类子组件,{this.props.msg}</div>
)
}
}

function SonFunctionComponent(props) {
return <div>我是函数子组件,{props.msg}</div>
}

class ParentClassComponent extends React.Component {
state = {
msg: "msg from parent..."
}
render() {
return (
<>
<SonClassComponent msg={this.state.msg} />
<SonFunctionComponent msg={this.state.msg} />
</>
)
}
}

props 可以传递任意数据, 数字、字符串、布尔值、数组、对象、函数、JSX

class SonClassComponent extends React.Component {
render() {
return (
<>
<div>我是类子组件,{this.props.msg}</div>
<ul>
{this.props.list.map(
(item, index)=> {
return <li key={index}>{item}</li>
}
)}
</ul>
<button onClick={this.props.alert}>点击提示</button>
{this.props.navi}
</>
)
}
}

class ParentClassComponent extends React.Component {
state = {
msg: "msg from parent...",
list: [1, 2, 3, 4],
alert: ()=>{
alert("父组件提示!")
}
}
render() {
return (
<>
 <SonClassComponent msg={this.state.msg}
        list={this.state.list}
        alert={this.state.alert}
        navi={<div>父组件的导航组件 </div>}/>
<SonFunctionComponent msg={this.state.msg} />
</>
)
}
}

子传父

  • 父组件传入回调函数,接收、处理参数
  • 子组件获取回调函数,传入自定义参数并调用

class SonClassComponent extends React.Component {
statusChange= (status) =>{
this.props.status(status)
}
render() {
return (
<>
<button onClick={()=>this.statusChange('状态1')}>状态1</button>
<button onClick={()=>this.statusChange('状态2')}>状态2</button>
</>
)
}
}

class ParentClassComponent extends React.Component {
statusChangeHandler = (subTaskStatus) =>{
alert("当前子任务状态:" + subTaskStatus)
}
render() {
return (
<>
<hr/>
<SonClassComponent status={this.statusChangeHandler}/>
</>
)
}
}

兄弟组件通信

组件A=>传输父组件=>传输组件B

class BroAceComponent extends React.Component {
dinnerChoose = (text) => {
this.props.dinnerSuggestion(text)
}
render() {
return (
<>
<div>老大:{this.props.dinner}</div>
<button onClick={() => this.dinnerChoose(prompt("老大今天决定吃..."))}>有个想法</button>
</>
)
}
}

class BroBravoBomponent extends React.Component {
dinnerChoose = (text) => {
this.props.dinnerSuggestion(text)
}
render() {
return (
<>
<div>老二:{this.props.dinner}</div>
<button onClick={() => this.dinnerChoose(prompt("老二今天决定吃..."))}>有个想法</button>
</>

)
}
}

class DaddyComponent extends React.Component {
state = {
dinner: "今天吃什么"
}
msgReceive = (dinnerChoice) => {
this.setState ({
dinner: "今天吃" + dinnerChoice
})

}
render() {
return (
<div>
<BroAceComponent dinner={this.state.dinner} dinnerSuggestion={this.msgReceive} />
<hr />
<BroBravoBomponent dinner={this.state.dinner} dinnerSuggestion={this.msgReceive} />
</div>
)
}
}

跨组件通信

在嵌套组件树结构中,父组件如何向末端组件发送消息?

  • 引入Provider,Consumer组件
  • Provider包裹父组件,增加自定义属性,传入需要传递的值
  • Consumer包裹末端组件,提取自定义属性的值
import React,{ createContext } from 'react';
const { Provider, Consumer } = createContext();

// 使用 provider 与 consumer 进行跨组件通信
class ParentComponent extends React.Component {
state = {
parentStatus: "父组件状态"
}
render() {
return (
<Provider value={this.state.parentStatus}>
<div>
this is parent.
<AComponent/>
</div>
</Provider>

);
}
}


class AComponent extends React.Component {
render() {
return (
<div>
this is A
<CComponent />
</div>
);

}
}


class CComponent extends React.Component {
render() {
return (
<div>
this is C,
<Consumer>
{(value)=><b>{value}</b>}
</Consumer>
</div>
);

}
}

root.render(
<div>
<ParentComponent />
</div>
);

createContext与useContext混用

组件属性

children属性

  • 父组件:向子组件的非自闭合标签中传值
  • 子组件:通过 props.children 取值
import React from "react";
import ReactDOM from 'react-dom/client'
const root = ReactDOM.createRoot(document.getElementById('root'))
// 传递html
function Layout({ children }) {
return (
<div className="layout">
<header>Header</header>
<main>{children}</main>
<footer>Footer</footer>
</div>
);
}

function AppOfHtml() {
return (
<Layout>
<h1>Welcome to My App</h1>
<p>This is the main content.</p>
</Layout>
);
}

// 传递Jsx
function Card({ title, content }) {
return (
<div className="card">
<h3>{title}</h3>
<p>{content}</p>
</div>
);
}

function AppOfJsx() {
return (
<Layout>
<Card title="Card 1" content="This is the first card." />
<Card title="Card 2" content="This is the second card." />
</Layout>
)
}

// 传递函数
function UserInfo({ children }) {
const user = { name: "Almost", age: 42 }
return (
<>
<h5>用户信息</h5>
<p>{children(user)}</p>
</>
)
}

function AppOfFunction() {
return (
<UserInfo>
{(user) => (
<div>
<h5>Welcome, {user.name}</h5>
<h6>You are {user.age} years old.</h6>
</div>
)}
</UserInfo>
)
}

// 传递列表
function TodoList({ children }) {
return (
<div className="todoList">
<h5>This is a todo list</h5>
<ul>
{React.Children.map(children, (child, index) => (
<li key={index} className="todolist-item">
{child}
</li>
))}
</ul>
</div>
)
}

function AppOfChildren() {
return (
<TodoList>
<h6>work</h6>
<h6>study</h6>
<h6>play</h6>
</TodoList>
)
}

root.render(
<>
<AppOfHtml /><hr />
<AppOfJsx /><hr />
<AppOfFunction /><hr />
<AppOfChildren /><hr />
</>
)

props校验

使用prop校验包对传入的数据进行校验

  1. 安装属性校验包:npm install prop-types
  2. 导入prop-types 包
  3. 使用组件名.propTypes = {} 给组件添加校验规则

import PropTypes from 'prop-types';
class MyComponent extends React.Component {
static propTypes = {
myProp: PropTypes.string.isRequired,
};
render() {
return <div>{this.props.myProp}</div>;
}
}

默认值

class Parent extends React.Component {
colors = ["red", "blue"];
render() {
return (
<div>
<SonFunction colors={this.colors} />
<hr/>
<SonClass colors ={this.colors} />
</div>
);
}
}
// 函数式组件
function SonFunction({ colors = ["e"] }) {
const list = colors.map((item, index) => {
return <li key={index}>{item}</li>;
});
return <ul>{list}</ul>;
}
// 类组件
class SonClass extends React.Component {
// 类静态属性声明默认值
static defaultProps = {
colors: ["11111", "22222", "33333"],
};
render() {
return (
<ul>
{this.props.colors.map((item, index) => {
return <li key={index}>{item}</li>;
})}
</ul>
);
}
}

生命周期

组件的生命周期是指组件从被创建到挂载到页面中运行起来,再到组件不用时卸载的过程,只有类组件才有生命周期(类组件需要实例化, 函数组件不需要实例化)

生命周期

挂载阶段

钩子 函数触发时机作用
constructor创建组件时,最先执行,初始化的时候只执行一次1. 初始化 state 2. 创建 Ref 3. 使用 bind 解决 this 指向问题等
render每次组件渲染都会触发渲染 UI(注意: 不能在里面调用 setState() , 会无限循环)
componentDidMount组件挂载(完成 DOM 渲染)后执行,初始化的时候执行一次1. 发送网络请求 2.DOM 操作
class Parent extends React.Component {
// 创建组件时,最先执行,初始化的时候只执行一次
constructor() {
super();
console.log("constructor...创建组件时,最先执行,初始化的时候只执行一次");
}
// 组件挂载(完成 DOM 渲染)后执行,初始化的时候执行一次
componentDidMount() {
console.log("componentDidMount...组件挂载(完成 DOM 渲染)后执行,初始化的时候执行一次");
}

state = {
count: 0
};

handleClick = () => {
console.log("计数器函数调用");
this.setState({
...this.state,
count: this.state.count + 1,
});
};

render() {
console.log("render...每次组件渲染都会触发");
return (
<div>
<button onClick={this.handleClick}>{this.state.count}</button>
</div>
);
}
}

root.render(
<>
<Parent />
</>
)

挂载函数顺序

更新阶段

钩子函数触发时机作用
render每次组件渲染都会触发渲染 UI(与 挂载阶段 是同一个 render)
componentDidUpdate组件更新后(DOM 渲染完毕)DOM 操作,可以获取到更新后的 DOM 内容,不要直接调用 setState


class Parent extends React.Component {
// 创建组件时,最先执行,初始化的时候只执行一次
constructor() {
super();
console.log("constructor...创建组件时,最先执行,初始化的时候只执行一次");
}
// 组件挂载(完成 DOM 渲染)后执行,初始化的时候执行一次
componentDidMount() {
console.log("componentDidMount...组件挂载(完成 DOM 渲染)后执行,初始化的时候执行一次");
}
// 组件更新后(DOM 渲染完毕)
componentDidUpdate() {
console.log("componentDidUpdate...组件更新后(DOM 渲染完毕)");
}

state = {
count: 0
};
handleClick = () => {
console.log("计数器函数调用");
this.setState({
...this.state,
count: this.state.count + 1,
});
};
render() {
console.log("render...每次组件渲染都会触发");
return (
<div>
<button onClick={this.handleClick}>{this.state.count}</button>
</div>
);
}
}

root.render(
<>
<Parent />
</>
)

卸载阶段

钩子函数触发时机作用
componentWillUnmount组件卸载(从页面中消失)执行清理工作(比如:清理定时器等)
import React from "react";
import ReactDOM from 'react-dom/client'
const root = ReactDOM.createRoot(document.getElementById('root'))

class Parent extends React.Component {
// 创建组件时,最先执行,初始化的时候只执行一次
constructor() {
super();
console.log("constructor...创建组件时,最先执行,初始化的时候只执行一次");
}
// 组件挂载(完成 DOM 渲染)后执行,初始化的时候执行一次
componentDidMount() {
console.log("componentDidMount...组件挂载(完成 DOM 渲染)后执行,初始化的时候执行一次");
}
// 组件更新后(DOM 渲染完毕)
componentDidUpdate() {
console.log("componentDidUpdate...组件更新后(DOM 渲染完毕)");
}

state = {
count: 0,
flag: true
};
handleClick = () => {
console.log("计数器函数调用");
this.setState({
...this.state,
count: this.state.count + 1,
});
};
handleClickVisible = () => {
console.log("切换组件状态函数调用");
this.setState({
...this.state,
flag: !this.state.flag,
});
};
render() {
console.log("render...每次组件渲染都会触发");
return (
<div>
<button onClick={this.handleClick}>{this.state.count}</button>
<button onClick={this.handleClickVisible}>点击切换子组件状态</button>
{this.state.flag && <Son/>}
</div>
);
}
}

class Son extends React.Component {
// 组件卸载(从页面中消失)
componentWillUnmount() {
console.log("子组件:componentWillUnmount...组件卸载(从页面中消失)");
}
render() {
return <div>子组件展示中...</div>
}
}

root.render(<Parent />)

![[Pasted image 20250324105646.png]]

Hooks

Hooks简介

Hooks来源

Hooks 的本质:一套能够使函数组件更强大,更灵活的”钩子” 钩子: 在特定条件下自动执行 React 体系里组件分为 类组件 和 函数组件 函数组件是一个更加匹配 React 的设计理念 UI = f(data),也更有利于逻辑拆分与重用的组件表达形式,而先前的函数组件是不可以有自己的状态的,为了能让函数组件可以拥有自己的状态,所以从 react v16.8 开始,Hooks 应运而生

Hooks的作用

  1. 有了 hooks 之后,为了兼容老版本,class 类组件并没有被移除,俩者都可以使用
  2. 有了 hooks 之后,不能在把函数称为无状态组件了,因为 hooks 为函数组件提供了状态
  3. hooks 只能在函数组件中使用
  4. 组件的状态逻辑复用,在 hooks 出现之前,react 先后尝试了 mixins 混入,HOC 高阶组件,render-props 等模式, 但是都有各自的问题,比如 mixin 的数据来源不清晰,高阶组件的嵌套问题等等
  5. class 组件自身的问题, class 组件就像一个厚重的‘战舰’ 一样,大而全,提供了很多东西,有不可忽视的学习成本,比如各种生命周期,this 指向问题等等,而我们更多时候需要的是一个轻快灵活的’快艇’

useState

基础

useState 为函数组件提供状态(state),属于hooks的一种

使用方法

  1. 导入 useState 函数
  2. 调用 useState 函数,并传入状态的初始值, 返回值:数组,包含两个值:状态值和修改该状态的函数
  3. useState`函数的返回值中,拿到状态和修改状态的方法
  4. JSX 中展示状态
  5. 修改状态的方法更新状态
function App() {
const result = useState(0);
const [count, setCount] = result;
const handleClick = (number) => {
setCount(number)
}
return (
<>
<button onClick={() => { handleClick(count + 1) }}>增加</button>
<div>Count: {count}</div>
</>
)
}
组件的更新过程:
组件第一次渲染:
1. 从头开始执行该组件中的代码逻辑
2. 调用 `useState(0)` 将传入的参数作为状态初始值,即:0
3. 渲染组件,此时,获取到的状态 count 值为: 0

组件第二次渲染, 每次调用 setCount 都会执行:
1. 点击按钮,调用 `setCount(number);` 修改状态,因为状态发生改变所以组件会重新渲染
2. 组件重新渲染时,会再次执行该组件中的代码逻辑
3. 再次调用 `useState(0)`,此时 React 内部会拿到最新的状态值而非初始值,比如,该案例中最新的状态值为 1
4. 再次渲染组件,此时获取到的状态 count 值为:1
5. 注意:useState 的初始值(参数)只会在组件第一次渲染时生效。也就是说,以后的每次渲染,useState 获取到都是最新的状态值,React 组件会记住每次最新的状态值

注意事项:

  1. useState 的参数, 作为 count 的初始值
  2. [count, setCount]是解构赋值, 名字可以自定义, 见明知意即可
  3. count, setCount, 顺序不能互换, 第一个表示数据状态, 第二个是修改数据的方法
  4. count 和 setCount 是一对, setCount 只能用来修改对应的 count 的值
  5. setCount 是一个函数,参数表示最新的状态值
  6. 调用该函数后,将使用新值替换旧值
  7. 修改状态后,由于状态发生变化,会引起视图变化
  8. 修改状态的时候,一定要使用新的状态替换旧的状态,不能直接修改旧的状态
案例1

记录当前窗口滑动距离

// useWindowScroll.js

// 引入 React 的 useState 钩子
import { useState } from "react";

// 定义一个名为 useWindowScroll 的自定义钩子函数
export function useWindowScroll() {
// 使用 useState 钩子创建一个状态变量 y 和设置状态的函数 setY,并将初始值设为 0
const [y, setY] = useState(0);

// 添加滚动事件监听器到 window 对象上
window.addEventListener("scroll", () => {
// 获取当前文档顶部相对于视口的垂直偏移量
const h = document.documentElement.scrollTop;
// 更新状态变量 y 的值为获取到的偏移量 h
setY(h);
});

// 返回状态变量 y,以便在组件中使用
return y;
}
import ReactDOM from "react-dom/client"; 
import { useWindowScroll } from "./other/useWindowScroll"; // 导入自定义的useWindowScroll钩子函数
const root = ReactDOM.createRoot(document.getElementById("root"));
function App() {
const y = useWindowScroll(); // 使用useWindowScroll钩子获取滚动位置
return(
<div>
<div style={{ position: "fixed", top: 0, left: 0, zIndex: 1000, backgroundColor: "white", padding: "10px" }}>
当前滑动距离: {y}
</div>
<div style={{ height: "12000px" }}>{y}</div>; // 返回一个包含滚动位置的div元素
</div>
)
}

root.render(<App />);
高级

语法选择

  1. 如果就是初始化一个普通的数据 直接使用 useState(普通数据) 即可
  2. 如果要初始化的数据无法直接得到需要通过计算才能获取到,使用useState(()=>{})
const [name, setName] = useState(() => {
// 编写计算逻辑 return '计算之后的初始值'
});
import ReactDOM from "react-dom/client";
import { useState } from "react";
const root = ReactDOM.createRoot(document.getElementById("root"));

function Counter(props) {
const [count, setCount] = useState(() => {
// 使用函数的返回值, 作为初始值
return props.count;
});
return (
<div>
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
);
}

root.render(
<>
<Counter count={10} />
<Counter count={20} />
</>
);

useEffect

函数副作用: 副作用是相对于主作用来说的,一个函数除了主作用,其他的作用就是副作用

// arrs.sort() ,除排序外还会修改原数组(返回的是原数组),可以说:sort()的主作用是排序,副作用是修改原数组
const arr = [6, 5, 4, 3, 2, 1];
const arr1 = arr.sort();
console.log(arr1);
console.log(arr);// 两次打印结果相同

// 解决方案:使用slice()获取新的数组对象
const arr = [6, 5, 4, 3, 2, 1];
const arr1 = arr.slice().sort();
console.log(arr1);
console.log(arr);// 两次打印结果不同

对于 React 组件来说,主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用。

常见 React 组件副作用:

  • 数据请求 ajax 发送
  • 手动修改 dom
  • localstorage 操作

useEffect 函数的作用就是:为 react 函数组件提供副作用处理

使用方法

  1. 导入 useEffect 函数
  2. 调用 useEffect 函数,并传入回调函数
  3. 在回调函数中编写副作用处理(dom 操作)
  4. (可选)回调函数中返回执行副作用清理函数

示例1:首次渲染+每次更新变量时执行useEffect()

// 1.导入 `useEffect` 函数
import { useEffect, useState } from "react";
import ReactDOM from "react-dom/client";

const root = ReactDOM.createRoot(document.getElementById("root"));

function App() {
const [count, setCount] = useState(0);
// 2. 调用 useEffect 函数,并传入回调函数
useEffect(() => {
console.log("副作用执行了....");
// 3. 在回调函数中编写副作用处理(dom 操作)
document.title = count + "!!!!";
});
return (
<div>
<button onClick={() => setCount(count + 1)}>增加</button>
<div>{count}</div>
</div>
);
}
root.render(
<>
<App />
</>
);

示例2:仅在首次渲染时执行useEffect()

  useEffect(() => {
console.log("副作用执行了....");
document.title = count + "!!!!";
}, []);// 副作用不依赖于任何属性或状态,不会在组件更新时重新执行

示例3:添加特定依赖项,首次渲染+特定依赖性发生变化时执行useEffect()

import { useEffect, useState } from "react";
import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(document.getElementById("root"));

function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState("张三");
useEffect(() => {
console.log("副作用执行了....");
document.title = count + "!!!!";
}, [count]);// 副作用的执行依赖于count数值的改变
return (
<div>
<button onClick={() => setCount(count + 1)}>增加</button>
<div>{count}</div>
<button onClick={() => setName("李四")}>改名字</button>
<div>{name}</div>
</div>
);
}
root.render(
<>
<App />
</>
);

示例4:useEffect()中返回一个副作用清理函数

import { useEffect, useState } from "react";
import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(document.getElementById("root"));

function Son() {

useEffect(() => {
const timeId = setInterval(() => {
console.log("副作用执行了...");
}, 1000);
// 清理副作用的代码, 组件销毁后会自动触发
return () => {
clearInterval(timeId);
};
});
return <div>this is son</div>;
}

function Parent() {
const [flag, setFlag] = useState(true);
return (
<>
<button onClick={() => setFlag(!flag)}>click</button>
{flag ? <Son /> : null}
</>
);
}

root.render(
<>
<Parent />
</>
);

示例5:useEffect()中异步执行函数

// npm install axios

import axios from "axios";
import { useEffect, useState } from "react";
import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(document.getElementById("root"));

function App() {
useEffect(() => {
async function getData() {
const response = await axios.get( "https://www.fastmock.site/mock/65fd811567052bc43a50412985949ab6/api/shop/1/tab/all");
console.log(response);
}
getData();
}, []);
return <></>;
}

root.render(
<>
<App />
</>
);

useRef

使用 ref 操作 DOM 有时可能需要访问由 React 管理的 DOM 元素 —— 例如,让一个节点获得焦点、滚动到它或测量它的尺寸和位置。在 React 中没有内置的方法来做这些事情,所以你需要一个指向 DOM 节点的 ref 来实现。

使用步骤

  1. 导入 useRef 函数
  2. 执行 useRef 函数并传入 null,返回值为一个对象, 内部有一个 current 属性存放拿到的 dom 对象(组件实例)
  3. ref 绑定要获取的元素或者组件
// 1. 导入 useRef 函数
import { useRef } from "react";

function Parent() {
const parentRef = useRef(null);//2.获得一个空ref对象
const hadleClickBtn =()=> {
console.log(parentRef.current);
}
// 3.给指定元素的ref属性赋值该ref对象
return (
<>
<button ref={parentRef}
onClick={() => hadleClickBtn()}>P.F.Ref</button>
</>
);
}

useContext

之前学过跨组件通信 Context, ProviderConsumer组合使用 接下来学习在 hook 中的用法, useContext:

  1. 使用createContext 创建 Context 对象
  2. 在顶层组件通过Provider 提供数据
  3. 层组件通过useContext函数获取数据
import { createContext,useState,useContext } from "react";
import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(document.getElementById("root"));
const Context = createContext();
function ComponentA() {
return (
<div>
this is component A
<ComponentC />
</div>
);
}
function ComponentC() {
const count = useContext(Context);
return <div>this is component C, Parent's count is :{count}</div>;
}

function Parent() {
const [count, setCount] = useState(0);
return (
<Context.Provider value={count}>
<div>
<button onClick={()=>setCount(count+1)}>click</button>
<ComponentA />
</div>
</Context.Provider>
);
}

root.render(
<>
<Parent />
</>
);

路由

相关概念

单页面应用SPA 单页 Web 应用(single page web application, SPA) 整个应用只有一个完整的页面 点击页面中的导航链接不会刷新页, 只会进行页面的局部更新 数据需要通过 ajax 请求获取 项目渲染出来只有一个 html 文件, 主流的开发模式变成了通过路由进行页面切换 优点: 避免整体页面刷新, 用户体验变好 缺点: 前端负责事情变多了, 开发的难度变大

路由的本质

  • 路由就是一组 key-value 的对应关系, 多个路由,需要经过路由器的管理
  • 有路由的概念, 是为了实现单页面应用 SPA, 所有数据在一个页面展示, 不跳转其他页面

Vite项目搭建

Vite Vs WebPack

Vite 和 Webpack 都是现代前端构建工具,用于优化开发流程和打包项目

方面ViteWebpack
开发速度极快(原生 ES Modules)较慢(全量打包)
生产构建Rollup(高效但配置灵活度较低)自建打包(高度灵活)
配置复杂度简单,约定大于配置复杂,需手动配置多数功能
生态插件较少(但够用)极其丰富
适用场景现代浏览器、中小型项目、快速迭代大型复杂项目、需要深度定制

环境准备

Vite中文文档

安装Vite:nodejs版本需18+或20+ (`node --version`)

创建项目:
1.新建一个文件夹,使用cmd切换至该文件夹下
2.执行 `npm create vite@latest`,使用命令行选项:
- 文件夹名称:react-router
- react
- JavaScript
3.`npm i`
4.`npm run dev`

安装react-router包: `npm i react-router-dom`

使用路由

新建Navigate.jsx

import {BrowserRouter, Routes, Route, Link} from "react-router-dom"

const Home =() => <div>home</div>
const Abount =() => <div>abount</div>

function Navigate() {
return (
<div className="navigate">
{/* 声明当前要用一个非hash模式的路由 */}
<BrowserRouter>
{/* 指定跳转的组件, to用来配置路由地址 */}
<Link to="/">
<button>首页</button>
</Link>
<Link to="/about">
<button>关于</button>
</Link>
{/* 路由出口, 路由对应的组件会在这里进行渲染 */}
<Routes>
{/* 指定路径和组件的对应关系, path代表路径, element代表组件, 成对出现, path ==> element */}
<Route path="/" element={<Home/>}></Route>
<Route path="/about" element={<Abount/>}></Route>
</Routes>
</BrowserRouter>
</div>
)
}

export default Navigate;

修改main.jsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import Navigate from './other/Navigate'

createRoot(document.getElementById('root')).render(
<StrictMode>
<Navigate/>
</StrictMode>,
)

react-router组件

React-Router文档

路由分类

BrowerRouter

作用: 包裹整个应用,一个 React 应用只需要使用一次

模式实现方式路由 url 表现
HashRouter监听 url hash 值实现http://localhost:3000/#/about
BrowserRouterh5 的 history.pushState API 实现http://localhost:3000/about
// 举例:使用HashRouter
function App() {
return (
<div className="App">
{/* 生命当前要用一个hash模式的路由 */}
<HashRouter>
<Link to="/"><button>首页</button></Link>
<Link to="/about"><button>关于</button></Link>
<Routes>
<Route path="/" element={<Home />}></Route>
<Route path="/about" element={<About />}></Route>
</Routes>
</HashRouter>
</div>
);
}
Routes

作用: 提供一个路由出口,组件内部会存在多个内置的 Route 组件,满足条件的路由会被渲染到组件内部

Route

作用: 用于定义路由路径和渲染组件的对应关系 其中 path 属性用来指定匹配的路径地址,element 属性指定要渲染的组件

<Routes>
<Route path="/" element={<Home />}></Route>
{/* 当 url 上访问的地址为 /about 时,当前路由发生匹配,对应的 About 组件渲染 */}
<Route path="/about" element={<About />}></Route>
</Routes>

引入单独的组件实现路由跳转:

// Login.jsx
import {useNavigate} from "react-router-dom"

function Login () {
// 必须写在组件内
const navigate = useNavigate();
return (
<div>
当前为登录页
<button onClick={()=>{
navigate("/")
}}>返回主页</button>
</div>
)
}
export default Login;
//Navigate.jsx
import {BrowserRouter, Routes, Route, Link} from "react-router-dom"

import Login from "./Login"

const Home =() => <div>home</div>
const Abount =() => <div>abount</div>

function Navigate() {
return (
<div className="navigate">
<BrowserRouter>
<Link to="/">
<button>首页</button>
</Link>
<Link to="/about">
<button>关于</button>
</Link>
<hr/>
<Routes>
<Route path="/" element={<Home/>}></Route>
<Route path="/about" element={<Abount/>}></Route>
<Route path="/login" element={<Login/>}></Route>
</Routes>
</BrowserRouter>
</div>
)
}

export default Navigate;

路由传参

searchParams 传参

获取路由参数:

// Login.jsx
import { useNavigate, useSearchParams } from "react-router-dom"

function Login() {
const navigate = useNavigate();
const [params] =useSearchParams();
const id = params.get("id");
return (
<div>
当前为登录页,用户id为{id}
<br/>
<button onClick={() => {
navigate("/")
}}>返回主页</button>
</div>
)
}
export default Login;

传递路由参数:http://localhost:5173/login?id=001

params 传参

获取路由参数:

import { BrowserRouter, Routes, Route, Link, useParams } from "react-router-dom"

import Login from "./Login"

const Home = () => <div>home</div>
const Abount = () => <div>abount</div>
const Book = () => {
let param = useParams();
// 获取路由参数
const postId = param.postId
return <div>book:{postId?postId:'默认书籍'}</div>
}

function Navigate() {
return (
<div className="navigate">
<BrowserRouter>
<Link to="/">
<button>首页</button>
</Link>
<Link to="/book">
<button>书籍</button>
</Link>
<Link to="/about">
<button>关于</button>
</Link>
<hr />
<Routes>
<Route path="/" element={<Home />}></Route>
{/*声明路由参数*/}
<Route path="/book/:postId" element={<Book />}></Route>
<Route path="/about" element={<Abount />}></Route>
<Route path="/login" element={<Login />}></Route>
</Routes>
</BrowserRouter>
</div>
)
}

export default Navigate;

传递路由参数:`http://localhost:5173/book/1984

嵌套路由

场景:在我们做的很多的管理后台系统中,通常我们都会设计一个 Layout 组件,在它内部实现嵌套路由

假设存在layout组件,其下包含Article Board两个子组件:

// Layout.jsx
import { Outlet } from "react-router-dom";
function Layout() {
return <div>layout</div>;
}
export default Layout;
// Board.jsx
function Board() {
return <div>Board</div>;
}
export default Board;
// Article.jsx
function Article() {
return <div>Article</div>;
}
export default Article;

App入口:

// App.jsx 定义路由
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Article from "./Article";
import Board from "./Board";
import Layout from "./Layout";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout></Layout>}>
<Route path="board" element={<Board></Board>}></Route>
<Route path="article" element={<Article></Article>}></Route>
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;

通过引入Outlet,为当前组件增加二级路由的入口:

// Layout.jsx
import { Outlet } from "react-router-dom";
function Layout() {
return (
<div>
layout
{/* 二级路由的出口 */}
<Outlet></Outlet>
</div>
);
}
export default Layout;

默认路由

场景: 应用首次渲染完毕就需要显示的二级路由

实现步骤:

  1. 给默认二级路由标记 index 属性
  2. 原本的路径 path 属性去掉
// App.jsx
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Article from "./Article";
import Board from "./Board";
import Layout from "./Layout";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout></Layout>}>
<Route index element={<Board></Board>}></Route>
<Route path="article" element={<Article></Article>}></Route>
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;

404路由配置

当路由未命中时,配置兜底的路由组件

// NotFound.jsx
function NotFound() {
return <div>NotFound</div>;
}
export default NotFound;
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Article from "./Article";
import Board from "./Board";
import Layout from "./Layout";
import NotFound from "./NotFound";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout></Layout>}>
<Route index element={<Board></Board>}></Route>
<Route path="article" element={<Article></Article>}></Route>
</Route>
{/* 如果没有匹配成功, 走这个路由 */}
<Route path="*" element={<NotFound />}></Route>
</Routes>
</BrowserRouter>
);
}
export default App;

集中式路由配置

import { BrowserRouter, Route, Routes, useRoutes } from "react-router-dom";
import Article from "./Article";
import Board from "./Board";
import Layout from "./Layout";
import NotFound from "./NotFound";

// 1. 准备一个路由数组 数组中定义所有的路由对应关系
const routesList = [
{
path: "/",
element: <Layout />,
children: [
{
element: <Board />,
index: true, // index设置为true 变成默认的二级路由
},
{
path: "article",
element: <Article />,
},
],
},
// 增加n个路由对应关系
{
path: "*",
element: <NotFound />,
},
];

// 2. 使用useRoutes方法传入routesList生成Routes组件
function WrapperRoutes() {
let element = useRoutes(routesList);
return element;
}

function App() {
return (
<BrowserRouter>
{/* 3. 替换之前的Routes组件 */}
<WrapperRoutes />
</BrowserRouter>
);
}
export default App;