04|快速学会使用 useEffect

1,086 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

在👉 上一节 中,我们掌握了useStateuseState可以使函数组件维持住状态。本节我们一起学习 React 中的useEffect(副作用)。在开始之前,我想说一句:useEffect 并不是组件的生命周期。

⚠️ 注意:如果你以往以 Class Component 方式开发,请一定不要将 useEffect 对应到 Class Component 的任何生命周期中,请你忘掉那些生命周期!!!

image.png

useEffect 的基本概念

副作用是指一段和当前执行结果无关的代码。比如说要修改函数外部的某个变量,要发起一个请求,等等。也就是说,在函数组件的当次执行过程中,useEffect 中代码的执行是不影响渲染出来的 UI 的。下面是 useEffect 的函数签名👇

useEffect(callback, dependencies)

第一个为要执行的函数 callback,第二个是可选的依赖项数组 dependencies。 依赖项这个参数是可选的,callback 会根据依赖项分为以下三种情况:

  1. 如果不指定,那么 callback 就会在每次函数组件执行完后都执行;
  2. 如果指定了,那么只有依赖项中的值发生变化的时候,它才会执行;
  3. 如果指定为空数组,那么 callback 会在 mount (首次render)以后执行。

值得注意的是,callback 是在页面渲染完成以后才会执行,我们看下面的代码的执行顺序

import { useEffect } from 'react';
function App() {

  useEffect(() => {
    console.log('useEffect打印')
  }, [])

  console.log('render打印')

  return (
    <div>渲染内容</div>
  )
}

export default App;

May-26-2022 18-03-48.gif

callback 除了执行顺序这个特点,第二个特点就是callback本身不能是一个 Promise 函数,如果你需要在里面执行异步代码,可以在内部做,示例如下👇

function App() {
  
  useEffect(async() => { // ❌ 像这种情况就是不允许的
    const res = await query()
  }, [])
  
  useEffect(() => { // ✅ 这两种方式都可以
    // 1. 封装一个函数,然后执行
    const promise = async () => {
      const res = await query()
    }
    promise()
    
    // 2. 使用IFEE(立即执行函数)其实本质与上面一样
    (async() => {
      const res = await query()
    })(); // 如果后面还有代码的话需要加上分号,不然 js 解析会报错
  }, [])
  
  return null
}

callback 的第三个特点就是它可以返回一个函数,这个函数会在下一次 render 以后执行(假设我们useEffect所依赖的值发生变化,会再次触发 useEffect),它主要用于清除副作用,比如定时器等。(后面会有例子,这里就不写了)

useEffect 可以用来干什么?

如果你打开 react 的官方网站,搜索useEffect,相信我,你看完绝对是一脸懵逼,心里也许一万个问号。这玩意儿是个啥?我能用它干什么?

image.png

别着急,下面我将举一些简单的例子,简单的说明一下 useEffect 的使用场景,作为一个基本印象,后续我们会经常使用它,所以不用担心。

1. 数据请求

这也许是最常见的一种场景,我想不需要过多解释,直接看怎么用。

interface UserType {
  name: string;
  age: number;
}

// 你可以假装这是个请求的接口,一秒钟以后会返回你一个用户列表
function queryUsers(): Promise<UserType[]> {
  return new Promise((resolve, reject) => {
    const list = [
      { id: 1, name: '小黑', age: 18 },
      { id: 2, name: '小白', age: 16 },
      { id: 3, name: '小光', age: 19 },
    ]
    setTimeout(() => {
      resolve(list)
    }, 1000)
  })
}

function App() {

  const [users, setUsers] = useState<UserType[]>([])
  
  useEffect(() => {
    const queryUsersHandler = async () => {
      const res = await queryUsers() // 发起请求获得数据
      // 设置新的数据
      setUsers(res)
    }

    queryUsersHandler()
  }, [])

  return (
    <div>
      {
        {/* 根据数据渲染 */}
        users.map(item => (
          <div key={item.id} style={{ marginBottom: 20 }}>
            <div>姓名:{item.name}</div>
            <div>年龄:{item.age}</div>
          </div>
        ))
      }
    </div>
  )
}


