React入门笔记
后端程序员的React入门笔记
参考来源:
相关介绍
概览
- React 英文文档 https://react.dev/
- React 中文文档 https://zh-hans.react.dev/
特性
- 声明式设计 — React 采用声明范式,可以轻松描述应用。
- 高效, 通过对 DOM 的模拟(虚拟 dom), 最大限度地减少与 DOM 的交互。
- 灵活 — React 可以与已知的库或框架很好地配合。
- JSX — JSX 是 JavaScript 语法的扩展
- 组件 — 通过 React 构建组件,使得代码更加容易得到复用,能够很好的应用在顶目的开发中。
- 单向响应的数据流 — React 实现了单向响应的数据流
虚拟 DOM
- DOM 操作非常昂贵
- 在前端开发中,性能消耗最大的就是 DOM 操作,而且这部分代码会让整体项目的代码变得难以维护
- React 把真实 DOM 树转换成 JavaScript 对象树,也就是 Virtual DOM
优点
- 系出名门, facebook 开发
- 一个专注于构建用户界面的 JavaScript 框架,和 vue 和 angular 并称前端三大框架
- React 的出现让创建交互式 UI 变得轻而易举
- 函数和类就是 UI 的载体, 将数据传入 React 的类和函数中,返回的就是 UI 界面
- React 应用中,一切皆组件
- React 还具有跨平台能力, Node 进行服务器渲染,还可以用 React Native 进行原生移动应用的开发
- 小厂喜欢 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
表达式
可以使用的表达式
- 字符串、数值、布尔值、null、undefined、object( [] / )
- 1 + 2、‘abc’.split(”)、[‘a’, ‘b’].join(’-‘)
- fn()
函数的返回值有两种书写方式
- 花括号+return 返回值:
function getAge=()=>{return 18}
- 圆括号+返回值:
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组件
函数组件
- 组件的名称必须首字母大写(大驼峰的写法),react 内部会根据这个来判断是组件还是普通的 HTML 标签
- 函数组件必须有返回值,表示该组件的 UI 结构;
- 如果不需 要渲染任何内容,则返回 null
- 组件就像 HTML 标签一样可以被渲染到页面中。
- 组件表示的是一段结构内容,对于函数组件来说,渲染的内容是函数的返回值
- 使用函数名称作为组件标签名称,可以是双标签也可以是单标签
function HelloFunctionComponenet(){
return <h1>hello world.</h1>
}
root.render(
<HelloFunctionComponenet/>
);
类组件
- 类名称也必须以大写字母开头, 大驼峰写法
- 类组件应该继承
React.Component
父类,从而使用父类中提供的方法或属性 - 类组件必 须提供
render
方法 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>
);
组件属性
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校验包对传入的数据进行校验
- 安装属性校验包:
npm install prop-types
- 导入
prop-types
包- 使用
组件名.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的作用
- 有了 hooks 之后,为了兼容老版本,class 类组件并没有被移除,俩者都可以使用
- 有了 hooks 之后,不能在把函数称为无状态组件了,因为 hooks 为函数组件提供了状态
- hooks 只能在函数组件中使用
- 组件的状态逻辑复用,在 hooks 出现之前,react 先后尝试了 mixins 混入,HOC 高阶组件,render-props 等模式, 但是都有各自的问题,比如 mixin 的数据来源不清晰,高阶组件的嵌套问题等等
- class 组件自身的问题, class 组件就像一个厚重的‘战舰’ 一样,大而全,提供了很多东西,有不可忽视的学习成本,比如各种生命周期,this 指向问题等等,而我们更多时候需要的是一个轻快灵活的’快艇’
useState
基础
useState 为函数组件提供状态(state),属于hooks的一种
使用方法:
- 导入
useState
函数 - 调用
useState
函数,并传入状态的初始值, 返回值:数组,包含两个值:状态值和修改该状态的函数 - useState`函数的返回值中,拿到状态和修改状态的方法
- JSX 中展示状态
- 修改状态的方法更新状态
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 组件会记住每次最新的状态值
注意事项:
- useState 的参数, 作为 count 的初始值
[count, setCount]
是解构赋值, 名字可以自定义, 见明知意即可- count, setCount, 顺序不能互换, 第一个表示数据状态, 第二个是修改数据的方法
- count 和 setCount 是一对, setCount 只能用来修改对应的 count 的值
- setCount 是一个函数,参数表示
最新的状态值
- 调用该函数后,将使用新值替换旧值
- 修改状态后,由于状态发生变化,会引起视图变化
- 修改状态的时候,一定要使用新的状态替换旧的状态,不能直接修改旧的状态
案例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 />);
高级
语法选择
- 如果就是初始化一个普通的数据 直接使用
useState(普通数据)
即可 - 如果要初始化的数据无法直接 得到需要通过计算才能获取到,使用
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 函数组件提供副作用处理
使用方法:
- 导入
useEffect
函数 - 调用
useEffect
函数,并传入回调函数 - 在回调函数中编写副作用处理(dom 操作)
- (可选)回调函数中返回执行副作用清理函数
示例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 来实现。
使用步骤
- 导入
useRef
函数- 执行
useRef
函数并传入 null,返回值为一个对象, 内部有一个 current 属性存放拿到的 dom 对象(组件实例)- 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,
Provider
和Consumer
组合使用 接下来学习在 hook 中的用法, useContext:
- 使用
createContext
创建 Context 对象- 在顶层组件通过
Provider
提供数据- 层组件通过
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 都是现代前端构 建工具,用于优化开发流程和打包项目
方面 | Vite | Webpack |
---|---|---|
开发速度 | 极快(原生 ES Modules) | 较慢(全量打包) |
生产构建 | Rollup(高效但配置灵活度较低) | 自建打包(高度灵活) |
配置复杂度 | 简单,约定大于配置 | 复杂,需手动配置多数功能 |
生态插件 | 较少(但够用) | 极其丰富 |
适用场景 | 现代浏览器、中小型项目、快速迭代 | 大型复杂项目、需要深度定制 |
环境准备
安装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组件
路由分类
BrowerRouter
作用: 包裹整个应用,一个 React 应用只需要使用一次
模式 | 实现方式 | 路由 url 表现 |
---|---|---|
HashRouter | 监听 url hash 值实现 | http://localhost:3000/#/about |
BrowserRouter | h5 的 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;