前言
最近刚学完 React,想着把笔记分享给大家,本笔记特别适合从事后端想要学习前端的人。我看视频是黑马最新的 React 视频(黑马程序员前端React18入门到实战视频教程,从react+hooks核心基础到企业级项目开发实战(B站评论、极客园项目等)及大厂面试全通关_哔哩哔哩_bilibili),个人觉得讲得还不错的。想要完整版可以私信我,如果对你有帮助的话就点个赞关注下吧。后面持续分享 Java 相关技术和笔记。
一、React 基础
1. 创建一个 react 项目
1.利用 create-react-app 工具创建一个 react 项目
npx create-react-app project-name npm start # 启动项目
2.src 目录只保留 App.js 和 index.js 文件
3.精简 App.js 和 index.js 文件
1.1 src 目录下文件的作用
index.js 是项目的入口,从这里开始运行,App 是根组件被 Index.js 导入,最后渲染到 index.html 中 root 节点上
index.js:
// 项目的核心入口 从这里开始运行// React 必要的两个核心包 import React from 'react'; import ReactDOM from 'react-dom/client';// 导入项目的根组件 import App from './App';// 把 App 根组件渲染到 id 为 root 的 dom 节点上 const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<App /> );
App:
// 项目的根组件// App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上 function App() {return (<div className="App">this is React App</div>); }export default App;
2. jsx 基础-概念和本质
2.1 JSX 是什么?
JSX 表示在 JS 代码中编写 HTML 模板结构,是 React 中编写 UI 模板的方式
优势:
-
HTML 的声明式模板写法
-
JS 的可编程能力
JSX 是 JS 的拓展,浏览器不可直接识别,需要解析工具解析才可识别
2.2 JSX 编写 JS 代码
在 jsx 中可通过大括号 {} 识别 js 表达式,比如常见的变量、函数调用、方法调用等等
App.js:
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上function getName() {return 'jack'; }const count = 100; function App() {return (<div className="App">this is React App{/*1. 引号传递字符串*/}{'this is message'}{/*2. 识别 js 变量*/}{count}{/*3. 函数调用*/}{ getName() }{/*4. 方法调用*/}{new Date().getDate()}{/*5. 使用 js 对象*/}<div style={{ color: 'red' }}>this is div</div></div>); }export default App;
2.3 JSX 中实现列表渲染
提示:在 JSX 中可以使用原生 JS 中 map 方法遍历渲染列表
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上const count = 100;const list = [{id: 1001, name: 'Vue'},{id: 1002, name: 'React'},{id: 1003, name: 'Angular'}, ];function App() {return (<div className="App">this is App{/*渲染列表*/}{list.map(item => <li key={item.id}>{item.name}</li>)}</div>); }export default App;
注意:
-
渲染哪个结构就 return 那个
-
循环渲染记得要加上独一无二的 key(类型为 string 或 number)
2.4 JSX 实现条件渲染
在 React 中,可以通过逻辑与运算符 &&、三元表达式(?:)实现*基础的条件渲染
类似 Vue 的 v-if
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上const isLogin = true;function App() {return (<div className="App">{/*1. 逻辑与 &&*/}{isLogin && <span>this is span</span>}<br/>{/*2. 三元运算*/}{isLogin ? <span>is Login</span> : <span>not Login</span>}</div>); }export default App;
2.4.1 JSX 条件渲染的 demo
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上const articleType = 3; // 0 1 3 'articleType' 的取值范围// 定义核心函数(根据文章类型返回不同的 JSX 模板) function getArticleTemplate() {if (articleType === 0) {return <div>我是图文文章</div>} else if (articleType === 1) {return <div>我是单图文章</div>} else {return <div>我是三图文章</div>}}function App() {return (<div className="App">{/*调用函数渲染不同模板*/}{getArticleTemplate()}</div>); }export default App;
显示:我是三图文章
3. React 基础事件绑定
语法:on + 事件名 = {事件处理程序/函数名},遵循驼峰命令
1.绑定事件
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上function App() {const handleClick = () => {console.log('button 被点击了');}return (<div className="App"><button onClick={handleClick}>click me</button></div>); }export default App;
2.传递事件参数 e
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上function App() {// 拿到事件参数 econst handleClick = (e) => {console.log('button 被点击了', e);}return (<div className="App"><button onClick={handleClick}>click me</button></div>); }export default App;
3.传递自定义参数
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上function App() {// 传递自定义参数const handleClick = (name) => {console.log('button 被点击了', name);}return (<div className="App">{/*箭头函数传参*/}<button onClick={() => handleClick('jack')}>click me</button></div>); }export default App;
4.同时传递自定义参数和事件参数 e
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上function App() {// const handleClick = () => {// console.log('button 被点击了');// }// 拿到事件参数 e// const handleClick = (e) => {// console.log('button 被点击了', e);// }const handleClick = (name, e) => {console.log('button 被点击了', name, e);}return (<div className="App">{/*箭头函数传参*/}<button onClick={(e) => handleClick('jack', e)}>click me</button></div>); }export default App;
4. React组件
在 React 中,一个组件就是一个首字母大写的函数,内部含有组件的逻辑和 UI,渲染组件只需将组件当做标签书写即可
1.定义组件(function 定义或者箭头函数)
App.js:
function Button() {// 组件逻辑return <button>click me</button> }const Button = () => {// 组件逻辑return <button>click me</button> }
2.渲染组件(自闭和或成对标签)
function App() {return (<div className="App">{/*自闭和*/}<Button />{/*成对标签*/}<Button></Button></div>); }
5. useState 基础使用
其是 React 的一个 Hook,允许我们向组件添加一个状态变量,从而控制影响组件的渲染结果
const [count, setCount] = useState(0);
count 的值不可直接修改,只能通过 setCount 修改
本质:状态变量一旦发生变化组件的视图 UI 也会变化(数据驱动视图)
特点:
-
useState 是一个函数,返回值是一个数组
-
数组的第一个参数是状态变量,第二个参数是 set 函数用来修改状态变量
-
useState 的参数将作为 count 的初始值
一个 useState 的小 demo
App.js:
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上// useState 实现一个计数器按钮 import {useState} from "react";function App() {// 1. 调用 useState 添加一个状态变量// count 状态变量// setCount 修改状态变量const [count, setCount] = useState(0);// 2. 点击按钮的回调const handleClick = () => {// 作用:1.用传入的新值修改 count// 2.重新使用新的 count 渲染 UIsetCount(count + 1);};return (<div className="App"><button onClick={handleClick}>{count}</button></div>); }export default App;
拓展 demo
App.js:
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上// useState 实现一个计数器按钮 import {useState} from "react";function App() {// 修改对象const [form, setForm] = useState({name: 'jack'});const changeForm = () => {// 错误写法:直接修改// form.name = 'john';// 正确写法:setForm 传入一个全新的对象setForm({...form,name: 'john',})};return (<div className="App"><button onClick={changeForm}>修改 form {form.name}</button></div>); }export default App;
6. 如何修改组件样式
App.js:
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上// 导入样式 import './index.css' const style = {color: 'red',fontSize: '50px', };function App() {return (<div className="App">{/*行内控制*/}<span style={{color: 'red', fontSize: '50px'}}>this is span</span><span style={style}>this is span</span>{/*通过 class 类名控制*/}<span className='foo'>this is foo</span></div>); }export default App;
index.css:
.foo {color: blue; }
7. B 站评论案例
7.1 列表渲染
App.js:
import './App.scss' import avatar from './images/bozai.png' import {useState} from "react";/*** 评论列表的渲染和操作** 1. 根据状态渲染评论列表* 2. 删除评论*/// 评论列表数据 const list = [{// 评论idrpid: 3,// 用户信息user: {uid: '13258165',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '周杰伦',},// 评论内容content: '哎哟,不错哦',// 评论时间ctime: '10-18 08:15',like: 88,},{rpid: 2,user: {uid: '36080105',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '许嵩',},content: '我寻你千百度 日出到迟暮',ctime: '11-13 11:29',like: 88,},{rpid: 1,user: {uid: '30009257',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '黑马前端',},content: '学前端就来黑马',ctime: '10-19 09:00',like: 66,}, ]// 当前登录用户信息 const user = {// 用户iduid: '30009257',// 用户头像avatar,// 用户昵称uname: '黑马前端', }/*** 导航 Tab 的渲染和操作** 1. 渲染导航 Tab 和高亮* 2. 评论列表排序* 最热 => 喜欢数量降序* 最新 => 创建时间降序*/// 导航 Tab 数组 const tabs = [{ type: 'hot', text: '最热' },{ type: 'time', text: '最新' }, ]// 渲染评论列表 // 1.使用 useState 维护 listconst App = () => {const [commentList, setCommentList] = useState(list);return (<div className="app">{/* 导航 Tab */}<div className="reply-navigation"><ul className="nav-bar"><li className="nav-title"><span className="nav-title-text">评论</span>{/* 评论数量 */}<span className="total-reply">{10}</span></li><li className="nav-sort">{/* 高亮类名: active */}<span className='nav-item'>最新</span><span className='nav-item'>最热</span></li></ul></div><div className="reply-wrap">{/* 发表评论 */}<div className="box-normal">{/* 当前用户头像 */}<div className="reply-box-avatar"><div className="bili-avatar"><img className="bili-avatar-img" src={avatar} alt="用户头像" /></div></div><div className="reply-box-wrap">{/* 评论框 */}<textareaclassName="reply-box-textarea"placeholder="发一条友善的评论"/>{/* 发布按钮 */}<div className="reply-box-send"><div className="send-text">发布</div></div></div></div>{/* 评论列表 */}<div className="reply-list">{/* 评论项 */}{commentList.map(item => (<div key={item.rpid} className="reply-item">{/* 头像 */}<div className="root-reply-avatar"><div className="bili-avatar"><imgclassName="bili-avatar-img"alt=""src={item.user.avatar}/></div></div><div className="content-wrap">{/* 用户名 */}<div className="user-info"><div className="user-name">{item.user.uname}</div></div>{/* 评论内容 */}<div className="root-reply"><span className="reply-content">{item.content}</span><div className="reply-info">{/* 评论时间 */}<span className="reply-time">{item.ctime}</span>{/* 评论数量 */}<span className="reply-time">点赞数:{item.like}</span><span className="delete-btn">删除</span></div></div></div></div>))}</div></div></div>) }export default App
App.scss:
.app {width: 80%;margin: 50px auto; }.reply-navigation {margin-bottom: 22px;.nav-bar {display: flex;align-items: center;margin: 0;padding: 0;list-style: none;.nav-title {display: flex;align-items: center;width: 114px;font-size: 20px;.nav-title-text {color: #18191c;font-weight: 500;}.total-reply {margin: 0 36px 0 6px;color: #9499a0;font-weight: normal;font-size: 13px;}}.nav-sort {display: flex;align-items: center;color: #9499a0;font-size: 13px;.nav-item {cursor: pointer;&:hover {color: #00aeec;}&:last-child::after {display: none;}&::after {content: ' ';display: inline-block;height: 10px;width: 1px;margin: -1px 12px;background-color: #9499a0;}}.nav-item.active {color: #18191c;}}} }.reply-wrap {position: relative; } .box-normal {display: flex;transition: 0.2s;.reply-box-avatar {display: flex;align-items: center;justify-content: center;width: 80px;height: 50px;}.reply-box-wrap {display: flex;position: relative;flex: 1;.reply-box-textarea {width: 100%;height: 50px;padding: 5px 10px;box-sizing: border-box;color: #181931;font-family: inherit;line-height: 38px;background-color: #f1f2f3;border: 1px solid #f1f2f3;border-radius: 6px;outline: none;resize: none;transition: 0.2s;&::placeholder {color: #9499a0;font-size: 12px;}&:focus {height: 60px;background-color: #fff;border-color: #c9ccd0;}}}.reply-box-send {position: relative;display: flex;flex-basis: 86px;align-items: center;justify-content: center;margin-left: 10px;border-radius: 4px;cursor: pointer;transition: 0.2s;& .send-text {position: absolute;z-index: 1;color: #fff;font-size: 16px;}&::after {position: absolute;width: 100%;height: 100%;background-color: #00aeec;border-radius: 4px;opacity: 0.5;content: '';}&:hover::after {opacity: 1;}} } .bili-avatar {position: relative;display: block;width: 48px;height: 48px;margin: 0;padding: 0;border-radius: 50%; } .bili-avatar-img {position: absolute;top: 50%;left: 50%;display: block;width: 48px;height: 48px;object-fit: cover;border: none;border-radius: 50%;image-rendering: -webkit-optimize-contrast;transform: translate(-50%, -50%); }// 评论列表 .reply-list {margin-top: 14px; } .reply-item {padding: 22px 0 0 80px;.root-reply-avatar {position: absolute;left: 0;display: flex;justify-content: center;width: 80px;cursor: pointer;}.content-wrap {position: relative;flex: 1;&::after {content: ' ';display: block;height: 1px;width: 100%;margin-top: 14px;background-color: #e3e5e7;}.user-info {display: flex;align-items: center;margin-bottom: 4px;.user-name {height: 30px;margin-right: 5px;color: #61666d;font-size: 13px;line-height: 30px;cursor: pointer;}}.root-reply {position: relative;padding: 2px 0;color: #181931;font-size: 15px;line-height: 24px;.reply-info {position: relative;display: flex;align-items: center;margin-top: 2px;color: #9499a0;font-size: 13px;.reply-time {width: 86px;margin-right: 20px;}.reply-like {display: flex;align-items: center;margin-right: 19px;.like-icon {width: 14px;height: 14px;margin-right: 5px;color: #9499a0;background-position: -153px -25px;&:hover {background-position: -218px -25px;}}.like-icon.liked {background-position: -154px -89px;}}.reply-dislike {display: flex;align-items: center;margin-right: 19px;.dislike-icon {width: 16px;height: 16px;background-position: -153px -153px;&:hover {background-position: -217px -153px;}}.dislike-icon.disliked {background-position: -154px -217px;}}.delete-btn {cursor: pointer;&:hover {color: #00aeec;}}}}} }.reply-none {height: 64px;margin-bottom: 80px;color: #99a2aa;font-size: 13px;line-height: 64px;text-align: center; }
7.2 删除功能实现
需求:
-
只有自己的评论才可以删除
-
点击删除按钮,删除当前评论,列表中不再显示
核心思路:
-
删除显示 - 条件渲染
-
删除功能 - 拿到当前项 id 以 id 为条件对评论列表做过滤
7.3 渲染 Tab + 点击高亮实现
需求:点击哪个 tab 项,哪个做高亮处理
核心思路:
点击谁就把谁的type(独一无二的标识)记录下来,然后和遍历时的每一项的type做匹配,谁匹配到就设置负责高亮的类名
import './App.scss' import avatar from './images/bozai.png' import {useState} from "react";/*** 评论列表的渲染和操作** 1. 根据状态渲染评论列表* 2. 删除评论*/// 评论列表数据 const list = [{// 评论idrpid: 3,// 用户信息user: {uid: '13258165',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '周杰伦',},// 评论内容content: '哎哟,不错哦',// 评论时间ctime: '10-18 08:15',like: 88,},{rpid: 2,user: {uid: '36080105',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '许嵩',},content: '我寻你千百度 日出到迟暮',ctime: '11-13 11:29',like: 88,},{rpid: 1,user: {uid: '30009257',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '黑马前端',},content: '学前端就来黑马',ctime: '10-19 09:00',like: 66,}, ]// 当前登录用户信息 const user = {// 用户iduid: '30009257',// 用户头像avatar,// 用户昵称uname: '黑马前端', }/*** 导航 Tab 的渲染和操作** 1. 渲染导航 Tab 和高亮* 2. 评论列表排序* 最热 => 喜欢数量降序* 最新 => 创建时间降序*/// 导航 Tab 数组 const tabs = [{ type: 'hot', text: '最热' },{ type: 'time', text: '最新' }, ]const App = () => {// 渲染评论列表// 1.使用 useState 维护 listconst [commentList, setCommentList] = useState(list);// 删除功能const handleDel = (id) => {// 对 commentList 进行过滤setCommentList(commentList.filter(item => item.rpid !== id));};// tab 切换功能// 1.点击谁就把谁的 type 记录下来// 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示const [type, setType] = useState('hot');const handleTabChange = (type) => {console.log(type);setType(type);};return (<div className="app">{/* 导航 Tab */}<div className="reply-navigation"><ul className="nav-bar"><li className="nav-title"><span className="nav-title-text">评论</span>{/* 评论数量 */}<span className="total-reply">{10}</span></li><li className="nav-sort">{/* 高亮类名: active */}{tabs.map(item =><span key={item.type}onClick={() => handleTabChange(item.type)}className={`nav-item ${type === item.type && 'active'}`}>{item.text}</span>)}</li></ul></div><div className="reply-wrap">{/* 发表评论 */}<div className="box-normal">{/* 当前用户头像 */}<div className="reply-box-avatar"><div className="bili-avatar"><img className="bili-avatar-img" src={avatar} alt="用户头像" /></div></div><div className="reply-box-wrap">{/* 评论框 */}<textareaclassName="reply-box-textarea"placeholder="发一条友善的评论"/>{/* 发布按钮 */}<div className="reply-box-send"><div className="send-text">发布</div></div></div></div>{/* 评论列表 */}<div className="reply-list">{/* 评论项 */}{commentList.map(item => (<div key={item.rpid} className="reply-item">{/* 头像 */}<div className="root-reply-avatar"><div className="bili-avatar"><imgclassName="bili-avatar-img"alt=""src={item.user.avatar}/></div></div><div className="content-wrap">{/* 用户名 */}<div className="user-info"><div className="user-name">{item.user.uname}</div></div>{/* 评论内容 */}<div className="root-reply"><span className="reply-content">{item.content}</span><div className="reply-info">{/* 评论时间 */}<span className="reply-time">{item.ctime}</span>{/* 评论数量 */}<span className="reply-time">点赞数:{item.like}</span>{/*条件:user.id === item.user.id*/}{user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}</div></div></div></div>))}</div></div></div>) }export default App
7.4 排序功能
需求:点击最新,评论列表按照创建时间倒序排列(新的在前),点击最热按照点赞数排序(多的在前)
核心思路:把评论列表状态数据进行不同的排序处理,当成新值传给 set 函数重新渲染视图 UI
lodash 库
安装:
npm install lodash
引入:
import _ from 'lodash';
使用:
setCommentList(_.orderBy(commentList, 'like', 'desc'));
App.js:
import './App.scss' import avatar from './images/bozai.png' import {useState} from "react"; import _ from 'lodash'; /*** 评论列表的渲染和操作** 1. 根据状态渲染评论列表* 2. 删除评论*/// 评论列表数据 const list = [{// 评论idrpid: 3,// 用户信息user: {uid: '13258165',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '周杰伦',},// 评论内容content: '哎哟,不错哦',// 评论时间ctime: '10-20 08:15',like: 38,},{rpid: 2,user: {uid: '36080105',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '许嵩',},content: '我寻你千百度 日出到迟暮',ctime: '09-13 11:29',like: 88,},{rpid: 1,user: {uid: '30009257',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '黑马前端',},content: '学前端就来黑马',ctime: '10-19 09:00',like: 66,}, ]// 当前登录用户信息 const user = {// 用户iduid: '30009257',// 用户头像avatar,// 用户昵称uname: '黑马前端', }/*** 导航 Tab 的渲染和操作** 1. 渲染导航 Tab 和高亮* 2. 评论列表排序* 最热 => 喜欢数量降序* 最新 => 创建时间降序*/// 导航 Tab 数组 const tabs = [{ type: 'hot', text: '最热' },{ type: 'time', text: '最新' }, ]const App = () => {// 渲染评论列表// 1.使用 useState 维护 listconst [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));// 删除功能const handleDel = (id) => {// 对 commentList 进行过滤setCommentList(commentList.filter(item => item.rpid !== id));};// tab 切换功能// 1.点击谁就把谁的 type 记录下来// 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示const [type, setType] = useState('hot');const handleTabChange = (type) => {console.log(type);setType(type);// 基于列表的排序if (type === 'hot') {// 根据点赞数量排序// lodashsetCommentList(_.orderBy(commentList, 'like', 'desc'));} else {// 根据创建时间排序setCommentList(_.orderBy(commentList, 'ctime', 'desc'))}};return (<div className="app">{/* 导航 Tab */}<div className="reply-navigation"><ul className="nav-bar"><li className="nav-title"><span className="nav-title-text">评论</span>{/* 评论数量 */}<span className="total-reply">{10}</span></li><li className="nav-sort">{/* 高亮类名: active */}{tabs.map(item =><span key={item.type}onClick={() => handleTabChange(item.type)}className={`nav-item ${type === item.type && 'active'}`}>{item.text}</span>)}</li></ul></div><div className="reply-wrap">{/* 发表评论 */}<div className="box-normal">{/* 当前用户头像 */}<div className="reply-box-avatar"><div className="bili-avatar"><img className="bili-avatar-img" src={avatar} alt="用户头像" /></div></div><div className="reply-box-wrap">{/* 评论框 */}<textareaclassName="reply-box-textarea"placeholder="发一条友善的评论"/>{/* 发布按钮 */}<div className="reply-box-send"><div className="send-text">发布</div></div></div></div>{/* 评论列表 */}<div className="reply-list">{/* 评论项 */}{commentList.map(item => (<div key={item.rpid} className="reply-item">{/* 头像 */}<div className="root-reply-avatar"><div className="bili-avatar"><imgclassName="bili-avatar-img"alt=""src={item.user.avatar}/></div></div><div className="content-wrap">{/* 用户名 */}<div className="user-info"><div className="user-name">{item.user.uname}</div></div>{/* 评论内容 */}<div className="root-reply"><span className="reply-content">{item.content}</span><div className="reply-info">{/* 评论时间 */}<span className="reply-time">{item.ctime}</span>{/* 评论数量 */}<span className="reply-time">点赞数:{item.like}</span>{/*条件:user.id === item.user.id*/}{user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}</div></div></div></div>))}</div></div></div>) }export default App
8. classnams 优化类名控制
classnams 是一个简单的 JS 库,可以非常方便的通过条件动态控制 class 类名的显示
以前出现的问题:
语法:key 表示要控制的类名,value 表示条件,true 的时候类名就会显示
使用
安装:
npm install classnames
引入:
import classNames from "classnames";
用法:
<span key={item.type}onClick={() => handleTabChange(item.type)}className={classNames('nav-item', {active: type === item.type})}>{item.text}</span>)}
9. 受表单控制项
概念:使用 React 组件的状态(useState)控制表单状态
App.js
// 项目的根组件 // App -> index.js -> public/index.html(root) => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上// 1.声明一个 react 状态 - useState // 2.核心绑定流程 // 2.1通过 value 属性绑定 react 状态 // 2.2绑定 onChange 事件,通过事件参数 e 拿到输入框最新值,反向修改 react 状态import {useState} from "react";function App() {const [value, setValue] = useState();return (<div className="App"><inputvalue={value}onChange={(e) => setValue(e.target.value)}type='text'/></div>); }export default App;
10. React 中获取DOM
在 React 中获取/操作 DOM,需要使用 useRef 钩子函数,分为两步:
-
使用 useRef 创建 ref 对象,并与 JSX 绑定
-
在 DOM 可用/DOM 渲染完毕时,通过 inputRef.current 拿到 DOM 对象
App.js:
import React, { useRef } from "react";function App() {const inputRef = useRef(null);const showDom = () => {console.log(inputRef.current);};const setInputValue = () => {inputRef.current.value = '新的值';};const focusInput = () => {inputRef.current.focus();};const selectInputText = () => {inputRef.current.select();};const addClassToInput = () => {inputRef.current.classList.add('new-class');};const removeClassFromInput = () => {inputRef.current.classList.remove('new-class');};const addEventListenerToInput = () => {inputRef.current.addEventListener('input', (event) => {console.log('Input changed:', event.target.value);});};return (<div className="App"><input ref={inputRef} type='text'/><button onClick={showDom}>获取 dom</button><button onClick={setInputValue}>设置值</button><button onClick={focusInput}>聚焦</button><button onClick={selectInputText}>选择文本</button><button onClick={addClassToInput}>添加类</button><button onClick={removeClassFromInput}>移除类</button><button onClick={addEventListenerToInput}>添加事件监听器</button></div>); }export default App;
11.B 站评论优化
11.1 发表评论
App.scss:
.app {width: 80%;margin: 50px auto; }.reply-navigation {margin-bottom: 22px;.nav-bar {display: flex;align-items: center;margin: 0;padding: 0;list-style: none;.nav-title {display: flex;align-items: center;width: 114px;font-size: 20px;.nav-title-text {color: #18191c;font-weight: 500;}.total-reply {margin: 0 36px 0 6px;color: #9499a0;font-weight: normal;font-size: 13px;}}.nav-sort {display: flex;align-items: center;color: #9499a0;font-size: 13px;.nav-item {cursor: pointer;&:hover {color: #00aeec;}&:last-child::after {display: none;}&::after {content: ' ';display: inline-block;height: 10px;width: 1px;margin: -1px 12px;background-color: #9499a0;}}.nav-item.active {color: #18191c;}}} }.reply-wrap {position: relative; } .box-normal {display: flex;transition: 0.2s;.reply-box-avatar {display: flex;align-items: center;justify-content: center;width: 80px;height: 50px;}.reply-box-wrap {display: flex;position: relative;flex: 1;.reply-box-textarea {width: 100%;height: 50px;padding: 5px 10px;box-sizing: border-box;color: #181931;font-family: inherit;line-height: 38px;background-color: #f1f2f3;border: 1px solid #f1f2f3;border-radius: 6px;outline: none;resize: none;transition: 0.2s;&::placeholder {color: #9499a0;font-size: 12px;}&:focus {height: 60px;background-color: #fff;border-color: #c9ccd0;}}}.reply-box-send {position: relative;display: flex;flex-basis: 86px;align-items: center;justify-content: center;margin-left: 10px;border-radius: 4px;cursor: pointer;transition: 0.2s;& .send-text {position: absolute;z-index: 1;color: #fff;font-size: 16px;}&::after {position: absolute;width: 100%;height: 100%;background-color: #00aeec;border-radius: 4px;opacity: 0.5;content: '';}&:hover::after {opacity: 1;}} } .bili-avatar {position: relative;display: block;width: 48px;height: 48px;margin: 0;padding: 0;border-radius: 50%; } .bili-avatar-img {position: absolute;top: 50%;left: 50%;display: block;width: 48px;height: 48px;object-fit: cover;border: none;border-radius: 50%;image-rendering: -webkit-optimize-contrast;transform: translate(-50%, -50%); }// 评论列表 .reply-list {margin-top: 14px; } .reply-item {padding: 22px 0 0 80px;.root-reply-avatar {position: absolute;left: 0;display: flex;justify-content: center;width: 80px;cursor: pointer;}.content-wrap {position: relative;flex: 1;&::after {content: ' ';display: block;height: 1px;width: 100%;margin-top: 14px;background-color: #e3e5e7;}.user-info {display: flex;align-items: center;margin-bottom: 4px;.user-name {height: 30px;margin-right: 5px;color: #61666d;font-size: 13px;line-height: 30px;cursor: pointer;}}.root-reply {position: relative;padding: 2px 0;color: #181931;font-size: 15px;line-height: 24px;.reply-info {position: relative;display: flex;align-items: center;margin-top: 2px;color: #9499a0;font-size: 13px;.reply-time {width: 86px;margin-right: 20px;}.reply-like {display: flex;align-items: center;margin-right: 19px;.like-icon {width: 14px;height: 14px;margin-right: 5px;color: #9499a0;background-position: -153px -25px;&:hover {background-position: -218px -25px;}}.like-icon.liked {background-position: -154px -89px;}}.reply-dislike {display: flex;align-items: center;margin-right: 19px;.dislike-icon {width: 16px;height: 16px;background-position: -153px -153px;&:hover {background-position: -217px -153px;}}.dislike-icon.disliked {background-position: -154px -217px;}}.delete-btn {cursor: pointer;&:hover {color: #00aeec;}}}}} }.reply-none {height: 64px;margin-bottom: 80px;color: #99a2aa;font-size: 13px;line-height: 64px;text-align: center; }
App.js:
import './App.scss' import avatar from './images/bozai.png' import {useState} from "react"; import _ from 'lodash'; import classNames from "classnames"; /*** 评论列表的渲染和操作** 1. 根据状态渲染评论列表* 2. 删除评论*/// 评论列表数据 const list = [{// 评论idrpid: 3,// 用户信息user: {uid: '13258165',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '周杰伦',},// 评论内容content: '哎哟,不错哦',// 评论时间ctime: '10-20 08:15',like: 38,},{rpid: 2,user: {uid: '36080105',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '许嵩',},content: '我寻你千百度 日出到迟暮',ctime: '09-13 11:29',like: 88,},{rpid: 1,user: {uid: '30009257',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '黑马前端',},content: '学前端就来黑马',ctime: '10-19 09:00',like: 66,}, ]// 当前登录用户信息 const user = {// 用户iduid: '30009257',// 用户头像avatar,// 用户昵称uname: '黑马前端', }/*** 导航 Tab 的渲染和操作** 1. 渲染导航 Tab 和高亮* 2. 评论列表排序* 最热 => 喜欢数量降序* 最新 => 创建时间降序*/// 导航 Tab 数组 const tabs = [{ type: 'hot', text: '最热' },{ type: 'time', text: '最新' }, ]const App = () => {// 渲染评论列表// 1.使用 useState 维护 listconst [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));// 删除功能const handleDel = (id) => {// 对 commentList 进行过滤setCommentList(commentList.filter(item => item.rpid !== id));};// tab 切换功能// 1.点击谁就把谁的 type 记录下来// 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示const [type, setType] = useState('hot');const handleTabChange = (type) => {console.log(type);setType(type);// 基于列表的排序if (type === 'hot') {// 根据点赞数量排序// lodashsetCommentList(_.orderBy(commentList, 'like', 'desc'));} else {// 根据创建时间排序setCommentList(_.orderBy(commentList, 'ctime', 'desc'))}};// 发表评论const [content, setContent] = useState('');const handlePublish = () => {setCommentList([...commentList,{rpid: 4,user: {uid: '30009257',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '黑马前端',},content: content,ctime: '10-19 09:00',like: 80,}]);};return (<div className="app">{/* 导航 Tab */}<div className="reply-navigation"><ul className="nav-bar"><li className="nav-title"><span className="nav-title-text">评论</span>{/* 评论数量 */}<span className="total-reply">{10}</span></li><li className="nav-sort">{/* 高亮类名: active */}{tabs.map(item =><span key={item.type}onClick={() => handleTabChange(item.type)}className={classNames('nav-item', {active: type === item.type})}>{item.text}</span>)}</li></ul></div><div className="reply-wrap">{/* 发表评论 */}<div className="box-normal">{/* 当前用户头像 */}<div className="reply-box-avatar"><div className="bili-avatar"><img className="bili-avatar-img" src={avatar} alt="用户头像" /></div></div><div className="reply-box-wrap">{/* 评论框 */}<textareaclassName="reply-box-textarea"placeholder="发一条友善的评论"value={content}onChange={(e) => setContent(e.target.value)}/>{/* 发布按钮 */}<div className="reply-box-send"><div className="send-text" onClick={handlePublish}>发布</div></div></div></div>{/* 评论列表 */}<div className="reply-list">{/* 评论项 */}{commentList.map(item => (<div key={item.rpid} className="reply-item">{/* 头像 */}<div className="root-reply-avatar"><div className="bili-avatar"><imgclassName="bili-avatar-img"alt=""src={item.user.avatar}/></div></div><div className="content-wrap">{/* 用户名 */}<div className="user-info"><div className="user-name">{item.user.uname}</div></div>{/* 评论内容 */}<div className="root-reply"><span className="reply-content">{item.content}</span><div className="reply-info">{/* 评论时间 */}<span className="reply-time">{item.ctime}</span>{/* 评论数量 */}<span className="reply-time">点赞数:{item.like}</span>{/*条件:user.id === item.user.id*/}{user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}</div></div></div></div>))}</div></div></div>) }export default App
uuid 库
安装:
npm install uuid
引入:
import {v4 as uuidV4} from 'uuid' uuidV4(); // 使用
dayjs 库
安装:
npm install dayjs
引入:
import dayjs from 'dayjs' dayjs() // 使用
11.2 发表评论后清除输入框并聚焦
思路:
-
设置输入框的 useState 的 setContent 为空
-
利用 useRef 获取 dom 元素,再调用 focus 方法
App.js:
import './App.scss' import avatar from './images/bozai.png' import {useRef, useState} from "react"; import _ from 'lodash'; import classNames from "classnames"; import {v4 as uuidV4} from 'uuid' import dayjs from "dayjs";/*** 评论列表的渲染和操作** 1. 根据状态渲染评论列表* 2. 删除评论*/// 评论列表数据 const list = [{// 评论idrpid: 3,// 用户信息user: {uid: '13258165',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '周杰伦',},// 评论内容content: '哎哟,不错哦',// 评论时间ctime: '10-20 08:15',like: 38,},{rpid: 2,user: {uid: '36080105',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '许嵩',},content: '我寻你千百度 日出到迟暮',ctime: '09-13 11:29',like: 88,},{rpid: 1,user: {uid: '30009257',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '黑马前端',},content: '学前端就来黑马',ctime: '10-19 09:00',like: 66,}, ]// 当前登录用户信息 const user = {// 用户iduid: '30009257',// 用户头像avatar,// 用户昵称uname: '黑马前端', }/*** 导航 Tab 的渲染和操作** 1. 渲染导航 Tab 和高亮* 2. 评论列表排序* 最热 => 喜欢数量降序* 最新 => 创建时间降序*/// 导航 Tab 数组 const tabs = [{ type: 'hot', text: '最热' },{ type: 'time', text: '最新' }, ]const App = () => {// 渲染评论列表// 1.使用 useState 维护 listconst [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));const inputRef = useRef(null);// 删除功能const handleDel = (id) => {// 对 commentList 进行过滤setCommentList(commentList.filter(item => item.rpid !== id));};// tab 切换功能// 1.点击谁就把谁的 type 记录下来// 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示const [type, setType] = useState('hot');const handleTabChange = (type) => {console.log(type);setType(type);// 基于列表的排序if (type === 'hot') {// 根据点赞数量排序// lodashsetCommentList(_.orderBy(commentList, 'like', 'desc'));} else {// 根据创建时间排序setCommentList(_.orderBy(commentList, 'ctime', 'desc'))}};// 发表评论const [content, setContent] = useState('');const handlePublish = () => {setCommentList([...commentList,{rpid: uuidV4(),user: {uid: '30009257',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '黑马前端',},content: content,ctime: dayjs(new Date()).format('MM-DD hh:mm'),like: 80,}]);// 1.清楚输入框内容setContent('')// 2.重新聚焦inputRef.current.focus();};return (<div className="app">{/* 导航 Tab */}<div className="reply-navigation"><ul className="nav-bar"><li className="nav-title"><span className="nav-title-text">评论</span>{/* 评论数量 */}<span className="total-reply">{10}</span></li><li className="nav-sort">{/* 高亮类名: active */}{tabs.map(item =><span key={item.type}onClick={() => handleTabChange(item.type)}className={classNames('nav-item', {active: type === item.type})}>{item.text}</span>)}</li></ul></div><div className="reply-wrap">{/* 发表评论 */}<div className="box-normal">{/* 当前用户头像 */}<div className="reply-box-avatar"><div className="bili-avatar"><img className="bili-avatar-img" src={avatar} alt="用户头像" /></div></div><div className="reply-box-wrap">{/* 评论框 */}<textareaclassName="reply-box-textarea"placeholder="发一条友善的评论"value={content}onChange={(e) => setContent(e.target.value)}ref={inputRef}/>{/* 发布按钮 */}<div className="reply-box-send"><div className="send-text" onClick={handlePublish}>发布</div></div></div></div>{/* 评论列表 */}<div className="reply-list">{/* 评论项 */}{commentList.map(item => (<div key={item.rpid} className="reply-item">{/* 头像 */}<div className="root-reply-avatar"><div className="bili-avatar"><imgclassName="bili-avatar-img"alt=""src={item.user.avatar}/></div></div><div className="content-wrap">{/* 用户名 */}<div className="user-info"><div className="user-name">{item.user.uname}</div></div>{/* 评论内容 */}<div className="root-reply"><span className="reply-content">{item.content}</span><div className="reply-info">{/* 评论时间 */}<span className="reply-time">{item.ctime}</span>{/* 评论数量 */}<span className="reply-time">点赞数:{item.like}</span>{/*条件:user.id === item.user.id*/}{user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}</div></div></div></div>))}</div></div></div>) }export default App
12. 组件间通信
父传子 props
传递步骤:
1.父组件传递数据,子组件标签身上绑定属性 2.子组件接收数据,props 参数
父传子 demo:
App.js:
import React from "react";// 父传子 // 1.父组件传递数据,子组件标签身上绑定属性 // 2.子组件接收数据,props 参数 function Son (props) {// props:对象包含了父组件传递过来的所有数据console.log(props);return <div>this is son, father's param is {props.name}</div>; }function App() {const name = 'this is app name';return (<div className="App"><Son name={name} /></div>); }export default App;
小 demo:
App.js:
import React from "react";// 父传子 // 1.父组件传递数据,子组件标签身上绑定属性 // 2.子组件接收数据,props 参数 function Son (props) {// props:对象包含了父组件传递过来的所有数据console.log(props);return <div>this is son, father's param is {props.name}</div>; }function App() {const name = 'this is app name';return (<div className="App"><Sonname={name}age={18}isTrue={false}list={['vue', 'react']}cb={() => console.log(123)}child={<span>this is span</span>}/></div>); }export default App;
注意:
父组件几乎可以给子组件传任何东西,包括布尔,数值,数组,对象和函数等
但是子组件不可修改父组件传递的属性,谁传递的谁修改
父传子 特殊的 prop children
组件包裹传递
App.js:
import React from "react";// 父传子 // 1.父组件传递数据,子组件标签身上绑定属性 // 2.子组件接收数据,props 参数 function Son (props) {console.log(props)return <div>this is son, {props.children}</div> }function App() {return (<div className="App"><Son><span>this is span</span></Son></div>); }export default App;
显示:this is son,this is span
子传父
思路:子组件调用父组件中的函数并传递参数
App.js:
import React, {useState} from "react";// 核心:在子组件中调用父组件中的函数并传递实参 function Son ({onGetSonMsg}) {const sonMsg = 'this is son msg';return (<div>this is son<button onClick={() => onGetSonMsg(sonMsg)}>sendMsg</button></div>); }function App() {const [msg, setMsg] = useState('');const getMsg = (msg) => {console.log(msg);setMsg(msg);};return (<div className="App">this is App, {msg}<Son onGetSonMsg={getMsg} /></div>); }export default App;
使用状态提升实现兄弟组件通信
思路:借助“状态提升”机制,通过父组件进行兄弟组件之间的数据传递
App.js:
import React, {useState} from "react";// 1.子传父 A -> App // 2.子传父 B -> App function A ({onGetAName}) {// A 组件中的数据const name = 'this is A name';return (<div>this is A component<button onClick={() => onGetAName(name)}>send</button></div>); }function B (props) {return (<div>this is B component, {props.name}</div>); }function App() {const [name, setName] = useState('');const getAName = (name) => {console.log(name);setName(name);};return (<div className="App">this is App<A onGetAName={getAName}/><B name={name}/></div>); }export default App;
使用 context 机制跨层级组件通信
实现步骤:
-
使用createContext方法创建一个上下文对象Ctx
-
在顶层组件(App)中通过Ctx.Provider组件提供数据
-
在底层组件(B)中通过useContext钩子函数获取消费数据
App.js:
import React, {createContext, useContext} from "react";// App -> A -> B// 1.createContext 方法创建一个上下文对象 const MsgContext = createContext(); // 2.在顶层组件通过 Provider 组件提供数据 // 3.在底层组件通过 useContext 钩子函数使用数据function A () {return (<div>this is A component<B /></div>); }function B () {const msg = useContext(MsgContext);return (<div>this is B component, {msg}</div>); }function App() {const msg = 'this is app msg';return (<div className="App"><MsgContext.Provider value={msg}>this is App<A /></MsgContext.Provider></div>); }export default App;
结果:
使用场景:
13. UseEffect概念理解
useEffect 用于在 React 组件中创建不是由事件引起而是由渲染本身引起的操作,比如发送 ajax 请求,更改 DOM 等。
语法:
useEffect(() => {}, [])
参数1是一个函数,可以把它叫做副作用函数,在内部放置要执行的操作
参数2是一个数组,在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行。当是空数组时,副作用函数只会在组件渲染完毕后执行一次。
App.js:
import React, {useEffect, useState} from "react";const URL = 'http://geek.itheima.net/v1_0/channels';function App() {// 创建一个状态数据const [list, setList] = useState([]);useEffect(() => {// 额外的操作 获取频道列表async function getList() {const res = await fetch(URL);const jsonRes = await res.json();console.log(jsonRes);setList(jsonRes.data.channels);}getList();}, []);return (<div className="App"><ul>{list.map(item => <li key={item.id}>{item.name}</li>)}</ul></div>); }export default App;
useEffect 依赖参数说明
情况一:
function App() {// 1. 没有依赖项 初始 + 组件更新const [count, setCount] = useState(0);useEffect(() => {console.log('副作用函数执行了');});return (<div className="App">this is app<button onClick={() => {setCount(count + 1)}}>+{count}</button></div>); }export default App;
情况二:
function App() {// 1. 没有依赖项 初始 + 组件更新const [count, setCount] = useState(0);// useEffect(() => {// console.log('副作用函数执行了');// });// 2. 传入空数组依赖 只在初始渲染时执行useEffect(() => {console.log('副作用函数执行了');}, []);return (<div className="App">this is app<button onClick={() => {setCount(count + 1)}}>+{count}</button></div>); }export default App;
情况三:
import React, {useEffect, useState} from "react";function App() {// 1. 没有依赖项 初始 + 组件更新const [count, setCount] = useState(0);// useEffect(() => {// console.log('副作用函数执行了');// });// 2. 传入空数组依赖 只在初始渲染时执行// useEffect(() => {// console.log('副作用函数执行了');// }, []);// 3. 传入特定依赖项 初始 + 依赖项变化时执行useEffect(() => {console.log('副作用函数执行了');}, [count]);return (<div className="App">this is app<button onClick={() => {setCount(count + 1)}}>+{count}</button></div>); }export default App;
useEffect 清除副作用
在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开 启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用
import React, {useEffect, useState} from "react";function Son() {useEffect(() => {const timer = setInterval(() => {console.log('定时器执行中……')}, 1000);return () => {// 清除副作用(组件卸载时)clearInterval(timer);};}, []);return <div>this is son</div> }function App() {const [show, setShow] = useState(true);return (<div className="App">{show && <Son />}<button onClick={() => setShow(false)}>卸载 Son 组件</button></div>); }export default App;
说明:清除副作用的函数最常见的执行时机是组件卸载时自动执行
需求:Son 组件渲染时开启一个定时器,卸载时清除它
14. 自定义 Hook 函数
概念:自定义 Hook 是以 use 开头的函数,通过自定义 Hook 函数可以用来实现逻辑的封装和复用
封装思路:
-
声明一个以 use 开头的函数
-
在函数体内封装可复用的逻辑(只要是可复用的逻辑)
-
返回状态和回调(以对象或者数据返回)
-
在哪个组件中要用到,就执行这个函数,解构出状态和回调即可使用
import React, {useEffect, useState} from "react";// 问题:布尔切换逻辑与当前组件耦合在一起,不方便使用 // 解决思路:自定义 hookfunction useToggle() {// 可复用的代码逻辑const [value, setValue] = useState(true);const toggle = () => setValue(!value);// 哪些状态和回调函数需要在其他组件使用 returnreturn {value,toggle} }function App() {const { value, toggle } = useToggle();return (<div className="App">{value && <div>this is div</div>}<button onClick={toggle}>toggle</button></div>); }export default App;
15. ReactHooks 使用规则
-
只能在组件中或者其他自定义Hook函数中调用
-
只能在组件的顶层调用,不能嵌套在 if、for、其他函数中
import React, {useEffect, useState} from "react";// 问题:布尔切换逻辑与当前组件耦合在一起,不方便使用 // 解决思路:自定义 hookfunction useToggle() {// 可复用的代码逻辑const [value, setValue] = useState(true);const toggle = () => setValue(!value);// 哪些状态和回调函数需要在其他组件使用 returnreturn {value,toggle} }function App() {const { value, toggle } = useToggle();return (<div className="App">{value && <div>this is div</div>}<button onClick={toggle}>toggle</button></div>); }export default App;
16. B 站评论优化
模拟请求评论接口,抽象出 Hook
实现思路:
1.使用 json-server 工具模拟接口服务,通过 axios 发送接口请求
2.使用 useEffect 调用接口获取数据
安装 json-server 库:
npm i json-server -D
安装 axios 库:
npm install axios
db.json:
{"list": [{"rpid": 3,"user": {"uid": "13258165","avatar": "http://toutiao.itheima.net/resources/images/98.jpg","uname": "周杰伦"},"content": "哎哟,不错哦","ctime": "10-18 08: 15","like": 126},{"rpid": 2,"user": {"uid": "36080105","avatar": "http://toutiao.itheima.net/resources/images/98.jpg","uname": "许嵩"},"content": "我寻你千百度 日出到迟暮","ctime": "11-13 11: 29","like": 88},{"rpid": 1,"user": {"uid": "30009257","avatar": "http://toutiao.itheima.net/resources/images/98.jpg","uname": "黑马前端"},"content": "学前端就来黑马","ctime": "10-19 09: 00","like": 66}] }
改写 package.json:
"scripts": {"start": "react-scripts start","build": "react-scripts build","serve": "json-server db.json --port 3004"},
App.js:
import './App.scss' import avatar from './images/bozai.png' import {useEffect, useRef, useState} from "react"; import _ from 'lodash'; import classNames from "classnames"; import {v4 as uuidV4} from 'uuid' import dayjs from "dayjs"; import axios from "axios";/*** 评论列表的渲染和操作** 1. 根据状态渲染评论列表* 2. 删除评论*/// 评论列表数据 const list = [{// 评论idrpid: 3,// 用户信息user: {uid: '13258165',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '周杰伦',},// 评论内容content: '哎哟,不错哦',// 评论时间ctime: '10-20 08:15',like: 38,},{rpid: 2,user: {uid: '36080105',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '许嵩',},content: '我寻你千百度 日出到迟暮',ctime: '09-13 11:29',like: 88,},{rpid: 1,user: {uid: '30009257',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '黑马前端',},content: '学前端就来黑马',ctime: '10-19 09:00',like: 66,}, ]// 当前登录用户信息 const user = {// 用户iduid: '30009257',// 用户头像avatar,// 用户昵称uname: '黑马前端', }/*** 导航 Tab 的渲染和操作** 1. 渲染导航 Tab 和高亮* 2. 评论列表排序* 最热 => 喜欢数量降序* 最新 => 创建时间降序*/// 导航 Tab 数组 const tabs = [{ type: 'hot', text: '最热' },{ type: 'time', text: '最新' }, ]const App = () => {// 渲染评论列表// 1.使用 useState 维护 list// const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));// 获取接口数据渲染const [commentList, setCommentList] = useState([]);useEffect( () => {// 请求数据async function getList() {// axios 请求数据const res = await axios.get('http://localhost:3004/list');setCommentList(res.data);}getList();}, []);const inputRef = useRef(null);// 删除功能const handleDel = (id) => {// 对 commentList 进行过滤setCommentList(commentList.filter(item => item.rpid !== id));};// tab 切换功能// 1.点击谁就把谁的 type 记录下来// 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示const [type, setType] = useState('hot');const handleTabChange = (type) => {console.log(type);setType(type);// 基于列表的排序if (type === 'hot') {// 根据点赞数量排序// lodashsetCommentList(_.orderBy(commentList, 'like', 'desc'));} else {// 根据创建时间排序setCommentList(_.orderBy(commentList, 'ctime', 'desc'))}};// 发表评论const [content, setContent] = useState('');const handlePublish = () => {setCommentList([...commentList,{rpid: uuidV4(),user: {uid: '30009257',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '黑马前端',},content: content,ctime: dayjs(new Date()).format('MM-DD hh:mm'),like: 80,}]);// 1.清楚输入框内容setContent('')// 2.重新聚焦inputRef.current.focus();};return (<div className="app">{/* 导航 Tab */}<div className="reply-navigation"><ul className="nav-bar"><li className="nav-title"><span className="nav-title-text">评论</span>{/* 评论数量 */}<span className="total-reply">{10}</span></li><li className="nav-sort">{/* 高亮类名: active */}{tabs.map(item =><span key={item.type}onClick={() => handleTabChange(item.type)}className={classNames('nav-item', {active: type === item.type})}>{item.text}</span>)}</li></ul></div><div className="reply-wrap">{/* 发表评论 */}<div className="box-normal">{/* 当前用户头像 */}<div className="reply-box-avatar"><div className="bili-avatar"><img className="bili-avatar-img" src={avatar} alt="用户头像" /></div></div><div className="reply-box-wrap">{/* 评论框 */}<textareaclassName="reply-box-textarea"placeholder="发一条友善的评论"value={content}onChange={(e) => setContent(e.target.value)}ref={inputRef}/>{/* 发布按钮 */}<div className="reply-box-send"><div className="send-text" onClick={handlePublish}>发布</div></div></div></div>{/* 评论列表 */}<div className="reply-list">{/* 评论项 */}{commentList.map(item => (<div key={item.rpid} className="reply-item">{/* 头像 */}<div className="root-reply-avatar"><div className="bili-avatar"><imgclassName="bili-avatar-img"alt=""src={item.user.avatar}/></div></div><div className="content-wrap">{/* 用户名 */}<div className="user-info"><div className="user-name">{item.user.uname}</div></div>{/* 评论内容 */}<div className="root-reply"><span className="reply-content">{item.content}</span><div className="reply-info">{/* 评论时间 */}<span className="reply-time">{item.ctime}</span>{/* 评论数量 */}<span className="reply-time">点赞数:{item.like}</span>{/*条件:user.id === item.user.id*/}{user.uid === item.user.uid && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}</div></div></div></div>))}</div></div></div>) }export default App;
封装评论项 Item 组件
import './App.scss' import avatar from './images/bozai.png' import {useEffect, useRef, useState} from "react"; import _ from 'lodash'; import classNames from "classnames"; import {v4 as uuidV4} from 'uuid' import dayjs from "dayjs"; import axios from "axios";/*** 评论列表的渲染和操作** 1. 根据状态渲染评论列表* 2. 删除评论*/// 评论列表数据 const list = [{// 评论idrpid: 3,// 用户信息user: {uid: '13258165',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '周杰伦',},// 评论内容content: '哎哟,不错哦',// 评论时间ctime: '10-20 08:15',like: 38,},{rpid: 2,user: {uid: '36080105',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '许嵩',},content: '我寻你千百度 日出到迟暮',ctime: '09-13 11:29',like: 88,},{rpid: 1,user: {uid: '30009257',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '黑马前端',},content: '学前端就来黑马',ctime: '10-19 09:00',like: 66,}, ]// 当前登录用户信息 const user = {// 用户iduid: '30009257',// 用户头像avatar,// 用户昵称uname: '黑马前端', }/*** 导航 Tab 的渲染和操作** 1. 渲染导航 Tab 和高亮* 2. 评论列表排序* 最热 => 喜欢数量降序* 最新 => 创建时间降序*/// 导航 Tab 数组 const tabs = [{ type: 'hot', text: '最热' },{ type: 'time', text: '最新' }, ]// 封装请求数据的 Hook function useGetList() {// 获取接口数据渲染const [commentList, setCommentList] = useState([]);useEffect( () => {// 请求数据async function getList() {// axios 请求数据const res = await axios.get('http://localhost:3004/list');setCommentList(res.data);}getList();}, []);return {commentList,setCommentList}; }// 评论项组件 function Item({ item, onDel }) {return (<div className="reply-item">{/* 头像 */}<div className="root-reply-avatar"><div className="bili-avatar"><imgclassName="bili-avatar-img"alt=""src={item.user.avatar}/></div></div><div className="content-wrap">{/* 用户名 */}<div className="user-info"><div className="user-name">{item.user.uname}</div></div>{/* 评论内容 */}<div className="root-reply"><span className="reply-content">{item.content}</span><div className="reply-info">{/* 评论时间 */}<span className="reply-time">{item.ctime}</span>{/* 评论数量 */}<span className="reply-time">点赞数:{item.like}</span>{/*条件:user.id === item.user.id*/}{user.uid === item.user.uid && <span className="delete-btn" onClick={() => onDel(item.rpid)}>删除</span>}</div></div></div></div>) }const App = () => {// 渲染评论列表// 1.使用 useState 维护 list// const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));const { commentList, setCommentList } = useGetList();const inputRef = useRef(null);// 删除功能const handleDel = (id) => {// 对 commentList 进行过滤setCommentList(commentList.filter(item => item.rpid !== id));};// tab 切换功能// 1.点击谁就把谁的 type 记录下来// 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示const [type, setType] = useState('hot');const handleTabChange = (type) => {console.log(type);setType(type);// 基于列表的排序if (type === 'hot') {// 根据点赞数量排序// lodashsetCommentList(_.orderBy(commentList, 'like', 'desc'));} else {// 根据创建时间排序setCommentList(_.orderBy(commentList, 'ctime', 'desc'))}};// 发表评论const [content, setContent] = useState('');const handlePublish = () => {setCommentList([...commentList,{rpid: uuidV4(),user: {uid: '30009257',avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',uname: '黑马前端',},content: content,ctime: dayjs(new Date()).format('MM-DD hh:mm'),like: 80,}]);// 1.清楚输入框内容setContent('')// 2.重新聚焦inputRef.current.focus();};return (<div className="app">{/* 导航 Tab */}<div className="reply-navigation"><ul className="nav-bar"><li className="nav-title"><span className="nav-title-text">评论</span>{/* 评论数量 */}<span className="total-reply">{10}</span></li><li className="nav-sort">{/* 高亮类名: active */}{tabs.map(item =><span key={item.type}onClick={() => handleTabChange(item.type)}className={classNames('nav-item', {active: type === item.type})}>{item.text}</span>)}</li></ul></div><div className="reply-wrap">{/* 发表评论 */}<div className="box-normal">{/* 当前用户头像 */}<div className="reply-box-avatar"><div className="bili-avatar"><img className="bili-avatar-img" src={avatar} alt="用户头像" /></div></div><div className="reply-box-wrap">{/* 评论框 */}<textareaclassName="reply-box-textarea"placeholder="发一条友善的评论"value={content}onChange={(e) => setContent(e.target.value)}ref={inputRef}/>{/* 发布按钮 */}<div className="reply-box-send"><div className="send-text" onClick={handlePublish}>发布</div></div></div></div>{/* 评论列表 */}<div className="reply-list">{/* 评论项 */}{commentList.map(item => <Item item={item} onDel={handleDel} />)}</div></div></div>) }export default App;
17. Redux 集中状态管理工具
Redux 是 React 最常用的集中状态管理工具,类似于 Vue 中的 Pinia(Vuex),可以独立于框架运行
作用:通过集中管理的方式管理应用的状态
使用步骤:
-
定义一个 Redux 函数
-
使用 createStore 方法传入 reducer 函数,生成一个 store 实例对象
-
使用 store 实例的 subscribe 方法订阅数据的变化(数据一旦变化,可以得到通知)
-
使用 store 实例的 dispatch方法提交 action 对象触发数据变化(告诉 reducer 你想怎么改数据)
-
使用 store 实例的 getState 方法获取最新的状态数据更新到视图中
Redux 与 React环境准备
配套工具
-
Redux Toolkit — 官方推荐编写 Redux 逻辑的方式,是一套工具的集合,简化书写方式
-
简化 store 配置
-
内置 immer 支持可变式状态修改
-
内置 thunk,更好的异步创建
-
-
react-redux — 用来链接 Redux 和 React 组件的中间件
-
获取状态,更新状态
-
配置基础环境
-
使用CRA快速创建React项目
npx create-react-app react-redux
-
安装配套工具
npm i @reduxjs/toolkit react-redux
-
启动项目
npm run start
使用 React Toolkit 创建 counterStore
counterStore.js:
import {createSlice} from "@reduxjs/toolkit";const counterStore = createSlice({// store 名称name: 'counter',// 初始化状态initialState: {count: 0},// 修改状态的方法,同步方法,支持直接修改reducers: {increment(state) {state.count++;},decrement(state) {state.count--;}} });// 结构出来 actionCreator 函数 const {increment, decrement} = counterStore.actions;const reducer = counterStore.reducer;// 按需导出 actionCreator export {increment, decrement};// 默认导出 reducer export default reducer;
src\store\index.js:
import {configureStore} from "@reduxjs/toolkit"; import counterStore from "./modules/counterStore";const store = configureStore({reducer: {counter: counterStore} });export default store;
src\index.js:
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import {Provider} from "react-redux"; import store from "./store";const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<React.StrictMode><Provider store={store}><App /></Provider></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();
React 组件使用 store 中的数据
const { count } = useSelector(state => state.counter);
React 组件修改 store 中的数据
import './App.css'; import {useDispatch, useSelector} from "react-redux"; // 导入 actionCreator import {decrement, increment} from "./store/modules/counterStore";function App() {const { count } = useSelector(state => state.counter);const dispatch = useDispatch();return (<div className="App"><button onClick={() => dispatch(decrement())}>-</button><span>{count}</span><button onClick={() => dispatch(increment())}>+</button></div>); }export default App;
总结:
提交 action 修改 state 值
counterStore.js:
import {createSlice} from "@reduxjs/toolkit";const counterStore = createSlice({// store 名称name: 'counter',// 初始化状态initialState: {count: 0},// 修改状态的方法,同步方法,支持直接修改reducers: {increment(state) {state.count++;},decrement(state) {state.count--;},addToNum(state, action) {state.count += action.payload;}} });// 结构出来 actionCreator 函数 const {increment, decrement, addToNum} = counterStore.actions;const reducer = counterStore.reducer;// 按需导出 actionCreator export { increment, decrement, addToNum };// 默认导出 reducer export default reducer;
App.js:
import './App.css'; import {useDispatch, useSelector} from "react-redux"; // 导入 actionCreator import {addToNum, decrement, increment} from "./store/modules/counterStore";function App() {const { count } = useSelector(state => state.counter);const dispatch = useDispatch();return (<div className="App"><button onClick={() => dispatch(decrement())}>-</button><span>{count}</span><button onClick={() => dispatch(increment())}>+</button><button onClick={() => dispatch(addToNum(10))}>add to 10</button><button onClick={() => dispatch(addToNum(20))}>add to 20</button></div>); }export default App;
Redux 异步状态操作
异步操作样板:
-
创建stor的写法保持不变,配置好同步修改状态的方法
-
单独封装一个函数,在函数内部return一个新函数,在新函数中
-
封装异步请求获取数据
-
调用同步action Creater传入异步数据生成一个action对象,并使用dispatch提交
-
-
组件中dispatch的写法保特不变
channelStore.js:
import {createSlice} from "@reduxjs/toolkit"; import axios from "axios";const channelStore = createSlice({// store 名称name: 'channel',initialState: {channelList: []},reducers: {setChannels(state, action) {state.channelList = action.payload;}} });// 异步请求部分 const { setChannels } = channelStore.actions;const fetchChannelList = () => {return async (dispatch) => {const res = await axios.get('http://geek.itheima.net/v1_0/channels');dispatch(setChannels(res.data.data.channels));}; };export { fetchChannelList };const reducer = channelStore.reducer;export default reducer;
index.js:
import {configureStore} from "@reduxjs/toolkit"; import counterStore from "./modules/counterStore"; import channelStore from "./modules/channelStore";const store = configureStore({reducer: {counter: counterStore,channel: channelStore,} });export default store;
App.js:
import {useDispatch, useSelector} from "react-redux"; // 导入 actionCreator import {addToNum, decrement, increment} from "./store/modules/counterStore"; import {useEffect} from "react"; import {fetchChannelList} from "./store/modules/channelStore";function App() {const { count } = useSelector(state => state.counter);const { channelList } = useSelector(state => state.channel);const dispatch = useDispatch();useEffect(() => {dispatch(fetchChannelList());}, [dispatch]);return (<div className="App"><button onClick={() => dispatch(decrement())}>-</button><span>{count}</span><button onClick={() => dispatch(increment())}>+</button><button onClick={() => dispatch(addToNum(10))}>add to 10</button><button onClick={() => dispatch(addToNum(20))}>add to 20</button><ul>{channelList.map(item => <li key={item.id}>{item.name}</li>)}</ul></div>); }export default App;
18. React Router 路由
上手 demo
需求:创建一个可以切换登录和文章页的路由
实现:
npm i react-router-dom # 安装路由依赖
index.js:
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import reportWebVitals from './reportWebVitals'; import {createBrowserRouter, RouterProvider} from "react-router-dom";// 1. 创建 Router 实例对象并且配置路由对应关系 const router = createBrowserRouter([{path: '/login',element: <div>我是登录页</div>},{path: '/article',element: <div>我是文章页</div>}, ]);const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<React.StrictMode><RouterProvider router={router}></RouterProvider></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();
实际开发中的 router 配置
1.创建 page 文件夹,分别创建 Login 和 Article 目录,再分别创建 index.js 文件
src/page/article/index.js:
const Article = () => {return <div>我是文章</div> };export default Article;
src/page/login/index.js:
const Login = () => {return <div>我是登录</div> };export default Login;
2.创建 router 文件夹,在其中创建 index.js 文件
router/index.js:
import {createBrowserRouter} from "react-router-dom"; import Login from "../page/login"; import Article from "../page/article";const router = createBrowserRouter([{path: '/login',element: <Login />},{path: '/article',element: <Article />} ]);export default router;
3.在 src/index.js 文件中引入 router 实例
src/index.js:
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import reportWebVitals from './reportWebVitals'; import {createBrowserRouter, RouterProvider} from "react-router-dom";import router from './router';const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<React.StrictMode><RouterProvider router={router}></RouterProvider></React.StrictMode> );reportWebVitals();
路由导航
声明式导航
指的是在模板中通过 <Link />
组件描述要调到哪里去,常用于 Tab 栏。类似 Vue 的 router-link
示例:
<Link to="/article">文章</Link>
编程式导航
编程式导航是指通过useNavigate
钩子得到导航方法,然后通过调用方法以命令式的形式进行路由跳转,比如想在 登录请求完毕之后跳转就可以选择这种方式,更加灵活
示例:
import {Link, useNavigate} from "react-router-dom";const Login = () => {const navigate = useNavigate();return (<div><div>我是登录</div><Link to='/article'>跳转文章页</Link><button onClick={() => navigate('/article')}>跳转文章页</button></div>) };export default Login;
路由传参
searchParams 传参
// 传参 const navigate = useNavigate(); <button onClick={() => navigate('/article?id=1001&name=jack')}>searchParams 传参</button>// 拿参 const [params] = useSearchParams(); const id = params.get('id'); <div>我是文章页{id}</div>
params 传参
// 传参 const navigate = useNavigate(); <button onClick={() => navigate('/article/1001')}>params 传参</button>// 取参 const params = useParams(); const id = params.id; <div>我是文章页{id}</div>
注意 parmas 传参必须要在 router/index.js 文件的 path 加上 :id
import {createBrowserRouter} from "react-router-dom"; import Login from "../page/login"; import Article from "../page/article";const router = createBrowserRouter([{path: '/login',element: <Login />},{path: '/article/:id',element: <Article />} ]);export default router;
传参多值:
// 传参 const navigate = useNavigate(); <button onClick={() => navigate('/article?id=1001&name=jack')}>searchParams 传参</button> <button onClick={() => navigate('/article/1001/hejiajun')}>params 传参</button>// 取参 const params = useParams(); const id = params.id; const name = params.name; <div>我是文章页{id}</div> <div>我是文章页{name}</div>
router/index.js:
import {createBrowserRouter} from "react-router-dom"; import Login from "../page/login"; import Article from "../page/article";const router = createBrowserRouter([{path: '/login',element: <Login />},{path: '/article/:id/:name',element: <Article />} ]);export default router;
嵌套路由配置
大致步骤:
-
使用 children 属性配置路由嵌套关系
-
使用 <Outlet /> 组件配置二级路由渲染位置
router/index.js:
import {createBrowserRouter} from "react-router-dom"; import Login from "../page/login"; import Article from "../page/article"; import Layout from "../page/layout"; import Board from "../page/board"; import About from "../page/about";const router = createBrowserRouter([{path: '/',element: <Layout />,children: [{path: 'board',element: <Board />},{path: 'about',element: <About />}]},{path: '/login',element: <Login />},{path: '/article/:id/:name',element: <Article />} ]);export default router;
src/page/layout/index.js:
import {Link, Outlet} from "react-router-dom";const Layout = () => {return (<div>我是一级路由 layout 组件<Link to='/board'>面板</Link><Link to='/about'>关于</Link>{/*配置二级路由的出口*/}<Outlet /></div>) };export default Layout;
默认二级路由设置
当访问的是一级路由时,默认的二级路由组件可以得到渲染,只需要在二级路由的位置去掉path,设置 index 属性为 true
src/router/index.js
import {createBrowserRouter} from "react-router-dom"; import Login from "../page/login"; import Article from "../page/article"; import Layout from "../page/layout"; import Board from "../page/board"; import About from "../page/about";const router = createBrowserRouter([{path: '/',element: <Layout />,children: [// 设置默认二级路由,一级路由被访问的时候,它也能得到渲染{index: true,element: <Board />},{path: 'about',element: <About />}]},{path: '/login',element: <Login />},{path: '/article/:id/:name',element: <Article />} ]);export default router;
404 路由配置
场景:当浏览器输入url的路径在整个路由配置中都找不到对应的pth,为了用户体验,可以使用404兜底组件进行 渲染
两种路由模式
创建 hash 路由
import {createHashRouter} from "react-router-dom"; import Login from "../page/login"; import Article from "../page/article"; import Layout from "../page/layout"; import Board from "../page/board"; import About from "../page/about"; import NotFound from "../page/notfound";const router = createHashRouter([{path: '/',element: <Layout />,children: [// 设置默认二级路由,一级路由被访问的时候,它也能得到渲染{index: true,element: <Board />},{path: 'about',element: <About />}]},{path: '/login',element: <Login />},{path: '/article/:id/:name',element: <Article />},{path: '*',element: <NotFound />} ]);export default router;
19. 记账本
这边没有认真记,想要学习的小伙伴可以直接看原视频好了,配合视频和笔记学习效果会更好。
环境搭建
使用CRA创建项目,并安装必要依赖,包括下列基础包
npx create-react-app react-bill
-
Redux:状态管理-@reduxjs./toolkit、react-redux
-
路由-reac-router-dom
-
时间处理-dayjs
-
class:类名处理-classnames
-
移动端组件库-antd-mobile
-
请求插件-axios
npm i @reduxjs/toolkit react-redux react-router-dom dayjs classnames antd-mobile axios
配置别名路径
1.路径解析配置,把 @/ 解析为 src/ (1. npm i -D @craco/craco
2.项目根目录下创建配置文件craco.config.js
)
2.路径联想配置(VsCode),VsCode在输入@/时,自动联想出来对应的src/下的子级目录
数据 Mock
npm i -D json-server
20. UseReducer
作用:和 useState 作用类似,用来管理相对复杂的状态数据
基础用法:
-
定义一个reducer函数(根据不同的action返回不同的新状态)
-
在组件中调用useReducer,并传入reducer函数和状态的初始值
-
事件发生时,通过 dispatch 函数分派一个 action 对象(通知 reducer 要返回哪个新状态并渲染 UI)
App.js:
import React, {useReducer} from "react";// useReducer // 1. 定义 reducer 函数,根据不同的action返回不同的新状态 // 2. 在组件中调用 useReducer,并传入 reducer 函数和状态的初始值 // 3. 调用 dispatch 通知 reducer 产生一个新状态,利用新状态更新 UIfunction reducer(state, action) {switch (action.type) {case 'INC':return state + 1;case 'DEC':return state - 1;case 'SET':return action.payload;default:return state;}}function App() {const [state, dispatch] = useReducer(reducer, 0);return (<div className="App">this is app<button onClick={() => dispatch({ type: 'DEC' })}>-</button>{state}<button onClick={() => dispatch({ type: 'INC' })}>+</button><button onClick={() => dispatch({ type: 'SET', payload: 100 })}>update</button></div>); }export default App;
21. useMemo
需求:
作用:在组件每次渲染的时候缓存计算结果
useMemo(() => {// 根据 count1 返回计算的结果 }, [count1])
说明:使用useMemo做缓存之后可以保证只有count1依赖项发生变化时才会重新计算。接受两个参数,第一个是函数,第二个是依赖项。
App.js:
import React, {useMemo, useState} from "react";function fib(n) {console.log('计算函数执行了');if (n < 3) {return 1;}return fib(n - 2) + fib(n - 1); }function App() {const [count1, setCount1] = useState(0);const result = useMemo(() => {return fib(count1);}, [count1]);// const result = fib(count1);const [count2, setCount2] = useState(0);console.log('组件重新渲染了');return (<div className="App">this is app<button onClick={() => setCount1(count1 + 1)}>change count1: {count1}</button><button onClick={() => setCount2(count2 + 1)}>change count2: {count2}</button>result is {result}</div>); }export default App;
使用场景:消耗非常大的计算