React常见Hooks使用(二)

副作用

useEffect

useEffect 是 React 中用于处理副作用的钩子。并且useEffect 还在这里充当生命周期函数,在之前你可能会在类组件中使用 componentDidMountcomponentDidUpdatecomponentWillUnmount 来处理这些生命周期事件。

什么是副作用函数,什么是纯函数?

纯函数

  • 输入决定输出:相同的输入永远会得到相同的输出。这意味着函数的行为是可预测的。
  • 无副作用:纯函数不会修改外部状态,也不会依赖外部可变状态。因此,纯函数内部的操作不会影响外部的变量、文件、数据库等。
ts 复制代码
function add(a: number, b: number): number {
  return a + b;
}

无论你执行多少次:

ts 复制代码
add(1, 2) // 永远都是 3

没有修改任何外部变量、没有发请求、没有打印日志 ------ 这就是一个纯函数。

副作用函数

  • 副作用函数 指的是那些在执行时会改变外部状态或依赖外部可变状态的函数。

  • 可预测性降低但是副作用不一定是坏事有时候副作用带来的效果才是我们所期待的

  • 高耦合度函数非常依赖外部的变量状态紧密

    • 操作引用类型
    • 操作本地存储例如localStorage
    • 调用外部API,例如fetch ajax
    • 操作DOM
    • 计时器
ts 复制代码
let globalVariable = 0;

function calculateDouble(number){  
  globalVariable += 1; //修改函数外部环境变量

  localStorage.setItem('globalVariable', globalVariable); //修改 localStorage

  fetch(/*...*/).then((res)=>{ //网络请求
   //...  
  }); 

  document.querySelector('.app').style.color = 'red'; //修改 DOM element

  return number *2
}

这个函数每次调用都会改变外部状态,所以它是一个副作用函数。

useEffect用法

ts 复制代码
  useEffect(setup, dependencies?)
  useEffect(() => {
  	const connection = createConnection(serverUrl, roomId); // 执行副作用
    connection.connect();
  	return () => { // 清理函数
      connection.disconnect();
  	};
  }, [serverUrl, roomId]) // 依赖项列表
          

参数

  • setup:Effect处理函数,可以返回一个清理函数。组件挂载时执行setup,依赖项更新时先执行cleanup再执行setup,组件卸载时执行cleanup。
  • dependencies(可选):setup中使用到的响应式值列表(props、state等)。必须以数组形式编写如[dep1, dep2]。不传则每次重渲染都执行Effect。

返回值

useEffect 返回 undefined

tsx 复制代码
let a = useEffect(() => {})
console.log('a', a) //undefined

基本使用

副作用函数能做的事情useEffect都能做,例如操作DOM、网络请求、计时器等等。

操作DOM

jsx 复制代码
import { useEffect } from 'react'

function App() {
  const dom = document.getElementById('data')
  console.log(dom) // 这里的dom是null,因为useEffect是在组件渲染后执行的,此时dom还没有被渲染出来
  useEffect(() => {
    const data = document.getElementById('data')
    console.log(data) //<div id='data'>张三</div> 这里的data是有值的,因为useEffect是在组件渲染后执行的,此时dom已经被   渲染出来了
  }, [])
  return <div id='data'>张三</div>
}

网络请求

tsx 复制代码
useEffect(() => {
  fetch('http://localhost:5174/?name=AA')
}, [])

执行时机

组件挂载时执行

根据我们下面的例子可以观察到,组件在挂载的时候就执行了useEffect的副作用函数。

类似于componentDidMount

tsx 复制代码
useEffect(() => {
  console.log('组件挂载时执行')
})
组件更新时执行
  • 无依赖项更新

根据我们下面的例子可以观察到,当有响应式值发生改变时,useEffect的副作用函数就会执行。

类似于componentDidUpdate + componentDidMount

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

const App = () => {
   const [count, setCount] = useState(0)
   const [name, setName] = useState('')
   useEffect(() => {
      console.log('执行了', count, name)
   }) // useEffect没有第二个参数,无依赖项
   return (
      <div id='data'>
         <div>
            <h3>count:{count}</h3>
            <button onClick={() => setCount(count + 1)}>+</button>
         </div>
         <div>
            <h3>name:{name}</h3>
            <input value={name} onChange={e => setName(e.target.value)} />
         </div>
      </div>
   )
}
export default App
  • 有依赖项更新

