React18-useEffect函数

useEffect 理解:

  • 是一个hook函数
  • 渲染引起的操作

useState是用来在函数式组件中添加状态,而useEffect是用来处理函数组件中的副作用的。而react希望我们尽量把函数组件写成纯函数。

什么是副作用:

副作用是相对于主作用来说的,一个函数除了主作用,其他的作用就是副作用。对于 React 组件来说,主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用(比如,手动修改 DOM)

常见的副作用:

  1. 数据请求 ajax发送
  2. 手动修改dom
  3. localstorage操作

useEffect函数的作用就是为react函数组件提供副作用处理的!

再说纯函数:既:固定的输入,会得到固定的输出。

复制代码
function myPureFunction(count) {
  return count + 1;
}

react推荐函数式组件:

复制代码
/** 纯函数示例 */
fucntion App({num}){
  return <p>我们有{num}个前端程序员</p>
}

但是在实际开发过程中,我们的函数组件会有大量的逻辑操作到函数以外的东西,使函数变的不纯,这些操作就叫副作用。

从 API 获取数据、与数据库通信以及将日志发送到日志服务等,任何可能导致相同的输入会出现不同的输出的操作,都被认为是副作用。

useEffect就是用来集中处理这些副作用的地方。

复制代码
fucntion App({num}){
  useEffect(()=>{
    // todo....  
  })

  return <p>我们有{num}个前端程序员</p>
}

useEffect的函数签名如下:

复制代码
function useEffect(effect: EffectCallback, deps?: DependencyList): void;

type EffectCallback = () => (void | Destructor);

重点:

学习function component、学习hooks,一定要忘记class component中的知识,二者毫无关联性,一切强行去关联生命周期等概念的观点,都是错误的!

国内之所以流行关联生命周期的概念,主要是因为react官网中使用class Component做了比较。

思维模型:UI=Fn(state)

useEffect返回函数, 依赖项数组

返回函数:组件卸载时会被调用,所以所有在useEffect中定义的定时器,监听,订阅等,都需要在返回函数中做相应的取消操作。

依赖项数组:当依赖项改变时,就会触发这个useEffect的调用。[]代表不参与任何react的数据流

复制代码
useEffect(()=>{
    let timer = setTimeout(()=>{

    },1000)
    return ()=>{
      clearTimeout(timer)
    }
  },[])

不要欺骗react

复制代码
useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

上述代码中,我们useEffect内部使用了count,但是没有添加进依赖项中,这导致只会被执行一次。这就是我们在欺骗react。关于这件事情:

Dan的教程是这么说:We lied to React by saying our effect doesn't depend on a value from inside our component, when in fact it does!Our effect uses count --- a value inside the component (but outside the effect).

我们欺骗react说我们的effect不依赖组件内部的值,但事实上我们使用了count

Therefore, specifying [] as a dependency will create a bug. React will compare the dependencies, and skip updating this effect. Dependencies are equal, so we skip the effect.

因此,指定[]为依赖项会产生错误。React将比较依赖关系,因为依赖是相等的,所以跳过更新此effect。

Therefore, I encourage you to adopt it as a hard rule to always be honest about the effect dependencies, and specify them all.

因此,我鼓励您将其作为一项硬性规则,始终对effect依赖项保持诚实,并指定它们。

国内的教程通常是这么说的:useEffect依赖谁,就把谁加到依赖项数组中。

useEffect的执行流程

复制代码
function App() {
  const [counter, setCounter] = useState(0)
  useEffect(() => {
    console.log('from useEffect...', counter)
  })
  function incrementClickHandler() {
    setCounter(counter + 1)
    console.log("incrementClickHandler.....", counter)
  }

  console.log('before render...', counter)
  return (
    <div className='App'>
      <h1>{counter} </h1>
      <button onClick={incrementClickHandler}>Increment</button>
    </div>
  )
}
  1. 初次渲染函数输出before render...0,useEffect 中的函数将在视图渲染后立即运行,输出from useEffect...0
  2. 点击按钮:
    1. 点击事件触发incrementClickHandler 函数,输出incrementClickHandler.....0
    2. 由于函数内部触发了setCounter,导致APP重新渲染,输出before render...1
    3. 此时useEffect不会被执行,视图将被渲染,增量按钮上方的数字将从 0 变为 1。
    4. 视图渲染之后,useEffect运行,输出from useEffect...1

