目 录CONTENT

文章目录

温故而知新-useEffect和useLayoutEffect的区别

Hello!你好!我是村望~!
2024-12-22 / 0 评论 / 0 点赞 / 3 阅读 / 1,591 字
温馨提示:
我不想探寻任何东西的意义,我只享受当下思考的快乐~

useEffect

useEffect 干嘛的

函数组件是纯函数,传入 props,返回对应的结果,再次调用,传入 props,依然返回同样的结果。

但现在有了 effect 之后,每次执行函数,额外执行了一些逻辑。例如网络请求

import { useEffect, useState } from 'react'
import './App.css'

function App() {
  const [data, setData] = useState<number>(0)
  function queryData(): Promise<number> {
    return new Promise(resolve => {
      let timer: number | null = setTimeout(() => {
        resolve(200)
        if (timer) {
          clearTimeout(timer)
          timer = null
        }
      }, 1000)
    })
  }
  useEffect(() => {
    queryData().then(r => {
      setData(r)
    })
  }, [])
  return (
    <>
      {data}
    </>
  )
}

export default App

注意,想用 async await 语法需要单独写一个函数,因为 useEffect 参数的那个函数不支持 async

 type EffectCallback = () => void | Destructor;

上面的代码就是执行函数组件的时候,额外去请求数据!然后set到data中去渲染

iShot_2024-12-22_11.37.25.gif

useEffect的第二个参数叫做依赖数组

react 是根据它有没有变来决定是否执行 effect 函数的,如果没传则每次都执行。

import { useEffect, useState } from 'react'
import './App.css'

function App() {
  const [data, setData] = useState<number>(0)
  function addData() {
    setData(data + 1)
  }
  useEffect(() => {
    console.log("effect!!!")
  })
  
  return (
    <>
      <button onClick={addData}>addData</button>
      {data}
    </>
  )
}

export default App

也可以写任意的常量,因为它们都不变,所以不会触发 effect 的重新执行:

useEffect(() => {
  queryData().then(r => {
    setData(r)
  })
}, [1, 2, 3])

但如果其中有个变化的值,那就会触发重新执行了

useEffect(() => {
  console.log("effect!!!")
}, [1, 2, 3, new Date()])

这个数组我们一般写依赖的 state,这样在 state 变了之后就会触发重新执行了。

清除副作用(Clean - up)函数

当组件挂载、更新或卸载时,可能会有一些操作需要被执行,如订阅事件、设置定时器等,这些都是副作用。

当组件卸载或者重新执行useEffect(由于依赖项变化)时(执行),就需要清除之前的副作用。

以定时器为例,假设在组件挂载时设置了一个定时器:

import { useEffect, useState } from 'react'
import './App.css'

function App() {
  const [data, setData] = useState<number>(0)
  function addData() {
    setData(data + 1)
  }
  useEffect(() => {
    let timer = setInterval(() => {
      console.log(timer + ':', data * 2)
    }, 1000)
  }, [data])

  return (
    <>
      <button onClick={addData}>addData</button>
      <p>{data}</p>
    </>
  )
}

export default App

data 变化的时候,每一秒打印一次当前data*2后的值

点击多次后,会发现很多个定时器在执行。那么当effect重新执行的时候,就要清除本次的内部定时器 (这个return clean up 应该是 effect要重新执行的时候才会调用)

useEffect(() => {
  let timer = setInterval(() => {
    console.log(timer + ':', data * 2)
  }, 1000)
  return () => {
    clearInterval(timer)
  }
}, [data])

当依赖数组的data变化的时候会重新执行effect,当effect重新执行的时候,会执行当前effect的clean up 函数,清楚当前的定时器!

useLayoutEffect

和 useEffect 类似的还有一个 useLayoutEffect。那么这两者有什么区别呢!

1. 执行时机

useEffect:它是异步执行的。在浏览器完成渲染之后才会执行useEffect中的代码。具体来说,当 React 更新 DOM 后,会等待浏览器下一次重绘(repaint)完成后,才会去执行useEffect。这意味着如果useEffect中有修改 DOM 的操作,会触发额外的重绘。

useLayoutEffect是同步执行,会阻塞浏览器的渲染,直到useLayoutEffect中的操作完成。所以如果在useLayoutEffect中有复杂或者耗时的操作,会导致页面渲染延迟,用户可能会感觉到页面加载变慢。

import React, { useState, useLayoutEffect, useEffect } from 'react';
const App = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
  const buttonRef = React.createRef();
  const handleClick = () => {
    setIsOpen(!isOpen);
  };
  useEffect(() => {
    if (isOpen && buttonRef.current) {
      const buttonRect = buttonRef.current.getBoundingClientRect();
      const top = buttonRect.bottom;
      const left = buttonRect.left;
      setMenuPosition({ top, left });
    }
  }, [isOpen]);
  return (
    <div>
      <button ref={buttonRef} onClick={handleClick}>打开菜单</button>
      {isOpen && (
        <div className="menu" style={{ position: 'absolute', top: menuPosition.top + 'px', left: menuPosition.left + 'px' }}>
          <p>菜单选项1</p>
          <p>菜单选项2</p>
          <p>菜单选项3</p>
        </div>
      )}
    </div>
  );
};
export default App;

iShot_2024-12-22_16.15.14

上面是useEffect的效果,dom渲染和useEffect的执行是异步执行的,上面闪动的情况是因为dom先渲染完成了。

menu 的 top和left都是0,所以一开始出现在最上面,然后effect执行完成后,重新set Top Left 重新渲染了新的位置。所以就出现了闪动的情况。

如果使用useLayoutEffect就可以解决闪动的问题,因为会阻塞dom的渲染优先计算出top left 直接渲染正确的位置。

2. 性能影响

useEffect,因为是异步执行,所以通常不会阻塞浏览器的渲染。对于那些不影响用户看到的初始渲染效果的副作用操作,如数据获取、订阅事件(这些操作不涉及布局相关内容),使用useEffect是比较合适的,它可以让浏览器先把基本的页面内容渲染出来,再去处理这些副作用,有利于提高初始渲染的性能。

不过,如果在useEffect中有大量的操作或者频繁触发useEffect(由于依赖项频繁变化),可能会导致频繁的重绘,这在一定程度上会影响性能。

useLayoutEffect,由于是同步执行,会阻塞浏览器的渲染,直到useLayoutEffect中的操作完成。所以如果在useLayoutEffect中有复杂或者耗时的操作,会导致页面渲染延迟,用户可能会感觉到页面加载变慢。

但是,对于一些对布局精准性要求很高的操作,如计算一个复杂的布局元素的位置或者尺寸,并且这个结果需要在渲染时就准确呈现给用户,useLayoutEffect就很合适,因为它能保证布局的准确性。

0

评论区