根据我们下面的例子可以观察到,当依赖项数组中的count值发生改变时,useEffect的副作用函数就会执行。而当name值改变时,由于它不在依赖项数组中,所以不会触发副作用函数的执行。

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

const App = () => {
   const [count, setCount] = useState(0)
   const [name, setName] = useState('')
   useEffect(() => {
      console.log('执行了', count, name)
   }, [count]) //当count发生改变时执行
   return (
      <div id='data'>
         <div>
            <h3>count:{count}</h3>
            <button onClick={() => setCount(count + 1)}>+</button>
         </div>
         <div>
            <h3>name:{name}</h3>
            <input value={name} onChange={e => setName(e.target.value)} />
         </div>
      </div>
   )
}
export default App
  • 依赖项空值

根据我们下面的例子可以观察到,当依赖项为空数组时,useEffect的副作用函数只会执行一次,也就是组件挂载时执行。

适合做一些初始化的操作例如获取详情什么的。

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

const App = () => {
   const [count, setCount] = useState(0)
   const [name, setName] = useState('')
   useEffect(() => {
      console.log('执行了', count, name)
   }, []) //只会执行一次
   return (
      <div id='data'>
         <div>
            <h3>count:{count}</h3>
            <button onClick={() => setCount(count + 1)}>+</button>
         </div>
         <div>
            <h3>name:{name}</h3>
            <input value={name} onChange={e => setName(e.target.value)} />
         </div>
      </div>
   )
}
export default App
组件卸载时执行

useEffect的副作用函数可以返回一个清理函数,当组件卸载时,useEffect的副作用函数就会执行清理函数。

确切说清理函数就是副作用函数运行之前,会清楚上一次的副作用函数。

根据我们下面的例子可以观察到,当组件卸载时,useEffect的副作用函数就会执行。

类似于componentWillUnmount

tsx 复制代码
import { useEffect, useState } from "react"
// 子组件
const Child = (props: { name: string }) => {
   useEffect(() => {
      console.log('render', props.name)
      // 返回一个清理函数
      return () => {
         console.log('unmount', props.name) // 组件卸载时执行
      }
   }, [props.name])
   return <div>Child:{props.name}</div>
}
const App = () => {
   const [show, setShow] = useState(true)
   const [name, setName] = useState('')
   return (
      <div id='data'>
         <div>
            <h3>父组件</h3>
            <input value={name} onChange={e => setName(e.target.value)} />
            <button onClick={() => setShow(!show)}>显示/隐藏</button>
         </div>
         <hr />
         <h3>子组件</h3>
         {show && <Child name={name} />}
      </div>
   )
}

export default App
清理函数应用场景

例如我们下面这个例子,当name值发生改变时,useEffect的副作用函数就会执行,并且会开启一个定时器,当name值再次发生改变时,useEffect的副作用函数就会执行清理函数,清除上一次的定时器。这样就避免了接口请求的重复执行。

tsx 复制代码
import { useEffect, useState } from "react"
// 子组件
const Child = (props: { name: string }) => {
   useEffect(() => {
      let timer = setTimeout(() => {
         fetch(`http://localhost:5174/?name=${props.name}`)
      }, 1000)
      return () => {
         clearTimeout(timer) // 当name值发生改变时,useEffect的副作用函数就会执行,并且会开启一个定时器,避免了接口请求的重复执行
      }
   }, [props.name])
   return <div>Child</div>
}
const App = () => {
   const [show, setShow] = useState(true)
   const [name, setName] = useState('')
   return (
      <div id='data'>
         <div>
            <h3>父组件</h3>
            <input value={name} onChange={e => setName(e.target.value)} />
            <button onClick={() => setShow(!show)}>显示/隐藏</button>
         </div>
         <hr />
         <h3>子组件</h3>
         {show && <Child name={name} />}
      </div>
   )
}

export default App

真实案例

下面是一个真实的用户信息获取案例,通过id获取用户信息,并且当id发生改变时,会获取新的用户信息。