Q: 为什么useEffect要在渲染之后执行?

A: 因为用户只关心视图,他不关心我们的console.loglocalStorage(或任何其他与此相关的副作用),这就是为什么react在更改状态后立即反映状态的原因。如果在状态变化和渲染(视图)之间有一些过程,那么这个过程总是会减慢渲染速度并降低用户体验。

Q: A路由跳转B路由时,先执行A路由的挂载还是B路由的卸载?

关于useEffect中调用异步函数问题

复制代码
function Counter() {
  const [count, setCount] = useState(0);
  useEffect(async () => {
    const c = await new Promise(res => {
      setTimeout(() => {
        res(2)
      }, 1000)
    })
    console.log(c)
    setCount(8)
  }, [])
  return <h1>{count}</h1>;
}

首先说一下useEffect不支持async回调函数,是因为react规定useEffect必须返回一个普通函数。所以上面这段代码是不对的。

但是,这段代码并不会报错,仅是eslint的警告而已。千万不要在项目中这么使用

还有些人会说useEffect中调用异步函数,使用自执行函数。就像这样:

复制代码
useEffect(() => {
    (async function d() {
      await new Promise(res => {
        setTimeout(() => {
          st(num => num + 1)
          res()
        }, 2000)
      })
    }
    )();
  }, [])

垃圾代码,花里胡哨,花样百出

踏踏实实用异步调用.then去解决问题。

复制代码
function Counter() {
  const [count, setCount] = useState(0);
  useEffect(()=>{
    async function getData() {
      return new Promise(res => {
        setTimeout(() => {
          res(2)
        }, 1000)
      })
    }
    getData().then(res=>{
      setCount(res)
    })
  }, [])
  return <h1>{count}</h1>;
}

依赖报警问题与解决方案

以下几种情形会被react内置的eslint规则提出警告:React Hook useEffect has a missing dependency: ''. Either include it or remove the dependency array,而通常我们解决方案都是从思考**此处使用state的原因是什么?**开始:

  1. 当开发者想仅使用一次state, 不希望后续state改变的时候触发effect,于是effect的依赖项中就没有添加state

    let [state] = useState("")
    useEffect(() => {
    console.log(state)
    }, [])

解决方案: 使用state用来做什么?

  • 用来做网络请求,赋值一类的操作?直接使用初始化的值

    let [state,setState] = useState('')
    useEffect(() => {
    let initState = ""
    setState(initState)
    request({
    data: initState
    })
    }, [])

用来求值一类的操作?直接在函数组件中定义即可,不需要使用state

复制代码
let [state] = useState(100)
let value = state / 2
// todo...
  • // !2. 如果网络请求使用props的参数,那就不存在这个问题,因为props改变必然会重新渲染。
  1. 组件effect依赖多个状态,但是某个状态变更时不希望执行effect

场景:页面中有筛选条件+开始按钮,筛选条件单独修改时不触发effect,点击开始按钮的时候触发effect,并且使用筛选条件。

复制代码
let [type] = useState("")
  let [searchKey] = useState("")
  let [buttonState] = useState("")
  useEffect(() => {
    if(buttonState === 'start'){
      console.log(state1, state2)
    }
  }, [type,searchKey,buttonState])

解决方案:不应该使用effect

复制代码
let [type] = useState("")
  let [searchKey] = useState("")

  // 某按钮触发此函数
  function startAction(){
    console.log(state1, state2)
  }

  // todo...
  1. 用到了一些第三方的hooks,不添加依赖就会提示警告

例如:此处我们认为location不会改变

复制代码
  let location = useLocation()
  useEffect(() => {
    console.log(location)
  }, [])

解决方案:按照提示把location加入到依赖中就可以了,因为location不会变的

复制代码
  let location = useLocation()
  useEffect(() => {
    console.log(location)
  }, [location])
  1. props改变, 某个effect中使用了props参数和某状态

与第一点的差距是这里有其他依赖项

复制代码
  let [state3] = useState("")
  useEffect(() => {
    console.log(props.a, state3)
  }, [props.a])

