什么是Hooks
ref引用值
普通变量的改变一般是不好触发函数组件的渲染的,如果想让一般的数据也可以得到状态的保存,可以使用ref
import { useState ,useRef} from 'react'
function App() {
const [count, setCount] = useState(0)
let num = useRef(0)
const handleClick = () => {
setCount(count + 1)
num.current++ //current,当前值
console.log(num.current)
}
return (
<div>
<button onClick={handleClick}>计数</button>
</div>
)
}
export default App
那你就要问了:这和useState有什么区别?
更改时不会触发渲染,例如这里,我们注释掉setCount的语句:
import { useState ,useRef} from 'react'
function App() {
const [count, setCount] = useState(0)
let num = useRef(0)
const handleClick = () => {
// setCount(count + 1)
num.current++ //current,当前值
console.log(num.current)
}
return (
<div>
<button onClick={handleClick}>计数</button>
{count},{num.current}
</div>
)
}
export default App
可以看出,num的值变了,但是并没有渲染出来
所以我们一般不会把ref写在jsx的结构里,但是可以在渲染过程之外随意修改和更新current的值,不需要像useState一样使用里面的方法修改的值才有效
如果我们在里面开启定时器,在这里一秒触发一次,如果单击按钮,num的值一秒增加一次,如果点击按钮多次,就会同时开启多个定时器,数值就会涨的飞快
import { set } from 'lodash'
import { useState ,useRef} from 'react'
function App() {
const [count, setCount] = useState(0)
let timer=null
let num = useRef(0)
const handleClick = () => {
setCount(count + 1)
setInterval(() => {
console.log(123)
}, 1000);
}
return (
<div>
<button onClick={handleClick}>计数</button>
{count},{num.current}
</div>
)
}
export default App
一般的解决办法是每次新开一个定时器,就关掉旧定时器
import { set } from 'lodash'
import { useState ,useRef} from 'react'
function App() {
const [count, setCount] = useState(0)
let timer=null
const handleClick = () => {
clearInterval(timer)
timer=setInterval(() => {
console.log(123)
}, 1000);
}
return (
<div>
<button onClick={handleClick}>计数</button>
{count}
</div>
)
}
export default App
但是如果对整个函数重新调用(也就是启用useState)就无法销毁定时器了
因为整个函数重新调用,定时器是在上一次调用产生的,这一次删不掉上一次的定时器,作用域不同
import { set } from 'lodash'
import { useState ,useRef} from 'react'
function App() {
const [count, setCount] = useState(0)
let timer=null
const handleClick = () => {
setCount(count + 1)
clearInterval(timer)
timer=setInterval(() => {
console.log(123)
}, 1000);
}
return (
<div>
<button onClick={handleClick}>计数</button>
{count}
</div>
)
}
export default App
这时候就需要对timer做记忆,也就是使用ref,就算走了多次渲染函数,也可以销毁
import { set } from 'lodash'
import { useState, useRef } from 'react'
function App() {
const [count, setCount] = useState(0)
let timer = useRef(null)
const handleClick = () => {
setCount(count + 1)
clearInterval(timer.current)
timer.current = setInterval(() => {
console.log(123)
}, 1000)
}
return (
<div>
<button onClick={handleClick}>计数</button>
{count}
</div>
)
}
export default App
试了试,如果timer放全局定义是可以的,但是感觉这样不利于函数的封装性
import { useState, useRef } from 'react'
let timer = null
function App() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
clearInterval(timer)
timer = setInterval(() => {
console.log(123)
}, 1000)
}
return (
<div>
<button onClick={handleClick}>计数</button>
{count}
</div>
)
}
export default App
通过ref操作原生dom
react里可以使用js的方法操作dom,例如什么querySelector这种css选择器,但是更推荐我们使用React的方法操作dom
import { set } from 'lodash'
import { useState, useRef } from 'react'
function App() {
const myRef = useRef(null)
const handleClick = () => {
//通过ref操作原生dom
console.log(myRef.current.innerHTML)
myRef.current.style.background = 'red'
}
return (
<div>
<button onClick={handleClick}>计数</button>
<div ref={myRef}>hello React</div>
</div>
)
}
export default App
这么写是会报错的,钩子是按顺序使用的,在循环里这么写会报错,所以这么写不符合规范;而且也不建议把const myRef=useRef(null)写在结构里
import { set } from 'lodash'
import { useState, useRef } from 'react'
function App() {
const list = [
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
]
const handleClick = () => {
//通过ref操作原生dom
console.log(myRef.current.innerHTML)
myRef.current.style.background = 'red'
}
return (
<div>
{list.map((item) => {
const myRef = useRef(null)
return <li key={item.id} ref={myRef}>{item.text}</li>
})}
</div>
)
}
export default App
在循环操作里ref可以使用回调函数的写法:
import { useState, useRef } from 'react'
function App() {
const list = [
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
]
return (
<div>
<ul>
{list.map((item) => {
return (
<li
key={item.id}
ref={(myRef) => {
myRef.style.background = 'red'
}}
>
{item.text}
</li>
)
})}
</ul>
</div>
)
}
export default App
但是我报了很多错,说是
其实就算style在卸载的时候为空,加个逻辑判断就好了
铭记励志轩
import { useState, useRef } from 'react'
function App() {
const list = [
{ id: 1, text: 'aaa' },
{ id: 2, text: 'bbb' },
{ id: 3, text: 'ccc' },
]
return (
<div>
<ul>
{list.map((item) => {
return (
<li
key={item.id}
ref={(myRef) => {
if (myRef) {
myRef.style.background = 'red'
}
}}
>
{item.text}
</li>
)
})}
</ul>
</div>
)
}
export default App
组件设置ref需要forwardRef进行转发
forwardRef
是 React 提供的一个高阶函数,用于将 ref
从父组件传递到子组件中的 DOM 元素或组件实例。它主要用于解决在函数组件中直接使用 ref
时无法访问子组件内部 DOM 元素的问题。
在 MyInput
组件中,ref
被绑定到 <input>
元素,也就是把ref转发到内部的input身上,然后=在 handleClick
函数中,ref.current
用于直接操作 <input>
元素:
import { useRef ,forwardRef} from 'react'
const MyInput = forwardRef(function MyInput(props, ref) {
return (
<input type='text' ref={ref}></input>
)
})
function App() {
const ref = useRef(null)
const handleClick = () => {
ref.current.focus()
ref.current.style.background='red'
}
return (
<div>
<button onClick={handleClick}>点我</button>
<MyInput ref={ref} />
</div>
)
}
export default App
useImperativeHandle自定义ref
你想要它其中两个方法:
focus
和scrollIntoView
。为此,用单独额外的 ref 来指向真实的浏览器 DOM。然后使用useImperativeHandle
来暴露一个句柄,它只返回你想要父组件去调用的方法
import { useRef ,forwardRef,useImperativeHandle} from 'react'
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef=useRef(null)
useImperativeHandle(ref,()=>{
return{
focus(){
inputRef.current.focus()
}
}
})
return (
<input type='text' ref={inputRef}></input>
)
})
function App() {
const ref = useRef(null)
const handleClick = () => {
ref.current.focus()
}
return (
<div>
<button onClick={handleClick}>点我</button>
<MyInput ref={ref} />
</div>
)
}
export default App
点击按钮获取焦点
然后你就要问了(因为我也想问):这个获取焦点的操作为什么要写成这样?
这样会更加灵活,不用完全写在dom里,可以把你想要写的功能放在useImperativeHandle的第二个参数里,也就是里面的回调函数,来实现你自己的功能、自定义的功能
相当于你自己写了一个组件,可以像普通的组件使用,在里面添加属性和属性值,你也可以写自己的组件实现的功能
例如这个button的属性是他自己自带的,我们写的组件也可以写一些自己带的属性,focus
和 scrollIntoView
就是这个作用
<button onClick={handleClick}>点我</button>
比如我们再添加一个获取焦点以后背景变红色的功能:
import { useRef ,forwardRef,useImperativeHandle} from 'react'
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef=useRef(null)
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus()
},
focusAndStyle() {
inputRef.current.focus()
inputRef.current.style.background = 'red'
}
}
})
return (
<input type='text' ref={inputRef}></input>
)
})
function App() {
const ref = useRef(null)
const handleClick = () => {
if (ref) {
ref.current.focusAndStyle()
}
}
return (
<div>
<button onClick={handleClick}>点我</button>
<MyInput ref={ref} />
</div>
)
}
export default App
纯函数如何处理副作用
纯函数之前提过,只关注输入和输出、两次执行函数是否结果一样axios
上面图的意思是:组件是纯函数,但是有时候组件不得不写一些违背纯函数的功能,例如Ajax调用、还有我们用ref操作dom等都是副作用
事件本身可以去除副作用
import { useRef, forwardRef, useImperativeHandle } from 'react'
function App() {
const ref = useRef(null)
//副作用,不符合纯函数的规范
// setTimeout(() => {
// ref.current.focus()
// },1000)
//副作用,但是符合纯函数的规范,因为事件可以处理副作用
const handleClick = () => {
ref.current.focus()
}
return (
<div>
<button onClick={handleClick}>点击</button>
<input type='text' ref={ref} />
</div>
)
}
export default App
但不是所有的副作用都可以用事件去除,而且有时候需要初始化副作用
所以在react里时间操作可以处理副作用,比如useEffect钩子
import { useRef, forwardRef, useImperativeHandle, useEffect } from 'react'
function App() {
const ref = useRef(null)
//副作用,不符合纯函数的规范
// setTimeout(() => {
// ref.current.focus()
// },1000)
//副作用,但是符合纯函数的规范,因为事件可以处理副作用
// const handleClick = () => {
// ref.current.focus()
// }
//可以在初始的时候进行副作用操作
//useEffect触发的时机是jsx渲染后触发的,这样就会消除副作用的影响
useEffect(() => {
if (ref) {
ref.current.focus()
}
})
return (
<div>
<button onClick={handleClick}>点我</button>
<input type='text' ref={ref} />
</div>
)
}
export default App
一刷新就自动获取焦点
初次渲染和更新渲染,都会触发useEffect(),因为每次渲染jsx以后,会触发useEffect(),整个当前函数组件作用域的最后时机触发的
import {useState,useEffect} from 'react'
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log(count)
})
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>点我</button>
</div>
)
}
export default App
执行顺序:
组件首次渲染(调用 useState(0)
,初始化 count
为 0,渲染组件返回jsx
)->点击按钮(触发handleClick函数调用useCount)->重新渲染组件->执行useEffect中的副作用函数
useEffect的依赖项使用
一个组件里可能有多个副作用,原则上来说多个副作用可以放在同一个useEffect里,但是比较杂
可以使用useEffect的依赖项进行拆解,因为useEffect本身就是个函数
我们可以使用多个useEffect来控制副作用
import {useState,useEffect} from 'react'
function App() {
const [count, setCount] = useState(0)
const [msg, setMsg] = useState('hello React')
useEffect(() => {
console.log(msg)
})
useEffect(() => {
console.log(count)
})
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>点我</button>
</div>
)
}
export default App
两个根一个一样,也可以正常的更新、渲染
但是有时候根据不同的应用场景,有些副作用需要重新触发,有的不需要,可以指定哪些可以触发、哪些不可以。
添加useEffect的依赖项目
import {useState,useEffect} from 'react'
function App() {
const [count, setCount] = useState(0)
const [msg, setMsg] = useState('hello React')
useEffect(() => {
console.log(msg)
},[msg])
useEffect(() => {
console.log(count)
},[count])//这就是依赖项
const handleClick = () => {
setCount(count + 1)
}
return (
<div>
<button onClick={handleClick}>点我</button>
</div>
)
}
export default App
初始都触发,更新以后,只有对应依赖项发生改变时才触发,像这里msg没改变所以不触发
内部是怎么判别有没有发生变化的?是通过Object.is()方法来判定是否改变
像这样,依赖项是静态的空数组,只有初始会触发,以后发生改变都不会渲染他
不过尽量不要这么写,尽量还是要把里面用到的状态写在依赖项里
除了状态变量,还有props、计算变量等也可以写在依赖项里
计算变量:是指通过其他变量或数据动态计算得出的值,而不是直接存储的静态值。计算变量通常用于根据某些条件或逻辑动态生成数据,而不是手动维护这些数据。
函数也可以做依赖项
函数也可能成为计算变量,所以也可以作为依赖项
但是会出现一个问题,Object.is()方法在判别函数的时候会看引用类型的地址,两个函数是两个函数,内存地址不一样Object.is()就会判别为false
useCallBack可以修复这个问题,可以缓存函数,一般用于组件性能优化,其他情况不推荐使用,这里先不细说
最好的解决办法是把函数定义在useEffect内部
import {useState,useEffect} from 'react'
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
const foo = () => {
console.log(count)
}
foo()
},[count])//这就是依赖项
return (
<div>
</div>
)
}
export default App
useEffect清理操作
先写一个简易的关闭聊天室的功能
import {useState,useEffect} from 'react'
function Chat() {
return (
<div>
我是聊天室
</div>
)
}
function App() {
const [show,setShow] = useState(true)
const handleClick = () => {
setShow(false)
}
return (
<div>
<button onClick={handleClick}>点我关闭聊天室</button>
{show&&<Chat/>}
</div>
)
}
export default App
useEffect可以在卸载组件的时候清理
import {useState,useEffect} from 'react'
function Chat() {
useEffect(() => {
console.log('进入聊天室')
//useEffect返回一个函数,这个函数会在组件卸载的时候执行
return () => {
console.log('离开聊天室')
}
})
return (
<div>
我是聊天室
</div>
)
}
function App() {
const [show,setShow] = useState(true)
const handleClick = () => {
setShow(false)
}
return (
<div>
<button onClick={handleClick}>点我关闭聊天室</button>
{show&&<Chat/>}
</div>
)
}
export default App
增加了useEffect更新时的清理功能,清理功能是整个作用域的结束,而不是下一次作用域的开始
import {useState,useEffect} from 'react'
function Chat({title}) {
useEffect(() => {
console.log('进入',title)
//useEffect返回一个函数,这个函数会在组件卸载的时候执行
return () => {
console.log('离开',title)
}
},[title])
return (
<div>
我是课堂
</div>
)
}
function App() {
const [show, setShow] = useState(true)
const [title,setTitle]=useState('电磁场与电磁波')
const handleClick = () => {
setShow(false)
}
const handleChange = (e) => {
setTitle(e.target.value)
}
return (
<div>
<button onClick={handleClick}>点我退出课堂</button>
<select value={title} onChange={handleChange}>
<option value="电磁场与电磁波">电磁场与电磁波</option>
<option value="半导体物理">半导体物理</option>
</select>
{show && <Chat title={title} />}
</div>
)
}
export default App
清理功能是很常见的需求,如果注释掉清理的代码就会变成这样:
没有退出只有进入,如果是严格模式其实会把函数执行两边,可以看到👇
实际上是违背正常逻辑的,如果加上清理功能,在严格模式执行的现象应该是
也就是说严格模式也可以起到提醒你需要自检的作用
再举一个栗子,如果我们要切换不同的课程,切换不同的课程耗时不同
import {useState,useEffect} from 'react'
function fetchChat(title) {
const delay = title==='电磁场与电磁波'?2000:1000
return new Promise((resolve,reject)=>{
setTimeout(() => {
resolve([
{id:1,text:title+'1'},
{id:2,text:title+'2'},
{id:3,text:title+'3'}
])
}, delay);
})
}
function Chat({ title }) {
const [list,setList]=useState([])
useEffect(() => {
fetchChat(title).then((data) => {
setList(data)
})
return () => {
}
},[title])
return (
<div>
{list.map((item)=>{
return <li key={item.id}>{ item.text}</li>
})}
</div>
)
}
function App() {
const [show, setShow] = useState(true)
const [title,setTitle]=useState('电磁场与电磁波')
const handleClick = () => {
setShow(false)
}
const handleChange = (e) => {
setTitle(e.target.value)
}
return (
<div>
<button onClick={handleClick}>点我退出课堂</button>
<select value={title} onChange={handleChange}>
<option value="电磁场与电磁波">电磁场与电磁波</option>
<option value="半导体物理">半导体物理</option>
</select>
{show && <Chat title={title} />}
</div>
)
}
export default App
如果不添加useEffect,二者耗时不同,就会出现上一个还在加载,下一个作用域已经执行的问题
变成这样
加入清理,就可以解决问题
import {useState,useEffect} from 'react'
function fetchChat(title) {
const delay = title==='电磁场与电磁波'?2000:1000
return new Promise((resolve,reject)=>{
setTimeout(() => {
resolve([
{id:1,text:title+'1'},
{id:2,text:title+'2'},
{id:3,text:title+'3'}
])
}, delay);
})
}
function Chat({ title }) {
const [list, setList] = useState([])
useEffect(() => {
let ignore = false
fetchChat(title).then((data) => {
if (!ignore) {
setList(data)//上一次没结束不能提前更新
}
})
return () => {
ignore=true//做清理工作
}
},[title])
return (
<div>
{list.map((item)=>{
return <li key={item.id}>{ item.text}</li>
})}
</div>
)
}
function App() {
const [show, setShow] = useState(true)
const [title,setTitle]=useState('电磁场与电磁波')
const handleClick = () => {
setShow(false)
}
const handleChange = (e) => {
setTitle(e.target.value)
}
return (
<div>
<button onClick={handleClick}>点我退出课堂</button>
<select value={title} onChange={handleChange}>
<option value="电磁场与电磁波">电磁场与电磁波</option>
<option value="半导体物理">半导体物理</option>
</select>
{show && <Chat title={title} />}
</div>
)
}
export default App
有很多人在遇到这样的异步操作时会用async和await,但是React不支持你这么写
你可以单独写一个async函数,然后把要执行的放进去:
import { useState, useEffect } from 'react';
function fetchChat(title) {
const delay = title === '电磁场与电磁波' ? 2000 : 1000;
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([
{ id: 1, text: title + '1' },
{ id: 2, text: title + '2' },
{ id: 3, text: title + '3' }
]);
}, delay);
});
}
function Chat({ title }) {
const [list, setList] = useState([]);
useEffect(() => {
let ignore = false;
const fetchData = async () => {
const data = await fetchChat(title);
if (!ignore) {
setList(data); // 上一次没结束不能提前更新
}
};
fetchData();
return () => {
ignore = true; // 做清理工作
};
}, [title]);
return (
<div>
{list.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</div>
);
}
function App() {
const [show, setShow] = useState(true);
const [title, setTitle] = useState('电磁场与电磁波');
const handleClick = () => {
setShow(false);
};
const handleChange = (e) => {
setTitle(e.target.value);
};
return (
<div>
<button onClick={handleClick}>点我退出课堂</button>
<select value={title} onChange={handleChange}>
<option value="电磁场与电磁波">电磁场与电磁波</option>
<option value="半导体物理">半导体物理</option>
</select>
{show && <Chat title={title} />}
</div>
);
}
export default App;
useEffectEvent
实验性版本提供的钩子
如果你的状态变量有多个,只想执行一个可以用实验版本的useEffectEvent
介于六月份同学说实验版本也用不了,所以不讲了