useEffect函数
我们的组件是纯函数,但是在函数在执行过程中对外部造成的影响称之为副作用,例如:Aiax调用,DOM操作,与外部系统同步等。所以需要React提供纯函数操作外部的对象和变量的方式,useEffect钩子提供了解决办法。
1. 使用useEffect
useEffect触发时机是在jsx的DOM渲染完成之后,类似Vue中的mounted:
import { useRef, useEffect, useState } from 'react'
function App() {
console.log("App组件开始执行...")
const [num, setNum] = useState(0)
const myRef = useRef(null)
const handleClick = ()=>{
console.log("事件触发,开始执行...")
setNum(num+1)
console.log(num)
}
useEffect(()=>{
console.log("jsx渲染完毕,开始执行...")
myRef.current.style.backgroundColor = "red"
})
console.log("开始jsx渲染...")
return (
<div>
<div>
<button onClick={handleClick}>点击</button>
</div>
<div ref={myRef}>
{num}
</div>
</div>
)
}
export default App
运行完毕: 点击按钮后,事件处理函数里面有状态修改,触发jsx重新渲染,可以看到useEffect也会重新执行。
2. 单独触发useEffect
在组件里面支持编写多个useEffect,并且多个useEffect在JSX渲染完毕后同时都会触发。但如果需要更新jsx重新渲染的时候要求只会触发指定的useEffect, 需要指定状态值。可以通过将依赖项数组指定为调用的第二个参数来告诉 React跳过不必要的重新运行Effect。
import { useRef, useEffect, useState } from 'react'
function App() {
console.log("App组件开始执行...")
const [num, setNum] = useState(0)
const [msg, setMsg] = useState('hello React')
const myRef = useRef(null)
const handleClick = ()=>{
console.log("事件触发,开始执行...")
setNum(num+1)
console.log(num)
}
// 更新的时候,只有对应依赖项发生改变的才会触发
// 内部使用Object.js()方法判断是否改变
useEffect(()=>{
console.log(num)
}, [num])
useEffect(()=>{
console.log(msg)
}, [msg])
console.log("开始jsx渲染...")
return (
<div>
<div>
<button onClick={handleClick}>点击</button>
</div>
<div ref={myRef}>
{num}, {msg}
</div>
</div>
)
}
export default App
运行效果,但如果第二个参数依赖项是空数组,表示只会在第一次渲染后执行:
Object.is()方法介绍
ES6中提供的新API,它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
Object.is('foo', 'foo') // true
Object.is({}, {}) // false
3. 在useEffect内部依赖函数
函数可以作为计算属性使用,所以也需要依赖项,但如果定义在外部,会出现即使没有改变函数也会触发的情况:
import { useRef, useEffect, useState } from 'react'
function App() {
const [num, setNum] = useState(0)
const [msg, setMsg] = useState('hello React')
const myRef = useRef(null)
const handleClick = ()=>{
setNum(num+1)
console.log(num)
}
const foo = ()=>{
console.log(msg)
}
useEffect(()=>{
console.log(num)
}, [num])
useEffect(()=>{
foo()
}, [foo])
return (
<div>
<div>
<button onClick={handleClick}>点击</button>
</div>
<div ref={myRef}>
{num}, {msg}
</div>
</div>
)
}
export default App
点击按钮会改变num的状态值,但是会触发foo()
的执行: 原因就是在于React内部使用
Object.is()
进行对象比较,foo函数在外部定义,在num状态值改变的时候,重新执行App(), foo会重新被定义,虽然foo()在num改变的时候没有发生改变,但是执行Object.is(function(){}, function(){})
会是false, 因为foo()的内存地址发生改变,所以会被重新执行。
如果在useEffect()里面定义foo()方法,即可解决这个:
import { useRef, useEffect, useState } from 'react'
function App() {
const [num, setNum] = useState(0)
const [msg, setMsg] = useState('hello React')
const myRef = useRef(null)
const handleClick = ()=>{
setNum(num+1)
console.log(num)
}
useEffect(()=>{
console.log(num)
}, [num])
useEffect(()=>{
const foo = ()=>{
console.log(msg)
}
foo()
}, [msg])
return (
<div>
<div>
<button onClick={handleClick}>点击</button>
</div>
<div ref={myRef}>
{num}, {msg}
</div>
</div>
)
}
export default App
运行效果:
4. 使用useEffect进行清理
使用useEffect也可以在组件销毁或者更新的时候执行一些清理操作:
import { useRef, useEffect, useState } from 'react'
function Chat({title}) {
const [data, setData] = useState([])
useEffect(() => {
fetchData(title).then((res) => {
setData(res)
console.log(data)
})
console.log('进入...', title)
// useEffect的清理操作
return () => {
console.log('退出...', title)
}
}, [title])
return (
<div>Hell Chat
<ul>
{data?.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
)
}
// 模拟请求后台拿去数据
function fetchData(title){
const delay = title === '情感聊天室'?1000:200
return new Promise((resolve, reject) => {
setTimeout(()=>{
resolve([
{id:1, text: title+'1'},
{id:2, text: title+'2'},
{id:3, text: title+'3'},
])
}, delay)
})
}
function App() {
const [show, setShow] = useState(true)
const [title, setTitle] = useState('情感聊天室')
const handleClick = () => {
setShow(false)
}
const handleChange = (e) => {
setTitle(e.target.value)
}
return (
<div>
<div>
<button onClick={handleClick}>关闭聊天室</button>
</div>
<select value={title} onChange={handleChange}>
<option value="情感聊天室">情感聊天室</option>
<option value="体育聊天室">体育聊天室</option>
</select>
<div>
{show && <Chat title={title}/>}
</div>
</div>
)
}
export default App
运行效果: 初始化数据时,要注意清理操作,所以更简洁的方式是使用第三方,例如:ahooks中的useRequest来请求后端数据。
5. 异步写法
useEffect在异步请求数据配合asyc和await使用的时候,会报错:
import { Row, Col } from 'antd'
import axios from 'axios'
import { useEffect, useState } from 'react'
const dataUrl = 'https://api.seniverse.com/v3/location/search.json?key=S9TdqSWVKQAmPmXmB&q=北京'
function App() {
const [count, setCount] = useState([])
useEffect(async ()=>{
const data = await axios.get(dataUrl)
setCount(data.data.results)
return ()=>{}
})
return (
<>
<Row>
<Col span={24}>
{JSON.stringify(count, null, 2)}
</Col>
</Row>
</>
)
}
export default App
执行报错: 简单来说useEffect中不支持这种写法,官方在下面也给出了正确的写法,也就是需要再包裹一层函数,然后调用申明的函数即可:
import { Row, Col } from 'antd'
import axios from 'axios'
import { useEffect, useState } from 'react'
const dataUrl = 'https://api.seniverse.com/v3/location/search.json?key=S9TdqSWVKQAmPmXmB&q=北京'
function App() {
const [count, setCount] = useState([])
const [data, setData] = useState([])
useEffect(()=>{
// 包裹一层异步函数
async function getData() {
const data = await axios.get(dataUrl)
return data
}
getData().then((data) => setData(data.data.results))
return ()=>{}
}, [count])
return (
<>
<Row>
<Col span={24}>
{JSON.stringify(data, null, 2)}
</Col>
</Row>
</>
)
}
export default App