项目实战
React Hooks
缓存,性能优化,提升时间效率,但是不要为了技术而优化,应该是为了业务而进行优化
内置Hooks保证基础功能,灵活配合实现业务功能,抽离公共部分,自定义Hooks或者第三方,即复用代码
React组件公共逻辑的抽离和复用
- 之前是class组件,现在是函数组件
- class组件:Mixing HOC render-props 来复用
- 函数组件:使用Hooks-当前最完美的解决方法,vue3也参考这种实现方式
useState
用普通的js变量无法触发组件的更新,如下代码,方法触发了,但是数值没更新
function App() {
let count = 0;
function add() {
count++;
}
// 列表页
return (
<>
{/* <List1></List1> */}
<div>
<button onClick={add}>点我增加 {count}</button>
</div>
</>
);
}
下面的写法能触发更新。导入useState函数,设置初始数字为0,返回的是一个数组。数组的第一个值就是当前count的值,第二个是setCount函数,就是设置新值的函数,注意必须是传入新的值,不能通过count++传入
import React, { useState } from "react";
import "./List1.css";
// import List1 from "./List1";
function App() {
// let count = 0;
const [count, setCount] = useState(0);
function add() {
// count++;
setCount(count + 1);
}
// 列表页
return (
<>
{/* <List1></List1> */}
<div>
<button onClick={add}>点我增加 {count}</button>
</div>
</>
);
}
export default App;
state
一个组件的独特记忆
- props是父组件传递过来的信息
- state是组件内部的状态信息,不对外
- state变化触发组件更新,重新渲染rerender
- 特点:
- 异步更新,比如将添加数字抽出为组件,并在每一次点击后打印出来,会发现打印出来的数值是点击前的内容,如图所示。
-
// stateDemo组件 import React, { FC, useState } from "react"; const Demo: FC = () => { const [count, setCount] = useState(0); function add() { // count++; // setCount(count + 1); // 上下两个写法一致,都是异步更新 setCount((count) => count + 1); console.log(count); } return ( <> {/* <List1></List1> */} <div> <button onClick={add}>点我增加 {count}</button> </div> </> ); }; export default Demo;
-
- 如果一个变量不用jsx显示,就不需要用state来管理。比如设置一个变量name,页面上并不需要展示这个变量,但是每次方法的时候会调用setState来设置name,而setState只要改变就会触发页面更新,这就导致不需要的变量触发了页面的更新。这种情况下用useRef
-
- 可能会被合并,state异步更新,相同的内容执行的时候每次传入的都是最新的值,无论多少次被更新都是执行的相同步骤,导致结果应该为5的倍数却还是123这样子增加,用下面函数更新的方式可以解决合并问题
-
// stateDemo组件 import React, { FC, useState } from "react"; const Demo: FC = () => { const [count, setCount] = useState(0); function add() { // count++; // setCount(count + 1); 这个多次执行会导致合并 setCount((count) => count + 1); setCount((count) => count + 1); setCount((count) => count + 1); setCount((count) => count + 1); console.log(count); } return ( <> {/* <List1></List1> */} <div> <button onClick={add}>点我增加 {count}</button> </div> </> ); }; export default Demo;
-
-
不可变数据(重要)
-
不修改state的值,而是传入一个新的值。下列代码中只修改了age,其他属性使用解构语法获得,即解构这个数据对象,未修改的数据保持不变
-
import React, { FC, useState } from "react"; const Demo: FC = () => { const [userInfo, setUserInfo] = useState({ name: "双", age: 20 }); // function changeAge() { // setUserInfo({ // // 解构语法 // ...userInfo, // age: 21, // }); // } function addItem() { // 不能使用list.push,因为不可变数据,要传入新值而不是改变state // concat返回的是一个新数组,push返回的不是 setList(List.concat("z")); setList([...List, "z"]); } const [List, setList] = useState(["x", "y"]); return ( <div> <h2>state不可变数据</h2> {/* <div>{JSON.stringify(userInfo)}</div> <button onClick={changeAge}></button> */} <div>{JSON.stringify(List)}</div> <button onClick={addItem}>add item</button> </div> ); }; export default Demo;
-
-
实现增删改,因为抽离出了子组件,增删改需要通过父组件触发相应的操作,涉及到一个状态提升的概念,即数据源在父组件中,子组件中只需要执行传递过来的命令,显示数据即可。
-
以下是子组件中的代码,基本思想是面向对象,抽离出一个列表类,因为子组件被抽出所以对子组件进行操作的时候需要通知父组件,自己删除自己不合适
-
// 子组件 uestionCard import React, { FC } from "react"; import "./QuestionCard.css"; type PropsType = { id: string; title: string; isPublished: boolean; // 问号表示这个属性可写可不写,跟flutter语法相似 deletQuestion?: (id: string) => void; pubQuestion?: (id: string) => void; }; const QuestionCard: FC<PropsType> = (props) => { const { id, title, isPublished, deletQuestion, pubQuestion } = props; function pub(id: string) { pubQuestion && pubQuestion(id); } function del(id: string) { // 与运算符,前一个为true才去执行后一个判断条件 deletQuestion && deletQuestion(id); } return ( <div key={id} className="list-item"> <strong>{title}</strong> <br /> {/* {条件判断} */} {isPublished ? ( <span style={{ color: "green" }}>已发布</span> ) : ( <span>未发布</span> )} <button onClick={() => { pub(id); }} > 发布问卷 </button> <button onClick={() => { del(id); }} > 删除问卷 </button> </div> ); }; export default QuestionCard;
- 以下是父组件的代码,在父组件中定义对列表元素的增删改后将相应的方法传递给子元素,并在子组件中进行调用
-
import React, { FC, useState } from "react"; import QuestionCard from "./ListCard/QuestionCard"; const List2: FC = () => { const [questionList, setQuestionList] = useState([ { id: "q1", title: "问卷1", isPublished: true }, { id: "q2", title: "问卷2", isPublished: false }, { id: "q3", title: "问卷3", isPublished: false }, { id: "q4", title: "问卷4", isPublished: false }, ]); // 异步更新所以设置为5 const [count, setCount] = useState(5); function deletQuestion(id: string) { // 不可变数据 setQuestionList( // 返回true是保存,false就是删除,filter返回的是一个过滤后的新数组 questionList.filter((que) => { if (que.id == id) return false; else return true; }), ); } function pubQuestion(id: string) { setQuestionList( // 返回的同样是新的数组 questionList.map((que) => { if (que.id !== id) return que; return { ...que, isPublished: true, }; }), ); } function add() { setCount((count) => count + 1); setQuestionList( questionList.concat({ id: "q" + count, title: "问卷" + count, isPublished: false, }), ); } return ( <div> <h1>问卷列表页2</h1> <div> {questionList.map((question) => { const { id, title, isPublished } = question; return ( <QuestionCard key={id} id={id} title={title} isPublished={isPublished} deletQuestion={deletQuestion} pubQuestion={pubQuestion} /> ); })} </div> <div> <button onClick={add}>新增问卷</button> </div> </div> ); }; export default List2;
-
- 异步更新,比如将添加数字抽出为组件,并在每一次点击后打印出来,会发现打印出来的数值是点击前的内容,如图所示。
immer
出现背景,state是不可变数据,并且state操作成本比较高,有很大的不稳定性
- 执行相应的下载指令
npm install immer
- vsc上安装相应的识别插件 Code Spell Checker (可选)
- 以下代码为immer使用,和state做比较
-
// 更换年龄,对象单属性 import React, { FC, useState } from "react"; import { produce } from "immer"; const Demo: FC = () => { const [userInfo, setUserInfo] = useState({ name: "双", age: 20 }); function changeAge() { // setUserInfo({ // // 解构语法 // ...userInfo, // age: 21, // }); setUserInfo( produce((item) => { item.age = 21; }), ); } return ( <div> <h2>immer可变数据</h2> <div>{JSON.stringify(userInfo)}</div> <button onClick={changeAge}>点击更换年龄</button> </div> ); }; export default Demo; // 更换列表对象的属性 import React, { FC, useState } from "react"; import { produce } from "immer"; const Demo: FC = () => { function addItem() { // 不能使用list.push,因为不可变数据,要传入新值而不是改变state // concat返回的是一个新数组,push返回的不是 // setList(List.concat("z")); // setList([...List, "z"]); setList( produce((item) => { item.push("z"); }), ); } const [List, setList] = useState(["x", "y"]); return ( <div> <div>{JSON.stringify(List)}</div> <button onClick={addItem}>add item</button> </div> ); }; export default Demo;
-
使用immer重构问卷列表
-
import React, { FC, useState } from "react"; import { produce } from "immer"; import QuestionCard from "./ListCard/QuestionCard"; const List2: FC = () => { const [questionList, setQuestionList] = useState([ { id: "q1", title: "问卷1", isPublished: true }, { id: "q2", title: "问卷2", isPublished: false }, { id: "q3", title: "问卷3", isPublished: false }, { id: "q4", title: "问卷4", isPublished: false }, ]); // 异步更新所以设置为5 const [count, setCount] = useState(5); function deletQuestion(id: string) { // 不可变数据 // setQuestionList( // // 返回true是保存,false就是删除,filter返回的是一个过滤后的新数组 // questionList.filter((que) => { // if (que.id == id) return false; // else return true; // }), // ); // 更换为immer的方法 setQuestionList( produce((item) => { // 找到对应的id在列表中的位置 const index = item.findIndex((q) => q.id === id); // 在原数组上进行修改使用splice即可 item.splice(index, 1); }), ); } function pubQuestion(id: string) { // setQuestionList( // // 返回的同样是新的数组 // questionList.map((que) => { // if (que.id !== id) return que; // return { // ...que, // isPublished: true, // }; // }), // ); setQuestionList( produce((item) => { const q = item.find((i) => i.id === id); if (q) q.isPublished = !q.isPublished; }), ); } function add() { // setCount((count) => count + 1); // setQuestionList( // questionList.concat({ // id: "q" + count, // title: "问卷" + count, // isPublished: false, // }), // ); // 替换为immer setCount( produce((item) => { // 这里要返回return不然只增加到5就不递增了,暂不清楚原因 return (item += 1); }), ); setQuestionList( produce((item) => { item.push({ id: "q" + count, title: "问卷" + count, isPublished: false, }); }), ); } return ( <div> <h1>问卷列表页2</h1> <div> {questionList.map((question) => { const { id, title, isPublished } = question; return ( <QuestionCard key={id} id={id} title={title} isPublished={isPublished} deletQuestion={deletQuestion} pubQuestion={pubQuestion} /> ); })} </div> <div> <button onClick={add}>新增问卷</button> </div> </div> ); }; export default List2;
useEffect
当一个组件渲染完成或者当某个state更新时,加载一个Ajax网络请求,使用useEffect可以实现。state实现不了,因为如果在某个组件中写上需要执行的语句,会导致任何state更新都会触发组件的更新(重新执行函数),不满足初次更新或者某个特定条件更新的两个条件
副作用:本来组件只需要执行完函数即可,但是现在却要求在特定的时间或者满足特定的条件下实现函数,即副作用
数组传入的是依赖项,不设置即为无依赖,即在第一次更新后,后续不会触发,有设置则相应的依赖项改变时会触发这个函数。数组中可以添加多个依赖项,只要其中一个被触发就会执行这个函数
import React, { FC, useState, useEffect } from "react";
import { produce } from "immer";
import QuestionCard from "./ListCard/QuestionCard";
const List2: FC = () => {
// 一传函数,二传数组
useEffect(() => {
console.log("加载Ajax网络请求");
}, []);
}
子组件销毁时的调用,能打印出来销毁,但是不知道为什么视频里打印出来单个,但是我打印的时候有其他几个子组件的销毁状态
// QuestionCard
useEffect(() => {
console.log("组件挂载");
return () => {
console.log("组件销毁" + id);
};
});
解决打印两次的问题。如图所示,为什么会显示打印两次,是因为组件自销毁了一次。从react18开始,useEffect在开发环境下会执行两次,目的是模拟组件创建,销毁,再创建的流程,方便及早发现问题。发布版本就不会有这个问题
通过下列指令发布
npm run build
执行完毕后在build目录下可以查看到项目的相关目录和内容,再通过以下指令运行服务器查看打包后的项目,生产环境下useEffect只执行一次
http-server
npm install -g serve
serve -s build
其他内置Hooks
useRef
- 一般用于操作DOM
import React, { FC, useRef } from "react";
const Demo: FC = () => {
// 初始化传入默认值为null,因为初始化的时候页面还没创建
// ts泛型
const inputRef = useRef<HTMLInputElement>(null);
function selectInput() {
// 当前指向
const inputElm = inputRef.current;
if (inputElm) inputElm.select();
}
return (
<div>
{/* 通过ref指向DOM节点 */}
<input ref={inputRef} defaultValue="hello"></input>
<button onClick={selectInput}>选中input</button>
</div>
);
};
export default Demo;
- 可传入普通的JS变量,但不会触发rerender(渲染),比如下面这个例子,打印出来的数值改变了,但是界面上展示的数值没有改变,state修改会触发rerender,想要显示出来就用state,只是保存结果就是用ref
const Demo: FC = () => {
const nameRef = useRef("双"); // 不是DOM节点而是普通的JS变量
function changeName() {
nameRef.current = "双月亮";
console.log(nameRef.current);
}
return (
<>
<p>name{nameRef.current}</p>
<button onClick={changeName}>change name</button>
</>
);
};
- 要和vue3 ref区分开,vue3的ref是用于响应式监听的
useMemo
函数组件,每次state更新都会重新执行函数,useMemo可以缓存数据,不用每次执行函数都重新生成,可用于计算量较大的场景,提高缓存性能
import React, { FC, useMemo, useState } from "react";
const Demo: FC = () => {
console.log("demo");
const [num1, setNum1] = useState(10);
const [num2, setNum2] = useState(20);
const [text, setText] = useState("hello");
// 跟useEffect相似
const sum = useMemo(() => {
console.log("gen sum");
return num1 + num2;
}, [num1, num2]);
return (
<>
<p>{sum}</p>
<div>
<p>{num1}</p>
<button
onClick={() => {
setNum1(num1 + 1);
}}
>
add num1
</button>
</div>
<div>
<p>{num2}</p>
<button
onClick={() => {
setNum2(num2 + 1);
}}
>
add num2
</button>
</div>
<div>
{/* form组件,受控组件 */}
<input onChange={(e) => setText(e.target.value)} value={text} />
</div>
</>
);
};
export default Demo;
useMemo依赖num1和num2,两个数字变化的时候会触发相应的函数,即重新计算sum,而text不是依赖项,所以text改变的时候不会重新计算sum。
可以把useMemo作为性能优化的手段,但不要把它当成语义上的保证,即不要认为每次使用都会真正的缓存,因为Memo可能会选择性的缓存或者遗忘一个值,然后下次再缓存
useCallback
作用和useMemo一致,也是用于缓存,准们用于缓存函数,可以理解为useMemo的语法糖,下列代码中的两个方法比较类似,fn1是什么时候加载什么时候打印,fn2只有相关依赖改变的时候才会触发
import React, { FC, useState, useCallback } from "react";
const Demo: FC = () => {
const [text, setText] = useState("hello");
const fn1 = () => console.log("fn1", text);
const fn2 = useCallback(() => {
console.log("fn2", text);
}, [text]);
return (
<>
<div>
<button onClick={fn1}>fn1</button> {" "}
<button onClick={fn2}>fn2</button>
</div>
{/* form组件,受控组件,即输入的时候会导致页面数值的变化,类似vue的双向数据绑定 */}
<input onChange={(e) => setText(e.target.value)} value={text} />
</>
);
};
export default Demo;
自定义hooks
抽离出来的这个组件定义为ts组件即可
// ts文件即可,因为没有JSX片段,即div等标签
import { useEffect } from "react";
function useTitle(title: string) {
useEffect(() => {
document.title = title;
}, []);
}
export default useTitle;
监听鼠标事件
import { useState, useEffect } from "react";
// 获取鼠标位置
function useMouse() {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
// "这个组件",即谁引用就指向谁
useEffect(() => {
const mouseMoveHandler = (event: MouseEvent) => {
setX(event.clientX);
setY(event.clientY);
};
// 监听鼠标事件
window.addEventListener("mousemove", mouseMoveHandler);
// 组件销毁时一定要解绑DOM事件,不然可能会出现内存泄漏问题
// 解绑的时候需要和绑定的时候的函数相同
return () => {
window.removeEventListener("mousemove", mouseMoveHandler);
};
}, []);
return { x, y };
}
export default useMouse;
模拟异步加载数据
import { useState, useEffect } from "react";
function getInfo(): Promise<string> {
// 模拟异步信息
return new Promise((resolve) => {
setTimeout(() => {
resolve(Date.now().toString());
}, 1500);
});
}
const useGetInfo = () => {
const [loading, setLoading] = useState(true);
const [info, setInfo] = useState("");
useEffect(() => {
getInfo().then((info) => {
setLoading(false);
setInfo(info);
});
}, []);
return { loading, info };
};
export default useGetInfo;
第三方Hooks
- ahooks国内流行 官网
- react-hooks国外流程
使用下列指令下载ahooks
npm install ahooks --save
下载完后看官网的文档选择调用即可,官网有相关的讲解
使用规则
- 必须用useXxx格式命名
- 只能在两个地方调用Hook(组件内,其他Hook内)
- 必须保证每次调用顺序一致(不能把hooks放在for if内部,只能放在函数第一层,可以在hooks内部加逻辑判断)
闭包陷阱
当异步函数获取state时,可能不是当前最新的state,可使用useRef解决
- 对闭包问题还有点疑惑,通过文心一言描述一下,下面的代码中,
setTimeout
函数在调用时捕获了count
变量的当前值(在其调用时的值)。由于setTimeout
是异步执行的,它在 3 秒后执行时,并不会自动获取count
的最新值。这就是闭包的作用:它保存了函数定义时的环境(在这里是count
的值),
import React, { FC, useState, useEffect, useRef } from "react";
const Demo: FC = () => {
const [count, setCount] = useState(0);
function add() {
setCount(count + 1);
}
function alertFn() {
setTimeout(() => {
alert(count);
}, 3000);
}
return (
<>
<p>闭包陷阱</p>
<div>
<span>{count}</span>
<button onClick={add}>add</button>
<button onClick={alertFn}>alert</button>
</div>
</>
);
};
export default Demo;
上述问题就是当异步函数获取state时,可能不是当前最新的state,接下来使用useRef解决,本质是因为count是一个值类型,而ref是引用类型
import React, { FC, useState, useEffect, useRef } from "react";
const Demo: FC = () => {
const [count, setCount] = useState(0);
const countRef = useRef(0);
useEffect(() => {
countRef.current = count;
}, [count]);
function add() {
setCount(count + 1);
}
function alertFn() {
setTimeout(() => {
// alert(count);
alert(countRef.current);
}, 3000);
}
return (
<>
<p>闭包陷阱</p>
<div>
<span>{count}</span>
<button onClick={add}>add</button>
<button onClick={alertFn}>alert</button>
</div>
</>
);
};
export default Demo;