tsx 复制代码
import React, { useState, useEffect } from 'react';
interface UserData {
  name: string;
  email: string;
  username: string;
  phone: string;
  website: string;
}
function App() {
  const [userId, setUserId] = useState(1); // 假设初始用户ID为1
  const [userData, setUserData] = useState<UserData | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  /**
   * 当 userId 发生变化时,触发副作用函数,从 API 获取用户数据
   */
  useEffect(() => {
    const fetchUserData = async () => {
      setLoading(true);
      try {
        const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`); //免费api接口 可以直接使用
        if (!response.ok) {
          throw new Error('网络响应不正常');
        }
        const data = await response.json();
        setUserData(data);
      } catch (err: any) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchUserData();
  }, [userId]);

  /**
   * 处理用户输入框值变化的函数,将输入的用户 ID 更新到 userId 状态中
   * @param event - 输入框变化事件对象
   */
  const handleUserChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setUserId(parseInt(event.target.value));
  };

  return (
    <div>
      <h1>用户信息应用</h1>
      <label>
        输入用户ID:
        <input type="number" value={userId} onChange={handleUserChange} min="1" max="10" />
      </label>
      {loading && <p>加载中...</p>}
      {error && <p>错误: {error}</p>}
      {userData && (
        <div>
          <h2>用户信息</h2>
          <p>姓名: {userData.name}</p>
          <p>邮箱: {userData.email}</p>
          <p>用户名: {userData.username}</p>
          <p>电话: {userData.phone}</p>
          <p>网站: {userData.website}</p>
        </div>
      )}
    </div>
  );
}

export default App;

useLayoutEffect

useLayoutEffect 是 React 中的一个 Hook,用于在浏览器重新绘制屏幕之前触发。与 useEffect 类似。

用法

jsx 复制代码
useLayoutEffect(() => {
  // 副作用代码
  return () => {
    // 清理代码
  }
}, [dependencies]);

参数

  • setup:Effect处理函数,可以返回一个清理函数。组件挂载时执行setup,依赖项更新时先执行cleanup再执行setup,组件卸载时执行cleanup。
  • dependencies(可选):setup中使用到的响应式值列表(props、state等)。必须以数组形式编写如[dep1, dep2]。不传则每次重渲染都执行Effect。

返回值

useLayoutEffect 返回 undefined

区别(useLayoutEffect/useEffect)

区别 useLayoutEffect useEffect
执行时机 浏览器完成布局和绘制之前执行副作用 浏览器完成布局和绘制之后执行副作用
执行方式 同步执行 异步执行
DOM渲染 阻塞DOM渲染 不阻塞DOM渲染

测试DOM阻塞

下面这个例子展示了 useLayoutEffect 和 useEffect 在DOM渲染时的区别。useLayoutEffect 会阻塞DOM渲染,而 useEffect 不会。

tsx

jsx 复制代码
import React, { useLayoutEffect, useEffect, useState } from 'react';

function App() {
   const [count, setCount] = useState(0)
   //不阻塞DOM
   // useEffect(() => {
   //    for (let i = 0; i < 50000; i++) {
   //       //console.log(i);
   //       setCount(count => count + 1)
   //    }
   // }, []);
   //阻塞DOM
   // useLayoutEffect(() => {
   //    for (let i = 0; i < 50000; i++) {
   //       //console.log(i);
   //       setCount(count => count + 1)
   //    }
   // }, []);
   return (
      <div>
         <div>app </div>
         {
            Array.from({ length: count }).map((_, index) => (
               <div key={index}>{index}</div>
            ))
         }
      </div>
   );
}

export default App;

测试同步异步渲染

在下面的动画示例代码中:

  1. useEffect 实现的动画效果:
    • 初始渲染时 opacity: 0
    • 浏览器完成绘制
    • useEffect 异步执行,设置 opacity: 1
    • 用户可以看到完整的淡入动画过渡效果
  2. useLayoutEffect 实现的动画效果:
    • 初始渲染时 opacity: 0
    • DOM 更新后立即同步执行 useLayoutEffect
    • 设置 opacity: 1
    • 浏览器绘制时已经是最终状态
    • 用户看不到过渡动画效果
css 复制代码
#app1 {
    width: 200px;
    height: 200px;
    background: red;
}

#app2 {
    width: 200px;
    height: 200px;
    background: blue;
    margin-top: 20px;
    position: absolute;
    top: 230px;
}
tsx 复制代码
import React, { useLayoutEffect, useEffect, useRef } from 'react';
function App() {
   // 使用 useEffect 实现动画效果
   useEffect(() => {
      const app1 = document.getElementById('app1') as HTMLDivElement;
      app1.style.transition = 'opacity 3s';
      app1.style.opacity = '1';
   }, []);

   // 使用 useLayoutEffect 实现动画效果
   useLayoutEffect(() => {
      const app2 = document.getElementById('app2') as HTMLDivElement;
      app2.style.transition = 'opacity 3s';
      app2.style.opacity = '1';

   }, []);

   return (
      <div>
         <div id="app1"  style={{ opacity: 0 }}>app1</div>
         <div id="app2"  style={{ opacity: 0 }}>app2</div>
      </div>
   );
}

export default App;

应用场景

  • 需要同步读取或更改DOM:例如,你需要读取元素的大小或位置并在渲染前进行调整。
  • 防止闪烁:在某些情况下,异步的useEffect可能会导致可见的布局跳动或闪烁。例如,动画的启动或某些可见的快速DOM更改。
  • 模拟生命周期方法:如果你正在将旧的类组件迁移到功能组件,并需要模拟 componentDidMount、componentDidUpdate和componentWillUnmount的同步行为。

案例

可以记录滚动条位置,等用户返回这个页面时,滚动到之前记录的位置。增强用户体验。

tsx 复制代码
import React, { useLayoutEffect } from 'react';

function App() {
  const handelScrool = (e: React.UIEvent<HTMLDivElement>) => {
    const scroolTop = e.currentTarget.scrollTop;
    window.history.replaceState({}, '', `?top=${scroolTop}`); // 每次滚动时,将滚动位置保存到url中
  };
  useLayoutEffect(() => {
    // 获取url中的top值,然后滚动到指定位置
    const container = document.getElementById('container') as HTMLDivElement;
    const top = window.location.search.split('=')[1];
    container.scrollTop = parseInt(top); // 这里的top是字符串,需要转换成数字,否则会报错,因为scrollTop的类型是number,而不是unknow
  }, []);
  return (
    <div onScroll={handelScrool} id="container" style={{ height: '500px', overflow: 'auto' }}>
      {Array.from({ length: 500 }, (_, i) => (
        <div key={i} style={{ height: '100px', borderBottom: '1px solid #ccc' }}>
          Item {i + 1}
        </div>
      ))}
    </div>
  );
}

export default App;

useRef

当你在React中需要处理DOM元素或需要在组件渲染之间保持持久性数据时,便可以使用useRef。

ts 复制代码
import { useRef } from 'react';
const refValue = useRef(initialValue)
refValue.current // 访问ref的值 类似于vue的ref,Vue的ref是.value,其次就是vue的ref是响应式的,而react的ref不是响应式的

通过Ref操作DOM元素

参数

  • initialValue:ref 对象的 current 属性的初始值。可以是任意类型的值。这个参数在首次渲染后被忽略。

返回值

  • useRef返回一个对象,对象的current属性指向传入的初始值。 {current:xxxx}

注意

  • 改变 ref.current 属性时,React 不会重新渲染组件。React 不知道它何时会发生改变,因为 ref 是一个普通的 JavaScript 对象。
  • 除了 初始化 外不要在渲染期间写入或者读取 ref.current,否则会使组件行为变得不可预测。
tsx 复制代码
import { useRef } from "react"
function App() {
  //首先,声明一个 初始值 为 null 的 ref 对象
  let div = useRef(null)
  const heandleClick = () => {
    //当 React 创建 DOM 节点并将其渲染到屏幕时,React 将会把 DOM 节点设置为 ref 对象的 current 属性
    console.log(div.current)
  }
  return (
    <>
      {/*然后将 ref 对象作为 ref 属性传递给想要操作的 DOM 节点的 JSX*/}
      <div ref={div}>dom元素</div>
      <button onClick={heandleClick}>获取dom元素</button>
    </>
  )
}
export default App

数据存储

我们实现一个保存count的新值和旧值的例子,但是在过程中我们发现一个问题,就是num的值一直为0,这是为什么呢?

因为等useStateSetCount执行之后,组件会重新rerender,num的值又被初始化为了0,所以num的值一直为0。

ts 复制代码
import React, { useLayoutEffect, useRef, useState } from 'react';

function App() {
   let num = 0
   let [count, setCount] = useState(0)
   const handleClick = () => {
      setCount(count + 1)
      num = count;
   };
   return (
      <div>
         <button onClick={handleClick}>增加</button>
         <div>{count}:{num}</div>
      </div>
   );
}

export default App;

如何修改?

我们可以使用useRef来解决这个问题,因为useRef只会在初始化的时候执行一次,当组件reRender的时候,useRef的值不会被重新初始化。

tsx 复制代码
import React, { useLayoutEffect, useRef, useState } from 'react';

function App() {
   let num = useRef(0) // 将num转换成useRef类型,useRef的值不会被重新初始化
   let [count, setCount] = useState(0)
   const handleClick = () => {
      setCount(count + 1)
      num.current = count;
   };
   return (
      <div>
         <button onClick={handleClick}>增加</button>
         <div>{count}:{num.current}</div>
      </div>
   );
}

export default App;

实际应用

我们实现一个计时器的例子,在点击开始计数的时候,计时器会每300ms执行一次,在点击结束计数的时候,计时器会被清除。

问题

我们发现,点击end的时候,计时器并没有被清除,这是为什么呢?

原因

这是因为组件一直在重新ReRender,所以timer的值一直在被重新赋值为null,导致无法清除计时器。

tsx 复制代码
import React, { useLayoutEffect, useRef, useState } from 'react';

function App() {
   console.log('render')
   let timer: NodeJS.Timeout | null = null
   let [count, setCount] = useState(0)
   const handleClick = () => {
      timer = setInterval(() => {
         setCount(count => count + 1)
      }, 300)
   };
   const handleEnd = () => {
      console.log(timer); //点击end的时候,计时器并没有被清除
      if (timer) {
         clearInterval(timer)
         timer = null
      }
   };
   return (
      <div>
         <button onClick={handleClick}>开始计数</button>
         <button onClick={handleEnd}>结束计数</button>
         <div>{count}</div>
      </div>
   );
}

export default App;

如何修改?

我们可以使用useRef来解决这个问题,因为useRef的值不会因为组件的重新渲染而改变。

tsx 复制代码
import React, { useLayoutEffect, useRef, useState } from 'react';

function App() {
   console.log('render')
   let timer = useRef<null | NodeJS.Timeout>(null) // react里,定时器需要用uesRef定义
   let [count, setCount] = useState(0) 
   const handleClick = () => {
      timer.current = setInterval(() => {
         setCount(count => count + 1)
      }, 300)
   };
   const handleEnd = () => {
      if (timer.current) {
         clearInterval(timer.current)
         timer.current = null
      }
   };
   return (
      <div>
         <button onClick={handleClick}>开始计数</button>
         <button onClick={handleEnd}>结束计数</button>
         <div>{count}</div>
      </div>
   );
}

export default App;

注意事项

  1. 组件在重新渲染的时候,useRef的值不会被重新初始化。
  2. 改变 ref.current 属性时,React 不会重新渲染组件。React 不知道它何时会发生改变,因为 ref 是一个普通的 JavaScript 对象。
  3. useRef的值不能作为useEffect等其他hooks的依赖项,因为它并不是一个响应式状态。
  4. useRef不能直接获取子组件的实例,需要使用forwardRef。

!CAUTION

本文内容参考小满大佬

相关推荐
驭风少年君6 分钟前
《搭建属于自己的网站之网页前端学习》基础入门
前端·学习
刘一说1 小时前
深入理解 Spring Boot 嵌入式 Web 容器:从原理到性能调优
前端·spring boot·firefox
你的人类朋友1 小时前
设计模式的原则有哪些?
前端·后端·设计模式
!执行2 小时前
Web3 前端与合约交互
前端·web3·1024程序员节
潘小安2 小时前
跟着 AI 学(二)- Quill 接入速通
前端
十里-2 小时前
在 Vue2 中为 Element-UI 的 el-dialog 添加拖拽功能
前端·vue.js·ui
shada2 小时前
从Google Chrome商店下载CRX文件
前端·chrome
左耳咚2 小时前
项目开发中从补码到精度丢失的陷阱
前端·javascript·面试
黑云压城After2 小时前
vue2实现图片自定义裁剪功能(uniapp)
java·前端·javascript
芙蓉王真的好12 小时前
NestJS API 提示信息规范:让日志与前端提示保持一致的方法
前端·状态模式