2. 监听DOM事件

假设我们需要监听浏览器窗口大小,那么这时候 callback 中返回的函数就需要写上对应的移除监听的事件

function App() {

  useEffect(() => {
    const handler = () => {
      setScrollSize({
        wdith: window.innerWidth,
        height: window.innerHeight,
      })
    }
    handler()
    // 一般来讲肯定是要做防抖的,我这里就偷个懒
    window.addEventListener('resize', handler)

    return () => {
      // 在return 的函数中清除掉
      window.removeEventListener('resize', handler)
    }
  }, [])
  
  return (
    <div>
      <div>window width: {windowSize.wdith}</div>
      <div>window height: {windowSize.height}</div>
    </div>
  )
}

3. 监听数据做出响应

我个人感觉,一般来讲这样的场景都是数据变化了就发送请求(比如用户 id 变化了,就会根据新的 id 发送请求),当然纯粹的数据监听,然后做点儿什么也是有这样的情况存在的

function App() {

  const [userId, setUserId] = useState('a')
  
  useEffect(() => {
    // 大概就是如此,每次 userId 变化了就在这里发个请求啥的(mount 的时候也会执行)
    fetch(userId)
    // ......
  }, [userId])

  // .....
}

4. 定时器轮询

有时我们需要针对一个接口进行轮询操作,或者是其他需要使用到定时器每秒做些什么事情(总之就是需要用定时器来做点儿什么事,尽管一般都是使用延时器🤣 🤣 🤣)比如这里进入页面开始,就要不停的通过接口获取某个状态,根据返回的状态进行展示。(为了代码完整性,我都贴出来了,重点看 Demo 组件就行)

interface STA {
  sta: number
}

let retSta = 0
const resetRetSta = () => retSta = 0
const COMPLETE = 3

function queryStatus(): Promise<STA> {
  return daley({ sta: retSta++ }, 1000)
}

function daley<T>(res: T, time: number): Promise<T> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(res)
    }, time)
  })
}


function Demo() {
  
  const [status, setStatus] = useState(0)

  useEffect(() => {
    console.log('useEffect')
    let timer = setInterval(async () => {
      const res = await queryStatus()
      setStatus(res.sta)
      console.log((res))
      if (res.sta === COMPLETE) { // 完成以后停止发送请求
        clearInterval(timer)
        resetRetSta() // 为了重置返回sta,忽略即可
      }
    }, 1200)

    // 组件被关闭时,清除定时器
    return () => {
      console.log('return callback')
      clearInterval(timer)
      resetRetSta() // 为了重置返回sta,忽略即可
    }
  }, [])

  console.log('render')
  
  return (
    <div id='app'>
      <div>{ status === COMPLETE ? '任务完成' : '任务进行中' }</div>
    </div>
  )
}


function App() {

  const [visible, setVisible] = useState(false)

  const handle = () => {
    setVisible(!visible)
  }

  return (
    <div>
      <button onClick={handle}>{ visible ? '不展示' : '展示' }</button>
      { visible && <Demo /> }
    </div>
  )
}

你可以尝试一下不写callback中的返回函数中的clearInterval(timer),并在展示Demo组件时设置为不展示试试看,它会不停的发送请求(直至判断为完成,清楚定时器为止)。

同样的上面监听DOM事件也是一样,尝试监听一个dom节点,在其展示了以后再设置为不展示,那么会因为找不到dom节点而不停的产生错误日志。(在原生JS中会发生什么事情,那么在React会产生同样的现象,毕竟 React 的本质就是JS)内存泄漏就此造成~ 所以在处理这类场景时记得要清除掉副作用。

如何避免 useEffect 的“坑”

