为什么是hook?
| 名称 | Function Component | Class Component |
|---|---|---|
| 性能 | 90分 | 88分 |
| this | 无 | 有 |
| 函数编程 | 是 | 否 |
| 复用 | 简单 | 嵌套层级深 |
| 生命周期 | 优雅 | 复杂 |
| hook | 有 | 无 |
| 错误处理 | 无 | 有 |
| 代码顺序 | 有严格顺序 | 无 |
| 闭包 | 会有闭包问题 | 无 |
基础Hook
一、useState
1.1 基本使用
- 通过在函数组件里调用它来给组件添加一些内部 state,React 会在重复渲染时保留这个 state
- useState 唯一的参数就是初始 state
- 返回一个数组第一项是state,第二项更新 state 的函数
- 在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同
- setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列
function Counter(){
const [number,setNumber] = useState(0);
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
</>
)
}
1.2 函数式更新
- 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setTimeout(() => {
console.log(count,'setTimeout')
// 在3000秒内无论点击多少次,count都是当前的状态计算(根据点击次数会执行多次),所以并不会根据点击次数累加
setCount(count + 1)
}, 1000);
}
function handleClickFn() {
setTimeout(() => {
// 点击的次数多少函数就执行几次,
// 比如 3000秒点击三次: 相当于定时器生成三个,结果累计相加=3 prevCount上次状态
setCount((prevCount) => {
return prevCount + 1
})
}, 3000);
}
return (
<>
Count: {count}
<button onClick={handleClick}>handleClick+</button>
<button onClick={handleClickFn}>handleClickFn+</button>
</>
);
}
如果你需要用到上次的值,可以使用函数更新的方式。
1.3 惰性初始 state
- initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略
- 若果初始需要复杂计算,initialState可以是一个函数,函数只在初次渲染的时候被调用
function Counter3(){
const [{name,number},setValue] = useState(()=>{
return {name:'计数器',number:0};
});
return (
<>
<p>{name}:{number}</p>
<button onClick={()=>setValue({number:number+1})}>+</button>
</>
)
}
1.4 跳过 state 更新
调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)
需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。
也就是说 setState 同一个值, 当前组件可能渲染。这里说的渲染是指函数会被调用一下, console.log 会打印出来, 但不会继续向下渲染, 所以没什么大不了的。
文字不太能说明什么,看下面🍐:
import React,{useState,createContext,useContext, memo, useMemo, useEffect} from 'react';
const _num = [
{
count :1
}
]
export default function Parent(){
const [show, setShow] = useState(false)
const [num ,setNum] = useState(_num);
console.log('渲染了');
useEffect(() => {
// 这里_num都是同一个值,但是还会会引起页面函数执行。
setNum(_num)
}, [show]);
return (
<React.Fragment>
<button onClick={()=>{ setNum(_num)}}>{num[0].count}</button>
<button onClick={()=>setShow(!show)}>button</button>
</React.Fragment>
)
}
因为
callback function就像泼出去的水, 是收不回来的。函数的调用者想在调用到一半时中断可能比较困难, 除非setState内部throw异常。
使用总结
😊1. useState的初始值,只在第一次有效
✋2. 按照有序的方式使用usestate不得在循环判断等条件语句使用
😡3. useState 不会自动合并更新对象,更新对象时使用扩展符{...counter,number:6}
二、useEffect
有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。
2.1 基本使用
useEffect有两个参数, 参数一: function(执行项),参数二:array(依赖项)
function Effect(){
// useEffect 里面的函数会在组件全部挂载完后和组件更新完成后执行(也就是说在paint后)
// 省略第二个参数每次渲染都会执行(尽量避免此操作)
useEffect(() => {
console.log('执行了')
});
return (
<div>
<button onClick={}>useEffect</button>
</div>
)
}
2.2 模拟生命周期componentDidMount
// 😊 如果空数组,只会在第一次挂载后执行,类似commponentDidMount
useEffect(() => {},[]);
2.3 依赖某个state或props值
function Effect(){
const [name,setName] = useState('xy')
const [age,setAge] = useState(0)
// 😊 如果数组不为空,则会在数组中依赖的变量改变时重新执行
useEffect(()=>{
console.log('name更新了')
},[name]) // 依赖可以多个
useEffect(()=>{
console.log('age更新了')
},[age]) // 依赖可以多个
return (
<>
<button onClick={()=>{setName('xxxx')}}>{name}<button>
</>
)
}
2.4 模拟componentUnMount,清楚副作用
- 副作用函数还可以通过返回一个函数来指定如何清除副作用 为防止内存泄漏,清除函数会在组件卸载前执行。
- 另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除
function Effect(){
const [age,setAge] = useState(0)
// useEffect每次执行,都会先执行上次return的函数,再执行内部回掉函数
useEffect(()=>{
console.log('产生新定时器')
let time = setInterval(()=>{
console.log('sss')
},1000)
return ()=>{
console('清理上次的定时器')
clearInterval(time)
}
},[age])
return(
<div>
<button onClick={()=>{setAge(age+1)}}>+</button>
</div>
)
}
使用总结
😊1. 要把该函数在 useEffect 中申明,不能放到外部申明,然后再在 useEffect 中调用
function Example({ someProp }) {
function doSomething() {
console.log(someProp);
}
useEffect(() => {
doSomething();
}, []); // 🔴 这样不安全(它调用的 `doSomething` 函数使用了 `someProp`)
}
要记住 effect 外部的函数使用了哪些 props 和 state 很难。这也是为什么 通常你会想要在 effect 内部 去声明它所需要的函数。 这样就能容易的看出那个 effect 依赖了组件作用域中的哪些值:
function Example({ someProp }) {
useEffect(() => {
function doSomething() {
console.log(someProp);
}
doSomething();
}, [someProp]); // ✅ 安全(我们的 effect 仅用到了 `someProp`)
}
只有 当函数(以及它所调用的函数)不引用 props、state 以及由它们衍生而来的值时,你才能放心地把它们从依赖列表中省略。下面这个案例有一个 Bug:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
async function fetchProduct() {
const response = await fetch('http://myapi/product' + productId); // 使用了 productId prop
const json = await response.json();
setProduct(json);
}
useEffect(() => {
fetchProduct();
}, []); // 🔴 这样是无效的,因为 `fetchProduct` 使用了 `productId`
// ...
}
推荐的修复方案是把那个函数移动到你的 effect 内部。这样就能很容易的看出来你的 effect 使用了哪些 props 和 state,并确保它们都被声明了:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// 把这个函数移动到 effect 内部后,我们可以清楚地看到它用到的值。
async function fetchProduct() {
const response = await fetch('http://myapi/product' + productId);
const json = await response.json();
setProduct(json);
}
fetchProduct();
}, [productId]); // ✅ 有效,因为我们的 effect 只用到了 productId
// ...
}
- 只在更新时执行effect
- 手动存储一个布尔值来表示是首次渲染还是后续渲染,然后在effect 中检查这个标识
function effect(){
const countRenderRef = useRef(false);
const [num, setNum] = useState(0)
useEffect(function afterRender() {
if(countRenderRef.current){
console.log(countRenderRef.current, 'num更新执行的操作')
// doSomething()
}else{
countRenderRef.current = true;
}
},[num]);
return (
<div>
I've rendered {countRenderRef.current.toString()} times
<button onClick={()=>{setNum(num+1)}}>{num}</button>
</div>
);
}
三、useContext
3.1 基本使用
- 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
- 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
- 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
//father.jsx
import React, { useState, useEffect, createContext } from 'react';
import Childern from './children'
// 创建上下文
export const FatherContext = createContext();
export default function Father(){
const [num,setNum] = useState(0)
return (
<>
<p>{num}</p>
<FatherContext.Provider value={{num,setNum}}>
<Childern/>
</FatherContext.Provider>
</>
)
}
// children.jsx
import React, { useState, useEffect, useContext } from 'react';
import Father from './father';
export default function Childern(props){
const { num,setNum } = useContext(FatherContext);
useEffect(()=>{
setNum(num+1)
},[])
reurn (
<>
<p>{num}</p>
</>
)
}
3.2 使用总结
useContext详解
😊1. 一般用在组件通讯
额外Hook
四、useReducer
4.1 useReducer使用
- useState的内部实现就是通过useReducer
- useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。
- 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。
const [state, dispatch] = useReducer(reducer, initialArg, init);
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function MyUseReducer() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
4.2 惰性初始化
- 可以将init函数作为useReducer的第三个参数,初始state将被设置为init(initialArg)
- 有利于将计算的逻辑提取,同样对重制state也很方便
// 稍加改造
const initialCount = 0;
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function MyUseReducer() {
const [state, dispatch] = useReducer(reducer, initialCount , init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
五、useRef
六、useImperativeHandle
七、useLayoutEffect
性能优化
😁从减少代码执行(渲染次数)入手
八、useMemo
8.1 理解React.memo
- 使用 React.memo ,将函数组件传递给 memo 之后,就会返回一个新的组件,新组件的功能:如果接受到的属性不变,则不重新渲染函数;
举个🍐
import React,{memo,useState} from 'react'
function Child(props){
console.log('Child==render',props)
return (
<div>
<p>Child</p>
</div>
)
}
// 如果不使用memo 父组件执行,Child也会渲染,即使age值没有变化
Child = memo(Child)
function Father(){
const [num, setNum] = useState(0)
const [age, setAge] = useState(10)
return (
<>
<Child age={age}></Child>
<button onClick={()=>{setNum(num+1)}}>{num}</button>
</>
)
}
稍加修改
function Child(props){
console.log('Child render',props)
return (
<div>
<p>Child</p>
</div>
)
}
Child = memo(Child)
function Father(){
const [num, setNum] = useState(0)
const [age, setAge] = useState(10)
// Father执行, tempAge值虽然没变, 此时的tempAge是一个新对象内存地址发生改变,所以Child也会重新渲染
const tempAge = {age}
return (
<>
<Child person={tempAge}></Child>
<button onClick={()=>{setNum(num+1)}}>{num}</button>
</>
)
}
可以使用useMemo 解决以上问题
8.2 更深入的useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
根据官方文档的介绍我们可理解:在a和b的变量值不变的情况下,memoizedValue的值不变。即:useMemo函数的第一个入参函数不会被执行,从而达到节省计算量的目的。
🍊🍐
function Child(props){
console.log('Child render',props)
return (
<div>
<p>Child</p>
</div>
)
}
Child = memo(Child)
function Father(){
const [num, setNum] = useState(0)
const [age, setAge] = useState(10)
const tempAge = useMemo(()=>{
return {age}
},[age])
return (
<>
<Child person={tempAge}></Child>
<button onClick={()=>{setNum(num+1)}}>{num}</button>
</>
)
}
😂😁😊完美解决!
九、useCallback
9.1 基本使用
const memoizedCallback = useCallback(
// deeps: 和useEffect一样,如果没有每次渲染时都会运行,空数组只在挂载时运行。
// 如果有依赖项:有变化则会重新声明回调函数
() => {
doSomething(a, b);
},
[a, b],
);
根据官网文档的介绍我们可理解:在a和b的变量值不变的情况下,memoizedCallback的引用不变。即:useCallback的第一个入参函数会被缓存,从而达到渲染性能优化的目的。
9.2 深入的useCallback
🍊🍐
function Child(props){
console.log('Child render',props)
return (
<div>
<p>Child</p>
<button onClick={()=>{props.memoized()}}>子age{props.value.age}</button>
</div>
)
}
Child = memo(Child)
function Father(){
const [num, setNum] = useState(0)
const [age, setAge] = useState(10)
const tempAge = useMemo(()=>{
return {age}
},[age])
// 如果依赖属性不变, useCallback第一个参数函数会缓存。 所以修改num,子组件不会渲染。
const memoizedCallback = useCallback(()=>{
return setAge(age+1)
},[age])
return (
<>
<Child value={tempAge} memoized={memoizedCallback}></Child>
<button onClick={()=>{setNum(num+1)}}>父num{num}</button>
</>
)
}
9.3 useMemo和useCallback总结
useCallback和useMemo都可缓存函数的引用或值,但是从更细的使用角度来说useCallback缓存函数的引用,useMemo缓存计算数据的值。
useCallback第一个参数不会执行,useMemo会执行。
其他:
优化点:项目中使用redux管理,去掉不相关的props,避免不必要的渲染
一把锁的props属性:
错误×
只使用组件中用到的属性
正确✓
十、参考
十一、谢谢🙏
有错误地方还请纠正