文章目录
1. Hooks的简单了解
- 从功能上看:类组件约=函数组件+hooks
- 在React API中,凡事use开头的都是Hooks,比如:useState、useEffect、useCallback、useContext
1.1 为什么要学Hooks
- 之前存在的问题:
1. 类组件:生命周期难以理解,更难熟练运用
2. 复用状态的render-props和高阶组件思想理解起来不容易
3. 最火的状态管理方案 Redux,概念多,难以理解 - 学习hooks后:
1. 生命周期可以不用学。react hooks使用全新的理念来管理组件的运作过程(useEffect)
2. 高阶组件不用学。React hooks能够完美解决高阶组件想要解决的问题,并且更靠谱。(自定义hook)
3. redux不再是必需品。我们能够通过其他方式管理组件状态(useReducer+useContext)
2. useState
2.1 简单介绍
seState是允许你在React函数组件中添加state的Hook(钩子)
const [state,setState] = useState(initialvalue)
-
说明:
参数:state变量的初始值
返回值:数组,并且通过解构数组获取仅有的两个元素。第一个元素:state就是一个状态变量。第二个元素:setState是一个用于修改状态的函数。 -
简单小demo的效果:
-
代码:
import React,{useState} from 'react'
import {Slider} from 'antd-mobile';
//这里用到了antd-mobile,新手的话建议去官方文档
import './Demo.less'
export default function Demo() {
const [width, setWidth] = useState(100);
const [height,setHeight] = useState(100);
const [borderRadius,setBorderRadius] = useState(0);
// 行内样式对象
const style={
width:`${width}px`,
height:`${height}px`,
borderRadius:`${borderRadius}px`
}
return (
<div className='container'>
<p>width</p>
<Slider
value={width}
min={0}
max={300}
onChange={(val) => setWidth(val)}
/>
<p>height</p>
<Slider
value={height}
min={0}
max={300}
onChange={(val) => setHeight(val)}
/>
<p>radius</p>
<Slider
value={borderRadius}
min={0}
max={200}
onChange={(val) => setBorderRadius(val)}
/>
<div className="el" style={style}></div>
</div>
)
}
- 简单样式 :
* {
touch-action: none;
}
.container{
margin: 0px 20px;
.el{
width: 100px;
height: 100px;
background-color: pink;
margin: 0 auto;
margin-top: 50px;
}
p{
margin-top: 20px;
}
}
3. useEffect
3.1 简单介绍
Effect Hook 可以让你在函数组件中执行副作用操作。
- 副作用:数据获取、设置/销毁定时器以及手动更改React组件中的DOM都属于副作用。
- 在函数组件中,每当DOM完成一次渲染,都会有对应的副作用执行。
- 官网提示:如果你熟悉React class的生命周期函数,你可以把useEffect Hook看作 componentDidMount,componentDidUpdate和componentWillUnmount这三个函数组合。
- 个人建议:我们要抛开生命周期的固有思想,还要注意:生命周期和useEffect是完全不同的。
- 思想:
在function组件中,每当DOM完成一次渲染,都会有对应的副作用执行,useEffect用于提供自定义的执行内容,它的第一个参数(作为函数输入)就是自定义的执行内容。为了避免反复执行,传入第二个参数(由监听值组成的数组)作为比较(浅比较)变化的依赖,比较之后值都保持不变时,副作用逻辑就不再执行了。
语法:useEffect(callback,deps)
- callback:回调
- deps:依赖数组
- 回调:
DOM渲染完,执行一次useEffect的回调
每次更新之后也会执行useEffect的回调
每次我们重新渲染,都会生成新的effect,替换掉之前的
总结:某种意义上讲,effect更像时渲染结果的一部分–每个effect"属于"一次特定的渲染。 - 依赖数组
可以通过deps依赖项来控制回调执行的次数
3.2 基本使用
import React,{useEffect, useState} from 'react'
export default function Demo() {
// 回调执行:
// DOM渲染完,执行一次useEffect的回调
// 每次更新之后也会执行useEffect的回调
// 每次我们重新渲染,都会生成新的effect,替换掉之前的
// 总结:某种意义上讲,effect更像时渲染结果的一部分--每个effect"属于"一次特定的渲染
const [count,setCount] = useState(5);
//格式 useEffect(callback,deps)
useEffect(() => {
console.log('effect')
document.title = `您点击了${count}次`
})
return (
<div>
<h1>Count:{count}</h1>
<button onClick={() => setCount(count+1) }>add</button>
</div>
)
}
3. 3 依赖数组
- 可以通过deps依赖项来控制回调执行的次数。
import React,{useEffect, useState} from 'react'
export default function Demo() {
// 回调执行:
// DOM渲染完,执行一次useEffect的回调
// 每次更新之后也会执行useEffect的回调
// 每次我们重新渲染,都会生成新的effect,替换掉之前的
// 总结:某种意义上讲,effect更像时渲染结果的一部分--每个effect"属于"一次特定的渲染
const [count,setCount] = useState(0);
const [num,setNum] = useState(0);
//格式 useEffect(callback,deps)
useEffect(() =>{
console.log(`您点击了${count}次`);
},[count])
return (
<div>
<h1>Count:{count}</h1>
<button onClick={() => setCount(count+1) }>add</button>
<h1>Num:{num}</h1>
<button onClick={() => setNum(num+1) }>add</button>
</div>
)
}
- 总结: 依赖数组其实就是用来控制是否应该触发 callback(回调),从而能够减少不必要的计算,从而优化了性能,具体而言,只要依赖数组中的每一项与上一次渲染相比都没有改变,那么就跳过本次callback的执行。
3. 4 使用定时器
由于useEffect()默认函数组件状态改变后,就执行,但是有时候我们需要定时器只在函数组件渲染后只运行一次。
import React, { useEffect, useState } from 'react'
export default function Demo() {
const [count,setCount] = useState(0);
//控制回调执行的次数:使用依赖数组
// -如果想让回调只执行一次,依赖数组写为空数组[]
useEffect(() => {
setInterval(() => {
console.log('定时器执行了')
}, 1000);
},[])
return (
<div>
<h1>Count:{count}</h1>
<button onClick={() => setCount(count + 1)}>add</button>
</div>
)
}
3. 5 发送请求
import React, { useEffect, useState } from 'react'
import axios from 'axios'
export default function Demo() {
const [count,setCount] = useState(0);
//控制回调执行的次数:使用依赖数组
// -如果想让回调只执行一次,依赖数组写为空数组[]
//这种写法是我们在初次使用时要注意的,hooks推荐我们使用下面的方法,不推荐直接将callback变成async函数
// useEffect(async() => {
// const result = await axios.get('https://api-hmugo-web.itheima.net/api/public/v1/home/swiperdata');
// console.log(result);
// },[])
useEffect(() => {
const request = async() =>{
const result = await axios.get('https://api-hmugo-web.itheima.net/api/public/v1/home/swiperdata');
console.log(result);
};
request();
},[])
return (
<div>
<h1>Count:{count}</h1>
<button onClick={() => setCount(count+1)}>add</button>
</div>
)
}
3. 6 清除定时器
import React, { useEffect, useState } from 'react'
const Parent = () => {
const [isShow,setShow] = useState(true)
return (
<div>
<h1 onClick={() => setShow(!isShow)}>父组件</h1>
{
isShow ? <Demo/> : null
}
</div>
)
}
function Demo() {
useEffect(() => {
let time = setInterval(() => {
console.log('定时器开始了')
}, 1000);
// 返回的函数就是将要卸载的函数(子组件将要卸载时会触发)
// 卸载函数,清除定时器或其他的
return () => {
console.log('我要被卸载了');
clearInterval(time);
}
},[])
return (
<div>
子组件
</div>
)
}
export default Parent
3. 7 清除副作用总结
- 需要清除的副作用操作:比如定时器/延时器等。
- 不需要清除的副作用操作:比如发送请求获取数据。
那么在Hooks中,是如何清除(需要清除的)副作用操作的?
- 在生命周期函数中,我们使用componentWillUnmount()来执行组件销毁之前想要做的事情
- 但是在Hooks中,我们之前介绍了,我们可以通过返回销毁函数来充当componentWillUnmount()的功能。但是如果你想要以完全类比的方式来理解useEffect()里清除副作用,那么就不对了。
- 副作用回调函数中返回一个函数(销毁函数),用来副作用的清除工作,那么这个销毁函数什么时候执行呢?当组件将要被销毁的时候执行。是否和componentWillUnmount一样只执行一次呢?当然不是。
- 总结:
- 每次副作用执行,都会返回一个新的销毁函数 clear。
- clear 函数 会在下一个副作用逻辑之前执行 (DOM渲染之后)。
- 组件销毁也会执行。
- 简单总结为流程:数据改变 => DOM渲染 => 上次的销毁函数 => 执行当前的副作用逻辑
import React, { useEffect, useState } from 'react'
export default function Demo() {
const [count,setCount] = useState(0);
//控制回调执行的次数:使用依赖数组
useEffect(() => {
console.log('2-副作用逻辑')
let timer = setInterval(() => {
console.log(`定时器执行了,count为:${count}`)
}, 1000);
return () => {
console.log('3-销毁函数')
clearInterval(timer);
}
},[count])
console.log('1-DOM渲染');
return (
<div>
<h1>Count:{count}</h1>
<button onClick={() => setCount(count+1)}>add</button>
</div>
)
}
4. useRef
4.1 简单介绍
- useRef返回一个可变的ref对象,返回的ref对象在组件的整个生命周期内保持不变。可以类比React类组件中的ref。
- 用途 :
- 绑定DOM节点或React元素。
- 保持可变变量。
4.2 功能一:绑定DOM节点
import React, { useRef } from 'react'
export default function Demo() {
//useRef:调用返回一个ref对象
// 功能1:绑定DOM节点/React元素
const myRef = useRef();
const show = () => {
console.log(myRef.current.value);
}
return (
<div>
<input ref={myRef} type='text'/>
<button onClick={show}>点击获取</button>
</div>
)
}
4.3 功能二:绑定可变变量
通过保持可变变量来改造定时器
问题:在进行全局清除定时器时,遇到如果清除后,无法在页面render后在useEffect中再次调用。
解决具体步骤:
1.引入useRef
2.创建ref对象 const myRefTimer = useRef()
3.把定时器的timerID保存到ref对象里面的current里面
4. 后面读取/清除 =>myRefTimer.current
import React, { useEffect, useRef, useState } from 'react'
export default function Demo() {
const [count,setCount] = useState(0);
//创建ref对象
const myRefTimer = useRef();
useEffect(() => {
myRefTimer.current = setInterval(() => {
console.log('定时器',count)
}, 1000);
return () => {
clearInterval(myRefTimer.current);
}
},[count])
return (
<div>
<h1>Count:{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => clearInterval(myRefTimer.current)}>点击按钮清除定时器</button>
</div>
)
}
4.4 利用useState、useEffect、useRef做一个动画Demo
- Demo的动画演示:
- 代码实现
/*Demo基本样式 */
.ref_container{
width: 375px;
height: 667px;
background-color: pink;
border-top: 1px solid pink;
}
.ref_el{
width: 50px;
height: 50px;
background-color: red;
margin-top: 150px;
}
//Demo函数组件
import React, { useEffect, useRef, useState } from 'react'
// 动画库,需要下载引入
import anime from 'animejs'
import './Demo.css'
export default function Demo() {
const elementRef = useRef();
const [isAnimate1,setAnimate1] = useState(false);
const [isAnimate2,setAnimate2] = useState(false);
//利用副作用函数执行动画
useEffect(() => {
isAnimate1 && ml_animate01()
},[isAnimate1])
useEffect(() => {
isAnimate2 && ml_animate02()
},[isAnimate2])
//第一个动画
const ml_animate01 = () => {
anime({
// 要做动画的元素
targets:elementRef.current,
translateX:300,
backgroundColor:'#ff8822',
borderRadius:['0%','50%'],
complete:() => {
setAnimate1(false);
setAnimate2(true);
}
})
}
const ml_animate02 = () => {
anime({
targets:elementRef.current,
translateX:0,
backgroundColor:'#f00',
borderRadius:['50%','0%'],
complete:() => {
setAnimate2(false);
}
})
}
const clickShow = () => {
//开启第一个动画
setAnimate1(true);
}
return (
<div className='ref_container' onClick={clickShow}>
<div className="ref_el" ref={elementRef}>
</div>
</div>
)
}
5 useReducer
5.1 简单介绍:
- useReducer是useState的替代方案。它接收一个(形如(oldState,action) => newState的)reducer,并返回当前的state以及与其配套的dispatch方法。
- 语法格式:
语法说明:const [state,dispatch] = useReducer(reducer,initial);
- 参数:1-reducer 2-状态初始值
- 返回值:当前的state以及其配套的dispatch方法
- 使用场景:在某些场景下,useReducer会比useState更适用:例如state逻辑较复杂且包含多个子值,或者下一个state依赖于之前的state等。
5.2 使用useReducer实现计数器
import React, { useReducer } from 'react'
const reducer = (preState,action) => {
const {type,data} = action;
switch (type) {
case 'increment':
return preState + 1;
case 'decrement':
return preState - 1;
default:
return preState;
}
}
export default function Demo() {
const [count,dispatch] = useReducer(reducer,0);
return (
<div>
<h1>Count:{count}</h1>
<button onClick={() => dispatch({type:'increment'})}>+1</button>
<button onClick={() => dispatch({type:'decrement'})}>-1</button>
</div>
)
}
5.3 使用useReducer实现计数器(对象形式)
import React, { useReducer } from 'react'
//初始值
let initState = {count:4}
const reducer = (preState,action) => {
const {type,data} = action;
switch (type) {
case 'increment':
return {count:data + 1};
case 'decrement':
return {count:data - 1};
default:
return preState;
}
}
export default function Demo() {
const [state,dispatch] = useReducer(reducer,initState)
return (
<div>
<h1>Count:{state.count}</h1>
<button onClick={() => dispatch({type:'increment',data:state.count})}>+1</button>
<button onClick={() => dispatch({type:'decrement',data:state.count})}>-1</button>
</div>
)
}
6. useContext
6.1 简单介绍
- useContext,能够让我们在函数组件中使用context的能力,替换之前的context.Consumer。
- 语法格式:
const value = useContext(MyContext);
说明:
- 参数:一个context对象
- 返回值:返回Provider提供的value值
- 在函数组件中,使用useContext Hooks可以实现三个功能:
- 功能1:实现从上到下跨多层组件传递数据。
- 功能2:兄弟组件数据交互(共享数据)。
- 功能3:配合useReducer组合redux。
6.2 实现从上到下跨多层组件传递数据
import React, { useContext, useState } from 'react'
// context 在React中,解决了一个跨多层组件传递数据的问题
//使用context第一步 :创建 context对象
const myContext = React.createContext();
//父组件
const Parent = () => {
const [color,setColor] = useState('red');
{/* 第二步:使用context 里面的 Provider组件包裹着要接收数据的组件 */}
return (
<myContext.Provider value={color}>
<div onClick={() => setColor('blue')}>
<h1>父组件</h1>
<Child/>
</div>
</myContext.Provider>
)
}
//子组件
const Child = () => {
return (
<>
<h1>子组件</h1>
<Sun/>
</>
)
}
//孙组件
const Sun = () => {
//第三步:通过useContext 来接收
//参数:myContext
//返回值:就是provider提供的数据
let data = useContext(myContext);
console.log(data)
return (
<div>
<h1 style={{color:data}}>孙组件</h1>
</div>
)
}
export default Parent
6.3 利用useContext,配合useReducer组合redux
该例子将文件模块化,这个例子不算大,没有考虑样式,只有简单的样式,文件会有些多。下面看一下案例的功能动图:
- 动图
- 代码
Demo.css:
.container{
width:500px;
margin:0 auto;
}
.container ul li span {
display: inline-block;
width: 143px;
user-select: none;
}
Demo.jsx:
import React from 'react'
import TodoForm from './TodoForm'
import TodoList from './TodoList'
import TodoFooter from './TodoFooter'
import { MyProvider } from './context'
import './Demo.css'
export default function Demo() {
return (
<div className='container'>
<MyProvider>
<TodoForm/>
<TodoList/>
<TodoFooter/>
</MyProvider>
</div>
)
}
context.js:
import React, { useReducer } from "react";
import {reducer1, reducer2} from './reducer'
const initValue = [
{
id:1,
name:'吃饭',
done:false
},
{
id:2,
name:'睡觉',
done:false
}
]
export const MyContext = React.createContext();
export const MyProvider = ({children}) =>{
let [list,dispatch1] = useReducer(reducer1,initValue);
const [filter,dispatch2] = useReducer(reducer2,'all');
//根据过滤条件筛选我们希望的数据
switch (filter) {
case 'all':
list = list;
break;
case 'active':
list = list.filter(item => !item.done);
break;
case 'completed':
list = list.filter(item => item.done);
default:
break;
}
const value = {
list,
dispatch1,
filter,
dispatch2
}
return(
<MyContext.Provider value={value}>{children}</MyContext.Provider>
)
}
reducer.js:
export const reducer1 = (preState,action) => {
const {type,data} = action;
switch (type) {
case 'add':
return [...preState,{id:Date.now(),name:data,done:false}];
case 'del':
return preState.filter((item) => item.id !== data);
//切换任务是否完成
case 'toggle':
let newState = [...preState];
let todo = newState.find(item =>item.id === data);
todo.done = !todo.done;
return newState;
default:
return preState;
}
}
export const reducer2 = (preState,action) => {
const {type,data} = action;
switch (type) {
case 'filter':
return data;
default:
return preState;
}
}
TodoForm.jsx:
import React, { useContext, useRef } from 'react'
import { MyContext } from './context';
export default function TodoForm() {
const {dispatch1} = useContext(MyContext);
const inputRef = useRef();
const addTodo = () => {
const {value} = inputRef.current;
if(value)
dispatch1({type:'add',data:value});
inputRef.current.value = '';
}
return (
<div>
<input type='text' ref={inputRef}/>
<button onClick={addTodo}>添加</button>
</div>
)
}
TodoList.jsx:
import React,{useContext} from 'react'
import { MyContext } from './context'
export default function TodoList() {
const {list,dispatch1} = useContext(MyContext)
return (
<div>
<ul>
{
list.map((item) => {
return (
<li key={item.id}>
<span style={{textDecoration:item.done ? 'line-through' : 'none'}} onClick={() => dispatch1({type:'toggle',data:item.id})}>{item.name}</span>
<button onClick={ () => dispatch1({type:'del',data:item.id})}>×</button>
</li>
)
})
}
</ul>
</div>
)
}
TodoFooter.jsx:
import React, { useContext } from 'react'
import { reducer2 } from './reducer'
import { MyContext } from './context'
export default function TodoFooter() {
const {filter,dispatch2} = useContext(MyContext);
return (
<div>
<button disabled={filter === 'all'} onClick={() => dispatch2({type:'filter',data:'all'})}>ALL</button>
<button disabled={filter === 'active'} onClick={() => dispatch2({type:'filter',data:'active'})}>ACTIVE</button>
<button disabled={filter === 'completed'} onClick={() => dispatch2({type:'filter',data:'completed'})}>COMPLETED</button>
</div>
)
}
7. useMemo
7.1 简单介绍
- useMemo可以让你在函数组件中 缓存计算结果。
- 语法格式:
const memoizedValue = useMemo(() => fn(a,b),deps[a,b]);
语法说明:
它接收两个参数:
第一个参数为回调函数(计算过程fn,必须返回一个结果)。
第二个参数是依赖项(数组),当依赖项中某一个发生变化,结果将会重新计算。
注意:因为是缓存计算结果,所以参数1回调函数里面一定要有return返回值
import React, { useMemo, useState } from 'react'
export default function Demo() {
const [count,setCount] = useState(0);
const [other,setOther] = useState(0);
// 封装一个累加的函数
const calCount = (count) => {
console.log('重新计算了');
let _sum = 0;
for(let i = 1;i <= count; i ++){
_sum += i;
}
return _sum;
}
// 计算累加结果
//普通使用
// let sum = calCount(count);
//使用useMemo
let sum = useMemo(() => calCount(count),[count])
return (
<div>
<h1>Count-{count}-{sum}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
<h1>Other-{other}</h1>
<button onClick={() => setOther(other + 1)}>+1</button>
</div>
)
}
8. useCallback
8.1 简单介绍
useCallback 可以让你在函数组件中缓存计算函数
- 语法:
- 语法格式:
const fnA = useCallback(fnB,[a]);
- 语法说明:
- useCallback 会将我们传递给它的函数fnB返回,并且将这个结果fnA缓存
- 当依赖a变更时,会返回新的函数。把内联回调函数及依赖项数组作为参数传入useCallback,它将返回该回调函数的memoized版本,该回调函数仅在某个依赖项改变时才会更新,当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
import React, { useCallback, useState } from 'react'
let set = new Set();
const Demo = ()=> {
const [count,setCount] = useState(0);
const [other,setOther] = useState(0);
let inputChange = e => {
console.log(e.target.value);
}
//缓存函数
// 依赖数组 依赖项是count ,所以只有count 不变,才会 缓存 inputChange函数
inputChange = useCallback(inputChange,[count])
//添加到set
set.add(inputChange)
console.log('长度',set.size)
return (
<div>
<input type='text' onChange={inputChange}/>
<button onClick={() => setCount(count + 1)}>{count}</button>
<button onClick={() => setOther(other + 1)}>{other}</button>
</div>
)
}
export default Demo;