Hook函数
Hook函数也叫做钩子函数
# hook 函数和普通函数有什么区别
区别就在于函数名,函数名是以 use开头后面链接 首字母大写的,react就认为这个函数是hook函数
# 例如
usePosition hook
App Father 函数组件
fn save 普通函数
# hook函数使用原则
hook函数不能在类组件中使用,只能在函数组件中使用。
hook函数不能在普通函数中使用,只能在其他hook函数中使用。
hook函数数量必须是确定的。例如不能写在 if语句 或 for循环中
Hook函数使用案例
import React, { useState } from 'react'
export default function App() {
// Hook函数不能定义在普通函数中,报错
// function save(){
// let [count, setCount] = useState(100)
// }
// useSave 是自定义的hook函数,内部可以使用 其他hook
function useSave() {
let [count, setCount] = useState(100)
console.log(count)
}
// Hook函数不确定,报错
// if(true){
// let [msg, setMsg] = useState('atguigu')
// }
// Hook函数不确定,报错
// for (let i = 0; i < 100; i++) {
// let [msg, setMsg] = useState('atguigu')
// }
return (
<div>
<button onClick={useSave}>点击</button>
</div>
)
}
自定义Hook
使用自定义hook封装函数,可以简洁代码,抽离出不同组件中重复的代码。
hook函数封装:
主组件:src->App.jsx
import React from 'react'
import Mea from './components/Mea'
import Aqua from './components/Aqua'
export default function App() {
return (
<div>
<Mea />
<Aqua />
</div>
)
}
子组件:src->components->Mea.jsx
import React, { useState } from 'react'
import { useEffect } from 'react'
export default function Mea() {
let [x, setX] = useState(110)
let [y, setY] = useState(110)
// componentDidMount
useEffect(() => {
function move(e) {
setX(e.clientX)
setY(e.clientY)
}
window.addEventListener('mousemove', move)
// componentWillUnmount
return () => {
window.removeEventListener('mousemove', move)
}
}, [])
return (
<div style={{ position: 'absolute', left: x, top: y, width: 100, height: 100, border: '2px solid green',backgroundColor:'pink' }}>
Mea
</div>
)
}
子组件:src->components->Aqua.jsx
import React, { useState } from 'react'
import { useEffect } from 'react'
export default function Aqua() {
let [x, setX] = useState(0)
let [y, setY] = useState(0)
// componentDidMount
useEffect(() => {
function move(e) {
setX(e.clientX-110)
setY(e.clientY-110)
}
window.addEventListener('mousemove', move)
// componentWillUnmount
return () => {
window.removeEventListener('mousemove', move)
}
}, [])
return (
<div style={{ position: 'absolute', left: x, top: y,width: 100, height: 100, border: '2px solid green', backgroundColor: 'aqua' }}>
Aqua
</div>
)
}
使用Hook函数封装优化代码
定义一个Hook函数:src->Hook->usePosition.js
import { useEffect,useState } from 'react'
function usePosition(){
// 设置初始值
let [x, setX] = useState(110)
let [y, setY] = useState(110)
// componentDidMount
useEffect(() => {
function move(e) {
setX(e.clientX)
setY(e.clientY)
}
window.addEventListener('mousemove', move)
// componentWillUnmount
return () => {
window.removeEventListener('mousemove', move)
}
}, [])
return{
x,
y
}
}
export default usePosition
子组件:src->components->Mea.jsx
import React from 'react'
import usePosition from '../hook/usePosition'
export default function Mea() {
let { x, y } = usePosition()
return (
<div style={{ position: 'absolute', left: x, top: y, width: 100, height: 100, border: '2px solid green',backgroundColor:'pink' }}>
Mea
</div>
)
}
子组件:src->components->Aqua.jsx
import React from 'react'
import usePosition from '../hook/usePosition'
export default function Aqua() {
let{x,y} = usePosition()
x -= 110
y -= 110
return (
<div style={{ position: 'absolute', left: x, top: y,width: 100, height: 100, border: '2px solid green', backgroundColor: 'aqua' }}>
Aqua
</div>
)
}
组件通信
props通信
父组件通过给子组件标签上设置属性,然后子组件通过this.props接收数据实现父向子传递参数;
子组件通过父组件传递一个函数,子组件通过调用方法,并把实参传递过去,从而实现子向父传递参数;
这种通信的实现缺点是:不能隔代传递参数,同时兄弟之间传递参数需要父亲作为中转。
context通信
useContext
✨在接收context传递数据的组件中,使用useContext快速拿到数据
(爷-父-孙)
通过 context组件 和 useContext Hook函数实现组件通信
作用:祖先组件向任意后代组件通信
✨使用方法
1. 创建context实例: const context = React.createContext()
2. 使用 context.Provider组件 包裹后代组件 ,并绑定value数据,用于传递参数
3. 后代组件获取数据
1. 导入context实例
2. 使用 useContext 函数,解析context对象获取数据
🎨使用案例
创建content对象:src->context->context.js
// 1 导入createContext 函数
import {createContext} from 'react'
// 2 创建context对象
const context = createContext();
// 3. 将context对象暴露出去
export default context;
主组件:src->App.jsx
import React, { useState } from 'react'
// 4. 导入context对象
import context from './content/context.js'
import Child from './components/Child'
export default function App() {
let [user,setUser] = useState({
username: 'atguigu',
age: 10
})
function changeUserName(age){
const newUser = {
username: 'Hanser',
age,
}
setUser(newUser)
}
return (
// 5. 使用Provider组件包裹 后代组件,被包裹的后代组件及其所有的子组件,都可以直接使用 value中的数据
// value中传递一个对象,对象中可以同时传递多个属性。
<context.Provider value={{user,changeUserName}}>
<div>
<h3>App</h3>
<hr />
<Child />
</div>
</context.Provider>
)
}
子组件:src->Child.jsx
import React from 'react'
import Grandson from './Grandson'
import context from '../content/content'
import { useContext } from 'react'
export default function Child() {
// 解构content实例
let { user } = useContext(context)
// 渲染数据
return (
<div>
<h4>Child</h4>
<p>Child username: {user.username}</p>
<p>Child age: {user.age}</p>
<hr />
<Grandson />
</div>
)
}
孙子组件:src->grandson.jsx
import React, { useState } from 'react'
import context from '../content/content'
import { useContext } from 'react'
export default function Grandson() {
// 解构content实例
let { user, changeUserName } = useContext(context)
// 定义状态
let [age,setAge] = useState(20)
return (
<div>
<h4>Grandson</h4>
<p>Grandson username: {user.username}</p>
<p>Grandson age: {user.age}</p>
<p><button onClick={()=>changeUserName(45)}>更改年龄</button></p>
</div>
)
}
pubsub通信
pub:publish 发布
sub:subscribe 订阅
# 发布消息
PubSub.publish('消息名', 数据)
# 消息的订阅,消息更新触发回调函数,发布的消息数据就会出现在回调函数的data参数中
let 消息id = PubSub.subscribe('消息名', function(msg消息名, data数据){ })
# 取消订阅
PubSub.unsubscribe(消息id) ==> 取消该消息id 的订阅
PubSub.unsubscribe(消息名) ==> 取消该消息名所有的订阅
# 取消所有的消息
PubSub.clearAllSubscriptions() 取消所有的消息
使用方式:
1. 在组件生命周期挂载阶段 -> 订阅消息
2. 在组件生命周期即将卸载阶段 -> 取消订阅
# 安装pubsub
npm i pubsub-js
# 导入PubSub对象
import PubSub from 'pubsub-js'
pubsub使用案例(吴亦凡蔡徐坤)
主组件:src->App.jsx
import React, { useState } from 'react'
import { useEffect } from 'react'
// 1. 导入PubSub
import PubSub from 'pubsub-js'
import Fan from './components/Fan'
import Kun from './components/Kun'
export default function App() {
let [user,setUser] = useState({
username: 'atguigu',
age: 10
})
function changeUserName(age){
const newUser = {
username: 'Hanser',
age,
}
setUser(newUser)
}
useEffect(() => { // componentDidMount
// 订阅Fan消息
let fanId = PubSub.subscribe('fan', (msg, data) => {
console.log('App data: ', data);
})
// 订阅Kun消息
let kunId = PubSub.subscribe('kun', (msg, data) => {
console.log('App data: ', data);
})
return () => { // componentWillUnmount
PubSub.unsubscribe(fanId);// 取消订阅fan的消息
PubSub.unsubscribe(kunId);
}
}, [])
return (
<div>
<h3>App</h3>
<p><button onClick={() => {
PubSub.unsubscribe('fan');// 全网封杀fan消息
}}>取消订阅_fan</button></p>
<p><button onClick={() => {
PubSub.clearAllSubscriptions();
}}>封杀所有艺人</button></p>
<hr />
<Fan />
<hr />
<Kun />
</div>
)
}
子组件:src->components->Fan.jsx
import React, { useState } from 'react'
import PubSub from 'pubsub-js'
export default function Fan() {
let [song, setSong] = useState("大碗宽面")
return (
<div>
<h3>Fan</h3>
<button onClick={() => {
PubSub.publish('fan', song);
}}>发布消息</button>
</div>
)
}
子组件:src->components->Kun.jsx
import React from 'react'
import PubSub from 'pubsub-js'
import { useEffect } from 'react'
export default function Kun() {
return (
<div>
<h3>Kun</h3>
<button onClick={() => {
PubSub.publish('kun', '鸡你太美');
}}>发布消息</button>
</div>
)
}
todoList案例:使用pubsub传递数据
# Pubsub使用过程会产生闭包,使用setState()更改数据时,需要使用第二种方式,把缓存空间中的参数传递进去
const A = PubSub.subscribe('addTodo', B)
const B = function(){C}
const C = setTodos(D)
const D = (todos) => {return [todo, ...todos]}
A,B,C都是一个函数,A中嵌套B,B中嵌套C
C中的作用域中可以使用B中的和C中的作用域中的变量(上层作用域)
C中的todos变量是 首次组件挂载 时,todos中数据
当数据状态更新,代码重新调用,浏览器重新开辟一个执行栈,并从useState的缓存空间中获取最新的数据状态渲染。
D函数就是一个闭包,React设计时,这种方式获取的状态不是实时的,而是从它被创建的那次渲染中被「看到」的
于是需要使用一个参数接收缓存空间中的最新数据,把缓存空间中的数据赋值。
// 使用第二种写法:把缓存空间中的todos存放,原因由于产生了闭包
原来
setTodos([todo,...todos])
改写成
setTodos(todos=>{
return [todo,...todos]
})
原来
setTodos(todos.filter(todo=>!todo.isDone)
改写成
setTodos(todos => todos.filter(todo=>!todo.isDone))
其他Hook函数
useCallback
作用:缓存一个函数,避免组件更新后,函数被重复创建
优化函数组件中堆内存重复创建问题
语法: useCallback(()=>{}, [])
参数:参数是一个回调函数和一个空数组,回调函数执行逻辑代码
用法如同 useEffect(),回调函数中的第二个参数空数组代表只在挂载的时候执行一次,更新时不执行。
更新组件时,函数被重新创建的图示:如下图:App函数更新调用时, addCount会被重复创建。
App函数组件的执行过程:
App函数是一个函数,是一个引用类型,
1. 在window作用域中定义一个函数组件App,数据类型是引用数据类型,浏览器会开辟一个堆内存xxxfff111;
2. 堆内存中会存放着App函数代码(以字符串的形式存放),并产生一个内存地址xxxfff111;
3. App函数名对应的是一个内存地址:xxxfff111,指向堆内存
4. 第一次运行App函数组件时,内存会开辟一个调用执行栈的内存空间,会把App函数代码从堆内存中取出来按顺序执行
5. 当执行useState(88)时,useState Hook函数会开辟一个缓存空间,把88存入,
6. 同时setCount也是一个函数,也会开辟一个堆内存存放setCount的代码
7. 执行到 const addCount...时这也是一个函数,也会开辟一个堆内存存放addCount的代码
8. 当触发了数据更新,数据发生改变,App组件重新渲染,默认情况下会重新开辟空间放置一样的堆内存代码,造成了性能浪费
9. 于是通过useCallback Hook函数缓存函数,这样每次重新渲染,react就可以从缓存中读取代码不需要重新开辟空间存放。
10. 但是需要注意 useCallback(()=>{}) 这种形式产生了闭包,结合react的特性,永远只能读取到初始值
11. 所以需要使用useState的第二种修改数据方法:通过回调函数的形式;
import React, { useCallback, useState } from 'react'
export default function App() {
let [count, setCount] = useState(88)
// 未优化前,addCount 在每一个 App() 调用执行栈中都有一份
// const addCount = (num) => {
// setCount(count + num);
// }
/**
* useCallback
* 作用: 可以缓存一个函数
*/
const addCount = useCallback((num)=>{
// setCount(count + num); // 永远只能读取到初始值,所以需要使用如下回调函数的写法
setCount(count=>{
return count + num;
})
}, [])
return (
<div>
<p>count : {count}</p>
<p><button onClick={() => addCount(3)}>count +</button></p>
</div>
)
}
React.memo
类似于类的纯组件,优化当函数组件的 state 和 props没有改变的时候,不会触发重新渲染
语法:React.memo(函数组件)
使用案例
主组件:src->App.jsx
import React, { Component,PureComponent } from 'react'
import Hanser from './components/Hanser';
export default class App extends PureComponent {
state = {
count: 88
}
render() {
console.log('App render');
let { count } = this.state;
return (
<div>
<h3>App</h3>
<p>App state count: {count}</p>
<p><button onClick={() => {
this.setState({
count: count + 1
})
}}>count ++</button></p>
<p><button onClick={() => {
this.setState({
count: 99
})
}}>count == 99</button></p>
<hr />
<Hanser count={count} />
</div>
)
}
}
子组件:src->components->Hanser.jsx
import React, { useState } from 'react'
/**
* 函数组件:state已经做过优化了, 当状态不变时,只多渲染一次
* props没有优化,外部数据不改变的时候,也会重新render
*
* React.memo: 作用:函数组件 state 和 props 不发生改变的时候,不会触发重新render
*/
function Hanser({ count }) {
console.log('Hanser render');
let [money, setMoney] = useState(88)
return (
<div>
<h3>Hanser</h3>
<p>Hanser state money: {money}</p>
<p>Hanser props count: {count}</p>
<p><button onClick={() => {
setMoney(money + 100)
}}>money++</button></p>
<p><button onClick={() => {
setMoney(10000)
}}>money == 10000</button></p>
</div>
)
}
export default React.memo(Hanser)
useMemo
作用:缓存一个函数计算的结果,优化函数组件中重新渲染时无关方法重复执行问题
语法: useMemo(逻辑代码,[监听的值])
使用方法如同 useEffect() Hook函数 update+mounted
使用案例
App.jsx
import React from 'react'
import WithMemo from './components/WithMemo'
import WithoutMemo from './components/WithoutMemo'
export default function App() {
return (
<div>
<h3>without memo</h3>
<WithoutMemo/>
<h3>With memo</h3>
<WithMemo/>
</div>
)
}
WithMemo.jsx
import { useMemo, useState } from "react";
/**
* useMemo 缓存一个函数计算的结果
* @returns
*/
export default function WithMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
const expensive = useMemo(() => {
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}, [count]);
return <div>
<h4>{count}-{expensive}</h4>
{val}
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)} />
</div>
</div>;
}
WithoutMemo.jsx
// 使用前
import React, { useState } from 'react';
export default function WithoutMemo() {
const [count, setCount] = useState(1);
const [val, setValue] = useState('');
function expensive() { // expensive 昂贵的
console.log('compute');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}
return <div>
<h4>{count}-{val}-{expensive()}</h4>
<div>
<button onClick={() => setCount(count + 1)}>+c1</button>
<input value={val} onChange={event => setValue(event.target.value)} />
</div>
</div>;
}
useReducer
作用:组件中有大量数据,使用useState,略显繁琐,使用useReducer更方便的管理数据
语法:类似于简化版的redux
参数:
第一个参数是 reducer函数,
第二个参数是数据 initalState
返回值:返回一个数组,从数组中解构出 数据状态和dispatch函数,通过调用dispatch函数来执行reducer中的方法。
使用方法如同 redux
let [state,dispatch] = useReducer(reducer, initalState) //arr对象为 [{…}, ƒ]
定义reducer函数
function reducer(state,action){
switch(action.type){
case '方法名':{
return{
...state,
num:state.num + action.payload
}
}
...
}
}
调用dispatch(),参数是一个action对象(需要手动定义,区别于redux,redux自动创建)
dispatch({type:'方法名',payload:3})
使用示例:
src->App.jsx
import React from 'react'
import { useReducer } from 'react'
const initalState = {
num: 88,
msg: 'atguigu'
}
function reducer(state, action) {
switch (action.type) {
case 'addNum':
return {
...state,
num: state.num + action.payload
}
case 'decNum':
return {
...state,
num: state.num - action.payload
}
}
}
export default function App() {
let [state, dispatch] = useReducer(reducer, initalState)
console.log('state: ', state)
let { num, msg } = state;
return (
<div>
<p>state num: {num}</p>
<p>state msg: {msg}</p>
<p><button onClick={() => {
dispatch({ type: 'addNum', payload: 3 })
}}>num++</button></p>
<p><button onClick={() => {
dispatch({ type: 'decNum', payload: 5 })
}}>num--</button></p>
</div>
)
}
useDebugValue
可用于在 React 开发者工具中显示 自定义 hook 的标签
注意:只能在自定义hook中使用
参数:useDebugValue(数据状态)
返回值:undefined
使用示例:
src->hook->useFriendStatus.js(自定义hook)
import { useState, useDebugValue } from 'react'
// 创建一个方法:在最小值和最大值之间取随机整数
function getRandomIntInclusive(min, max) {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min //含最大值,含最小值
}
// 定义一个自定义hook函数
export default function useFriendStatus() {
// 定义一个数据状态
const [isOnline, setIsOnline] = useState(null)
// 设置一个定时器,每秒会随机变化为online和offline
setTimeout(() => {
let result = getRandomIntInclusive(0, 1)// [0 | 1]
console.log(result)
setIsOnline(result ? 'online' : 'offline')
}, 1000)
// 在react开发者工具中的这个 Hook 旁边显示标签
// "FriendStatus: Online"
// 调用useDebugValue(数据状态)
useDebugValue(isOnline)
return isOnline
}
入口文件:src->App.js
import React from 'react'
import useFriendStatus from './hook/useFriendStatus'
export default function App() {
useFriendStatus()
return (
<div>App</div>
)
}
useImpretiveHandle
作用:封装公共组件的时候,可以给使用公共组件的组件提供指定的API. 有条件的操作公共组件的真实dom
useImperativeHandle
可以让你在使用ref
时自定义暴露给父组件的实例值.useImperativeHandle
应当与 [forwardRef
]一起使用
函数组件本身不能绑定ref,会报错,但是配合React.forwardRef就可以绑定ref属性了
可以实现在父组件上操作子组件中的dom元素
我们希望父组件的ref,只能有限的操作子组件的真实dom,那就需要能够在子组件中自定义父组件ref的current属性,实现有限的功能===>useImpretiveHandle
使用示例
App.jsx
import React ,{useRef} from 'react'
import FunTest from './components/FunTest';
export default function App() {
const funRef = useRef('我是谁?');
return (
<div>
<h3>函数组件 绑定ref</h3>
<FunTest ref={funRef} />
<hr />
<button onClick={() => {
// 操作子组件中的dom
// funRef.current.style.color='red'
// funRef.current.style.fontSize = '30px'
funRef.current.changeColor();
funRef.current.changeFontSize();
}}>获取ref</button>
</div>
)
}
src->component->FunTest.jsx
import React, { useRef } from 'react'
import { useImperativeHandle } from 'react';
// 通过函数组件中的第二个参数接收父组件传过来的 ref属性
function FunTest(props, ref) {
console.log('ref:', ref); // 父组件传过来的 ref对象
const myselfRef = useRef();
// 设置父组件ref的权限,参数是 父组件ref和回调函数
// 回调函数中的返回值是一个对象,对象中的方法就是给父组件ref使用的方法
useImperativeHandle(ref, () => ({
changeColor() {
// 使用自己的ref对象绑定元素
myselfRef.current.style.color = 'green';
},
changeFontSize() {
myselfRef.current.style.fontSize = '30px';
}
}))
return (
// 未使用useImperativeHandle,父组件会完全控制子组件
// <div ref={ref}>FunTest</div>
// 使用useImperativeHandle,父组件只能操作useImperativeHandle中回调函数返回的对象中的方法
<div ref={myselfRef}>FunTest</div>
)
}
// 通过forwardRef()函数 让可以在函数组件上设置ref属性
export default React.forwardRef(FunTest)
useLayoutEffect
作用与
useEffect
相同,但它会在所有的 DOM 变更之后同步调用 effect。作用:可以使用它来读取 DOM 布局并同步触发重渲染。
在浏览器执行绘制之前,
useLayoutEffect
内部的更新计划将被同步刷新。
useEffect
这个是在render结束后,你的callback函数执行,但是不会阻塞浏览器渲染
useLayoutEffect
这个是用在处理DOM的时候,当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制
使用案例:
src->component->Animate.jsx
import React, { useEffect, useLayoutEffect, useRef } from 'react'
import TweenMax from 'gsap' // npm i gsap@3.7.0
import './index.css'
const Animate = () => {
const REl = useRef(null)
useLayoutEffect(() => {
/*下面这段代码的意思是当组件加载完成后,在0秒的时间内,将方块的横坐标位置移到600px的位置*/
TweenMax.to(REl.current, 0, { x: 600 })
}, [])
return (
<div className="animate">
<div ref={REl} className="square">
square
</div>
</div>
)
}
export default Animate
src->component->index.css
.square {
width: 100px;
height: 100px;
background-color: aqua;
}
src->App.jsx
import React from 'react'
import Animate from './components/Animate'
export default function App() {
return (
<div>
<Animate />
</div>
)
}