解决方案:直接使用初始值。此处使用到的不应该是state。

复制代码
  let [state,setState] = useState('')
  useEffect(() => {
    let initState = ""
    setState(initState)
    request({ // 模拟网络请求
      data: initState,
      xxx: props.a
    })
  }, [props.a])
  1. useEffect死循环

场景:分页加载数据时,第二页数据需要跟第一页数据拼接在一起,此时又不能把数据状态写进依赖中,否则就会死循环。

复制代码
  let [schoolList,setSchoolList] = useState([])
  useEffect(() => {
    setTimeout(() => { // 模拟网络请求
      let res = {
        data: [1, 2, 3, 4]
      }
      setSchoolList(schoolList.concat(res.data))
    },1000)
  }, [])

解决方案:使用setState的函数式赋值方式

复制代码
  let [schoolList,setSchoolList] = useState([])
  useEffect(() => {
    setTimeout(() => { // 模拟网络请求
      let res = {
        data: [1, 2, 3, 4]
      }
      setSchoolList(schoolList => {
        schoolList.push(...res.data)
        return schoolList
      })
    },1000)
  }, [])

你要按照上面那么写,页面是不会渲染的...

复制代码
let [schoolList,setSchoolList] = useState([])
  useEffect(() => {
    setTimeout(() => { // 模拟网络请求
      let res = {
        data: [1, 2, 3, 4]
      }
      setSchoolList(schoolList=>[...schoolList,...res.data])  
    },1000)
  }, [])

useReducer也可以用来绕过依赖项检查,useReducer也被称为hooks的作弊模式。不过没必要的话还是少用吧

useEffect批处理的bug

useEffect仅对react事件进行批处理操作。v18之后修改了这个问题。

复制代码
  let [text, setText] = useState({})
  let [text1, setText1] = useState({})

  useEffect(() => {
    console.log(text, text1)
    console.log("触发网络请求")
  }, [text, text1])

  useEffect(() => {
    console.log('批处理')
    setText({ a: "1" })
    setText1({ f: 2 })
  }, [])
  useEffect(() => {
    console.log('非批处理')
    setTimeout(() => {
      setText({ a: "1" })
      setText1({ f: 2 })
    }, 1000)
  }, [])

useLayoutEffect

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。会造成阻塞视觉更新

Race condition

复制代码
let num = 2

setTimeout(()=>{
  num = num * 2
},Math.random()*1000)

setTimeout(()=>{
  num = num + 20
},Math.random()*1000)

setTimeout(()=>{
  console.log(num) // => 输出num是多少??
},1000)

示例代码如下:快速的点击会造成竞态的bug,最终的UI并非正确显示的UI

复制代码
import React, { useEffect, useState } from "react";

export default function App() {
  const [currentId, setCurrentId] = useState(1);

  const handleClick = () => {
    const idToFetch = Math.round(Math.random() * 80);
    console.log(idToFetch, 'xx')
    setCurrentId(idToFetch);
  };

  return (
    <React.Fragment>
      <div>
        <p>Requesting ID: {currentId}</p>
        <button type="button" onClick={handleClick}>
          Fetch data!
        </button>
      </div>
      <hr />
      <DataDisplayer id={currentId} />
    </React.Fragment>
  );
}

function DataDisplayer(props) {
  const [data, setData] = useState(null);
  const [fetchedId, setFetchedId] = useState(null);

  useEffect(() => {
    // let active = true;
    const fetchData = async () => {
      setTimeout(async () => {
        const response = await fetch(
          `https://swapi.dev/api/people/${props.id}/`
        );
        const newData = await response.json();
        // if (active) {
        setFetchedId(props.id);
        console.log(props.id)
        setData(newData);
        // }
      }, Math.round(Math.random() * 1000));
    };

    fetchData();
    // return () => {
    //   active = false;
    // };
  }, [props.id]);

  if (data) {
    return (
      <div>
        <p style={{ color: fetchedId === props.id ? "green" : "red" }}>
          Displaying Data for: {fetchedId}
        </p>
        <p>{data.name}</p>
      </div>
    );
  } else {
    return null;
  }
}

为什么会发生这种情况?