其实只要你把上面的这几点搞明白了,一般来讲也不会太遇到什么坑。我们这里就简单举一两个需求说明一下。

假设我们有一个需求,在屏幕宽度在300以下时上报当前用户的状态。(哈哈哈,把产品经理 🦈 🌶️ 🤣 )

在下面的代码中,我们在页面加载时添加一个监听事件,我们期望的是,当页面发生resize并且屏幕宽度小于等于300时上报当前的用户状态userStatus和屏幕宽度screeWidth

function App() {

  const [userStatus, setUserStatus] = useState(0)

  useEffect(() => {
    const handler = () => {
      const screenWidth = window.innerWidth
      if (screenWidth <= 300) {
        // 假设这里是上传接口
        request(userStatus, screenWidth)
      }
    }
    handler()
    // 一般来讲肯定是要做防抖的,我这里就偷个懒
    window.addEventListener('resize', handler)

    return () => {
      // 在return 的函数中清除掉
      window.removeEventListener('resize', handler)
    }
  }, [])
  
  return (
    <div>
      <div>当前状态:{userStatus}</div>
      <button onClick={() => setUserStatus(userStatus + 1)}>状态值+1</button>
    </div>
  )
}

function request(userStatus: number, width: number) {
  console.log('上报用户状态与窗口宽度', userStatus, width)
  return daley({ sta: 1, msg: 'success' }, 1000)
}

接着我们在浏览器中看结果

Jun-02-2022 17-11-47.gif

可以发现,我们已经修改过了的userStatus明明已经等于 4 了,可在发送请求时仍等于 0

image.png

我直接说怎么处理这种情况吧,后续会更一节讲 React 渲染相关的。 这里有两种处理方式:

  1. 使用useRef单独存储 一个userStatus的副本
const [userStatus, setUserStatus] = useState(0)
const statusRef = useRef(userStatus) // 使用 useRef
statusRef.current = userStatus // 更新值

useEffect(() => {
  const handler = () => {
    const screenWidth = window.innerWidth
    if (screenWidth <= 300) {
      // 使用值
      request(statusRef.current, screenWidth)
    }
  }
}, [])

如果你还不知道useRef是什么,那么你暂时就当它的返回值.current是一个普通的变量好了。

  1. useEffectdependencies中添加userStatus即可。
  useEffect(() => {
    // ......
  }, [userStatus])

第一种方法确实行之有效,但是缺带来了一切其他麻烦,比如让代码变得更加复杂,难以维护等。所以还是推荐使用第二种方式。

但是我们在写代码过程中难免会存在忘记添加依赖项的时候,所以为了开发效率更高,更省心,官方建议我们安装校验 hooks 的插件—— eslint-plugin-react-hooks 。这个插件会帮助我们检查书写的 hook 是否正确的添加上了所使用的依赖项(需要有eslint,vscode 的 eslint 插件也可以)。

  1. 安装
yarn add eslint-plugin-react-hooks --dev

#OR
npm install eslint-plugin-react-hooks --save-dev
  1. 修改eslint配置(我这里是在package.json文件内)
  "eslintConfig": {
    // ......
    "plugins": ["react-hooks"],
    "rules": {
      "react-hooks/rules-of-hooks": "error",
      "react-hooks/exhaustive-deps": "warn"
    }
  },

我们来尝试一下

Jun-02-2022 17-59-37.gif

那么在我们写出错误的hook时就会出现错误提示,出现不规范的hook时也会提示我们原因来进行修复对应的问题。

这一节我们学习了useEffect的一些基本使用方法,在一些场景中如何使用,以及怎样有效避坑,我们本节就先到这里,如果你觉得还算有用,还请点点赞👍 谢谢啦~

这里我留一个小小的思考题:你知道为什么 callback 本身不能是一个Promise吗?欢迎在评论区留言回复答案 😝 (提示,答案就在文中哦~)

image.png