HOOK
HOOK简介
HOOK(钩子)是React16.8.0版本之后出现的
组件:函数组件(无状态组件)、类组件
类组件的问题:
-
this指向问题
-
生命周期很繁琐
-
其它问题
HOOK的出现是为了增强函数组件的功能,使之理论上能成为类组件的替代品
官方目前没有计划取消类组件
HOOK在类组件中不能使用,HOOK本质上是一个函数,该函数可以挂载任何功能
HOOK种类
命名是以use开头
- useState
- useEffect
- 其它
State Hook
State HOOK是一个函数组件中使用的函数(useState),用于在函数组件使用状态
useState函数有一个参数,这个参数的值表示状态的默认值,不填就是undefined
useState()返回一个数组,数组有两项,第一项是状态的值,第二项是一个改变状态的函数
import React,{useState} from 'react'
export default function App(){
// const state = useState(0);
// 返回状态值,和一个改变该状态的函数
const [n,setN] = useState(0);
return <div>
<button onClick={()=>{
setN(n-1);
}}>-</button>
<span>{ n }</span>
<button onClick={()=>{
setN(n + 1);
}}>+</button>
</div>
}
一个函数组件中可以有多个状态,利于横行切分关注点
import React, { useState } from 'react'
export default function App() {
// const state = useState(0);
const [n, setN] = useState(0);
const [visible, setVisible] = useState(true);
return <div>
<p style={{ display: visible ? 'block' : 'none' }}>
<button onClick={() => {
setN(n - 1);
}}>-</button>
<span>{n}</span>
<button onClick={() => {
setN(n + 1);
}}>+</button>
</p>
<button onClick={() => {
setVisible(!visible);
}}>显示/隐藏</button>
</div>
}
useState HOOK原理
对于一个函数组件来说,函数的调用就会重新进行渲染,函数中的状态改变也会导致函数组件重新运行。那么相应也会运行setState函数。
使用函数组件时,会创建一个状态数组,用于记录状态值。当函数组件再次调用会去检查相应的状态数组,由于之前就将状态记录在状态数组中,就会忽略调状态的默认值。这样就不会导致状态值变成默认值。
而且每使用一个函数组件他们的状态是相互独立的,因为创建的状态数组是不一样的。
useState使用的细节
-
useState最好写到函数的起始位置,便于阅读
-
useState严禁出现在代码块(判断、循环中),会导致状态值不对应(对应原理)。
-
useState返回的函数(数组的第二项)引用不变(节约内存空间)。
-
如果使用函数改变状态值,两次数据一样(使用Object.is比较),不会导致重新渲染。为了达到优化的效果。
-
使用函数改变数据,传入的值不会和原来的数据进行合并,而是直接替换。
import React, { useState } from 'react'
export default function App() {
const [data, setData] = useState({
x:1,
y:2
})
return <div>
<p>
x:{data.x},y:{data.y}
<button onClick={() => {
setData({
...data,
x:data.x+1
});
}}>+</button>
</p>
</div>
}
- 如果要实现强制刷新组件
- 类组件中使用:forceUpdate函数
- 函数组件:
import React, { Component } from 'react'
export default class App extends Component {
render() {
return (
<div>
<button
onClick={()=>{
// 不会运行shouldCompoentUpdate函数
this.forceUpdate();//强制刷新
}}
>强制刷新</button>
</div>
)
}
}
import React from 'react'
export default function App() {
const [,forceUpdate] = useState({});
return (
<div onClick={()=>{
// 空对象的地址不一样,导致刷新
forceUpdate({});
}}>
<button>强制刷新</button>
</div>
)
}
-
如果某些状态之间没有关系,应该分开写,不要合并一个对象
-
和类组件的状态一样,函数组件中改变状态可能是异步的(DOM事件中),多个状态改变回合并(提高效率),此时,不能信任之前的状态,而应该使用回掉函数该变状态。如果状态要依赖之前的状态,使用回掉函数。
import React,{useState} from 'react'
export default function App(){
console.log('render'); //合并运行一次
const [n,setN] = useState(0);
return <div>
<button onClick={()=>{
// setN(n-1);
// setN(n-1);
}}>-</button>
<span>{ n }</span> //2
<button onClick={()=>{
// setN(n + 1); //不会立即改变,事件完成之后一起改变
// setN(n + 1); // 此时n还是为0。
setN(preN => preN + 1); // 传入的函数,在事件完成之后统一运行
setN(preN => preN + 1);
}}>+</button>
</div>
}
useEffect
副作用函数
副作用操作:
操作DOM
计时器函数
操作浏览器
ajax请求
其它异步操作
本地存储
其它外部操作
import React, { useState, useEffect } from 'react'
export default function App() {
const [n, setN] = useState(0);
// 处理副操作
useEffect(() => {
document.title = `计时器:${n}`;
})
return (
<div>
<span>{n}</span>
<button onClick={() => {
setN(n + 1);
}}>+</button>
</div>
)
}
useEffect(一个函数),该函数的运行时间点是在页面完成真实的UI渲染之后。
-
也就是在浏览器把结果渲染出来之后,才运行。因此是异步的,不会导致浏览器阻塞。如果要复杂计算,使用这个无可厚非。
-
useEffect与类组件componentDidMount和componentDidUpdate的区别
- componentDidMount和componentDidUpdate是更改了真实DOM,但是用户还没看到UI更新
- useEffect中的副作用函数,更改了真实DOM,并且用户已经看到了UI更新。
-
每个函数组件中,可以多次使用,但是不要放在代码块(判断,循环中)。只要改变了要渲染的值就会重新运行函数。
import React, { useState, useEffect } from 'react'
const ref = React.createRef();
window.timer = null;
function stop() {
clearInterval(window.timer);
window.timer = null;
}
/**
* 该div是每次渲染完成后,从(0,0)坐标在10秒内移动到(props.left,props.top)位置
* @param {*} props
*/
function MoveDiv(props) {
console.log('渲染函数')
useEffect(() => {
console.log('副作用函数')
// 每一次渲染启动了多个计时器
// clearInterval(window.timer);
// window.timer = null;
stop();
// 渲染完才运行
const div = ref.current;
let curNum = 0; //移动的次数
const disX = props.left / 1000;
const disY = props.top / 1000;
window.timer = setInterval(() => {
curNum++;
const newLeft = disX * curNum;
const newTop = disY * curNum;
div.style.left = newLeft + 'px';
div.style.top = newTop + 'px';
// console.log(div.style.left, div.style.top)
if (curNum === 1000) {
// clearInterval(window.timer);
stop();
}
}, 10);
return ()=>{
console.log('清理函数')
}
})
return <div style={{
width: 100,
height: 100,
left: 0,
top: 0,
position: 'fixed',
background: "blue"
}} ref={ref}>
</div>
}
export default function App() {
const [point, setPoint] = useState({ x: 0, y: 0 })
const [visible, setVisible] = useState(true)
return <div>
{
visible &&
<div style={{
paddingTop: 200
}}>
<div>
x:<input type='input' value={point.x} onChange={(e) => {
setPoint({
...point,
x: e.target.value
})
}}></input>
y:<input type='input' value={point.y} onChange={(e) => {
setPoint({
...point,
y: e.target.value
})
}}></input>
</div>
<MoveDiv left={point.x} top={point.y}></MoveDiv>
</div>
}
<button onClick={() => {
setVisible(!visible);
}}>显示/隐藏</button>
</div>
}
<!-- 对应到第四点 -->
<!-- 运行结果为 -->
<!-- 第一次渲染 -->
渲染组件
副作用函数
<!-- 该值后渲染 -->
渲染组件
清理函数
副作用函数
当输入x,y后,window.timer存在值,这时点击btn,隐藏元素,元素不存在页面中,然后查看window.timer的值还存在,也就要等10秒后window.timer才为null。
- 为了解决上述问题,useEffect函数会返回一个函数,该函数叫做清除函数
- 该函数运行在副作用函数之前。
- 首次渲染不会运行。
- 组件被销毁时一定会运行
useEffect(()=>{return 清除函数});
上面例子利用清理函数改进后
import React, { useState, useEffect } from 'react'
const ref = React.createRef();
window.timer = null;
function stop() {
clearInterval(window.timer);
window.timer = null;
}
/**
* 该div是每次渲染完成后,从(0,0)坐标在10秒内移动到(props.left,props.top)位置
* @param {*} props
*/
function MoveDiv(props) {
useEffect(() => {
// 每一次渲染启动了多个计时器
// clearInterval(window.timer);
// window.timer = null;
// 渲染完才运行
const div = ref.current;
let curNum = 0; //移动的次数
const disX = props.left / 1000;
const disY = props.top / 1000;
window.timer = setInterval(() => {
curNum++;
const newLeft = disX * curNum;
const newTop = disY * curNum;
div.style.left = newLeft + 'px';
div.style.top = newTop + 'px';
// console.log(div.style.left, div.style.top)
if (curNum === 1000) {
// clearInterval(window.timer);
stop();
}
}, 10);
return stop;//直接返回清理函数
})
return <div style={{
width: 100,
height: 100,
left: 0,
top: 0,
position: 'fixed',
background: "blue"
}} ref={ref}>
</div>
}
export default function App() {
const [point, setPoint] = useState({ x: 0, y: 0 })
const [visible, setVisible] = useState(true)
return <div>
{
visible &&
<div style={{
paddingTop: 200
}}>
<div>
x:<input type='input' value={point.x} onChange={(e) => {
setPoint({
...point,
x: e.target.value
})
}}></input>
y:<input type='input' value={point.y} onChange={(e) => {
setPoint({
...point,
y: e.target.value
})
}}></input>
</div>
<MoveDiv left={point.x} top={point.y}></MoveDiv>
</div>
}
<button onClick={() => {
setVisible(!visible);
}}>显示/隐藏</button>
</div>
}
- useEffect可以传递第二参数,是一个数组,数组记录着副作用的依赖数据,只有依赖数据不一样时才会执行副作用。当传递了依赖数组,如果依赖数据没有发生改变,
那么副作用函数仅在第一次渲染后运行
清理函数仅在卸载组件后运行
import React, { useState, useEffect } from 'react'
const ref = React.createRef();
window.timer = null;
function stop() {
clearInterval(window.timer);
window.timer = null;
}
/**
* 该div是每次渲染完成后,从(0,0)坐标在10秒内移动到(props.left,props.top)位置
* @param {*} props
*/
function MoveDiv(props) {
useEffect(() => {
// 每一次渲染启动了多个计时器
// clearInterval(window.timer);
// window.timer = null;
console.log('12')
// 渲染完才运行
const div = ref.current;
let curNum = 0; //移动的次数
const disX = props.left / 1000;
const disY = props.top / 1000;
window.timer = setInterval(() => {
curNum++;
const newLeft = disX * curNum;
const newTop = disY * curNum;
div.style.left = newLeft + 'px';
div.style.top = newTop + 'px';
// console.log(div.style.left, div.style.top)
if (curNum === 1000) {
// clearInterval(window.timer);
stop();
}
}, 10);
return stop;//直接返回清理函数
},[props.left,props.top])
return <div style={{
width: 100,
height: 100,
left: 0,
top: 0,
position: 'fixed',
background: "blue"
}} ref={ref}>
</div>
}
export default function App() {
const [point, setPoint] = useState({ x: 100, y: 100 })
const [visible, setVisible] = useState(true)
const refX = React.createRef();
const refY = React.createRef();
return <div>
{
visible &&
<div style={{
paddingTop: 200
}}>
<div>
x:<input type='number' ref={refX}></input>
y:<input type='number' ref={refY}></input>
<button onClick={(e)=>{
setPoint({
x:parseInt(refX.current.value),
y:parseInt(refY.current.value)
})
}}>确定</button>
</div>
<MoveDiv left={point.x} top={point.y}></MoveDiv>
</div>
}
<button onClick={() => {
setVisible(!visible);
}}>显示/隐藏</button>
</div>
}
import React, { useState, useEffect } from 'react'
const ref = React.createRef();
window.timer = null;
function stop() {
clearInterval(window.timer);
window.timer = null;
}
/**
* 该div是每次渲染完成后,从(0,0)坐标在10秒内移动到(props.left,props.top)位置
* @param {*} props
*/
function MoveDiv(props) {
console.log('渲染组件')
useEffect(() => {
console.log('副作用函数,仅在挂载时运行一次')
return ()=>{
console.log('清理函数,仅在卸载时运行一次')
}
},[]);//依赖数组写成空数组就可以实现
return <div style={{
width: 100,
height: 100,
left: 0,
top: 0,
position: 'fixed',
background: "blue"
}} ref={ref}>
</div>
}
export default function App() {
const [visible, setVisible] = useState(true);
const [,forceUpdate] = useState({})
return <div>
{
visible &&
<div style={{
paddingTop: 200
}}>
<button onClick={()=>{
forceUpdate({});
}}>强制刷新</button>
<MoveDiv></MoveDiv>
</div>
}
<button onClick={() => {
setVisible(!visible);
}}>显示/隐藏</button>
</div>
}
- 副作用函数中使用函数上下文的变量,不会实时拿到数据,是由于闭包的影响。
import React, { useState, useEffect } from 'react'
export default function App() {
const [n, setN] = useState(0);
useEffect(() => {
setTimeout(()=>{
// n指向,当前App函数调用时的n
console.log(n); //点击button5次,隔5秒就会输出0,1,2,3,4,5
},5000)
})
return <div>
{n}
<button onClick={() => {
setN(n+1);
}}>n+1</button>
</div>
}
import React, { useState, useEffect } from 'react'
export default function App() {
const [n, setN] = useState(10);
useEffect(() => {
if(n===0){
return;
}
setTimeout(()=>{
setN(n-1)
},1000)
},[n])
return <div>
{n}
</div>
}
- 副作用函数会覆盖。尽量保持副作用函数稳定
import React, { useState, useEffect } from 'react'
let n = 1;
function test1(){
console.log('test1');
return ()=>{
console.log('test1清理函数')
}
}
function test2(){
console.log('test2');
return ()=>{
console.log('test2清理函数');
}
}
export default function App() {
const [, forceUpdate] = useState({})
useEffect(n % 2 === 0 ? test1 : test2);
n++
return <div>
<button onClick={()=>{
forceUpdate({});
}}>强制刷新</button>
</div>
}
自定义HOOK
- 自定义HOOK以use开头命名
- 调用自定义函数应该放到顶层,不要放在判断循环中
使用场景
-
很多组件都需要在第一次加载完成后,获取所有学生数据 -》 myHOOK
-
很多组件都需要在第一次加载完成后,启动一个计时器,然后在组件销毁时卸载 -》 myHOOK
// useGetAllStudents.js 第一次加载完成后,获取所有学生数据
import {useState,useEffect} from 'react'
import {getAllStudents} from '../../services/student'
export default function useGetAllStudents() {
const [students, setStudents] = useState([])
useEffect(() => {
(async function(){
const stu = await getAllStudents();
console.log(stu);
setStudents(stu.data)
})()
}, [])
return students;
}
// App.js
import React from 'react'
import useGetAllStudents from './components/myHOOK/useGetAllStudents'
function Test() {
const stu = useGetAllStudents();
const list = stu.map(item => <li key={item.id}>{item.address} -- {item.name}</li>)
return <ul>
{list}
</ul>
}
export default function App() {
return (
<div>
<Test></Test>
</div>
)
}