多个请求被并行触发(竞争渲染相同的视图),我们只是假设最后一个请求将最后解决。实际上,最后一个请求可能首先解决,或者只是失败,导致第一个请求最后解决。

竞争条件:最终的结果取决于程序的精准时序。

解决方案,通常为忽略请求的结果和取消请求的方式。例如上述示例代码中注释的代码就是采取的忽略结果的方式。

解决方案二:利用Suspense来立刻渲染的特性来解决竞态条件,具体可查阅文章:react-17.0.2-Suspense

重点

useEffect并非react中需要经常使用的hook, 我们应该尽量减少对useEffect的使用,在无需使用useEffect的时候就不要使用

示例:

使用useEffect

复制代码
function SchoolPage(props) {
    const { history } = props
    const [activeTab, setActiveTab] = useState(0)
    // 设置选中的tab
    useEffect(() => {
      switch (history.location.pathname.split('/')[3]) {
        case "special":
          setActiveTab(1)
          break
        case "future":
          setActiveTab(2)
          break
        case "news":
          setActiveTab(3)
          break
        case 'colleges':
          setActiveTab(4)
          break
        default:
          setActiveTab(0)
          break
      }
    }, [history.location.pathname])
  
    return (
      <div>
        <span className={\`\$\{styles.tabBar\} \$\{activeTab === 0 ? styles.active : ""\}\`}
          onClick={() => history.replace(\`/school/\$\{schoolCode\}/\`)}>
          学校概况
        </span>
        <span className={\`\$\{styles.tabBar\} \$\{activeTab === 1 ? styles.active : ""\}\`}
          onClick={() => history.replace(\`/school/\$\{schoolCode\}/special\`)}>
          开设专业
        </span>
        <span className={\`\$\{styles.tabBar\} \$\{activeTab === 2 ? styles.active : ""\}\`}
          onClick={() => history.replace(\`/school/\$\{schoolCode\}/future\`)}>
          升学方向
        </span>
        <span className={\`\$\{styles.tabBar\} \$\{activeTab === 3 ? styles.active : ""\}\`}
          onClick={() => history.replace(\`/school/\$\{schoolCode\}/news\`)}>
          招生快讯
        </span>
        <span className={\`\$\{styles.tabBar\} \$\{activeTab === 4 ? styles.active : ""\}\`}
          onClick={() => history.replace(\`/school/\$\{schoolCode\}/colleges\`)}>
          院校资讯
        </span>
      </div>
    )
  }

不使用useEffect

复制代码
  function SchoolPage(props) {
    const { history } = props
    // 设置选中的tab
    const activeTab =history.location.pathname.split('/')[3]
  
    return (
      <div>
        <span className={\`\$\{styles.tabBar\} \$\{activeTab === 'gaikuang' ? styles.active : ""\}\`}
          onClick={() => history.replace(\`/school/\$\{schoolCode\}/\`)}>
          学校概况
        </span>
        <span className={\`\$\{styles.tabBar\} \$\{activeTab === 'zhuanye' ? styles.active : ""\}\`}
          onClick={() => history.replace(\`/school/\$\{schoolCode\}/special\`)}>
          开设专业
        </span>
        <span className={\`\$\{styles.tabBar\} \$\{activeTab === 'fangxiang' ? styles.active : ""\}\`}
          onClick={() => history.replace(\`/school/\$\{schoolCode\}/future\`)}>
          升学方向
        </span>
        <span className={\`\$\{styles.tabBar\} \$\{activeTab === 'zhaosheng' ? styles.active : ""\}\`}
          onClick={() => history.replace(\`/school/\$\{schoolCode\}/news\`)}>
          招生快讯
        </span>
        <span className={\`\$\{styles.tabBar\} \$\{activeTab === 'zixun' ? styles.active : ""\}\`}
          onClick={() => history.replace(\`/school/\$\{schoolCode\}/colleges\`)}>
          院校资讯
        </span>
      </div>
    )
  }

总结使用步骤:

  1. 导入 useEffect 函数
  2. 调用 useEffect 函数,并传入回调函数
  3. 在回调函数中编写副作用处理(dom操作)
  4. 修改数据状态
  5. 检测副作用是否生效

语法:useEffect(()=>{},[])

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax