Skip to content

useEffect函数

我们的组件是纯函数,但是在函数在执行过程中对外部造成的影响称之为副作用,例如:Aiax调用,DOM操作,与外部系统同步等。所以需要React提供纯函数操作外部的对象和变量的方式,useEffect钩子提供了解决办法。

1. 使用useEffect

useEffect触发时机是在jsx的DOM渲染完成之后,类似Vue中的mounted:

jsx
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

运行完毕:
Alt text 点击按钮后,事件处理函数里面有状态修改,触发jsx重新渲染,可以看到useEffect也会重新执行。

2. 单独触发useEffect

在组件里面支持编写多个useEffect,并且多个useEffect在JSX渲染完毕后同时都会触发。但如果需要更新jsx重新渲染的时候要求只会触发指定的useEffect, 需要指定状态值。可以通过将依赖项数组指定为调用的第二个参数来告诉 React跳过不必要的重新运行Effect。

jsx
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

运行效果,但如果第二个参数依赖项是空数组,表示只会在第一次渲染后执行:
Alt text

Object.is()方法介绍

ES6中提供的新API,它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

js
Object.is('foo', 'foo')   // true
Object.is({}, {})         // false

3. 在useEffect内部依赖函数

函数可以作为计算属性使用,所以也需要依赖项,但如果定义在外部,会出现即使没有改变函数也会触发的情况:

jsx
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()的执行:
Alt text 原因就是在于React内部使用Object.is()进行对象比较,foo函数在外部定义,在num状态值改变的时候,重新执行App(), foo会重新被定义,虽然foo()在num改变的时候没有发生改变,但是执行Object.is(function(){}, function(){})会是false, 因为foo()的内存地址发生改变,所以会被重新执行。
如果在useEffect()里面定义foo()方法,即可解决这个:

jsx
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

运行效果:
Alt text

4. 使用useEffect进行清理

使用useEffect也可以在组件销毁或者更新的时候执行一些清理操作:

jsx
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

运行效果:
Alt text 初始化数据时,要注意清理操作,所以更简洁的方式是使用第三方,例如:ahooks中的useRequest来请求后端数据。

5. 异步写法

useEffect在异步请求数据配合asyc和await使用的时候,会报错:

jsx
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

执行报错:
Alt text 简单来说useEffect中不支持这种写法,官方在下面也给出了正确的写法,也就是需要再包裹一层函数,然后调用申明的函数即可:

jsx
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