四、useEffect和useLayoutEffect深入学习超大章

一、介绍

useEffect是hooks中又一个重要的函数。Effect hooks允许你在组件内部中执行副作用操作。

副作用包括:

  • 数据获取
  • 设置订阅
  • 手动更改DOM等等

useEffect就是为了处理这些副作用而被创造出来的函数,它相当于class中componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

二、基本使用

useEffect(()=>{},[])

useEffect 有两个参数。第一个参数是Effect,第二个参数是依赖值列表。

组件的每一次渲染都会产生一个新的Effect,因为这个Effect每一次渲染拿到的prop和state是不同的。

第二次参数是依赖值列表。

  • 当不传递的时候,useEffect会在每次渲染都执行
  • 当传递一个[]的时候,那么useEffect只会在挂载的时候执行(如果useEffect返回了一个函数,那么这个函数会在组件卸载的时候执行)
  • 当传递一个值的时候,如[a],那么useEffect就会根据这个值是否发生变化,来判断是否去执行useEffect函数
  • 当传递多个值的时候,如[a,b,c…],那么他就是会比较每一个值,有一个不相同就回去执行

三、两种副作用

在React中有两种常见的副作用操作:需要被清除和不需要被清除的。

无需清除的副作用:

有时候,我们只想在React和更新DOM操作之后运行一些额外的代码。比如说,发送网络请求、手动变更DOM、记录日志,这些都是常见的无需清除副作用的操作。因为我们在执行完这些操作之后,就可以忽略他们了。

class实现:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
 
  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

hooks实现:

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

从上述的代码中可以看出:

​ 在class组件中 在俩个生命周期函数中都设置了domcument.title,这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。

​ 而在hooks中只是在useEffect函数的回调函数中设置了一次document.title。

  • useEffect 做了什么?

    ​ 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

  • 为什么在组件内部调用 useEffect

    ​ 将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

  • useEffect 会在每次渲染后都执行吗?

    ​ 默认情况下,它在第一次渲染之后每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

    ​ 上述代码中,我们声明了 count state 变量,并告诉 React 我们需要使用 effect。紧接着传递函数给 useEffect Hook。此函数就是我们的 effect。然后使用 document.title 浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 count 值,因为他在函数的作用域内。当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。这个过程在每次渲染时都会发生,包括首次渲染。

    传递给 useEffect 的函数在每次渲染中都会有所不同,也就是useEffect的第一个参数。这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染

    ​ **注意:**与 componentDidMountcomponentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快(用户体验好)。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。

需要清除的副作用
  • 订阅外部的数据源
    • 这种情况下,清除副作用是十分重要的,可以防止内存泄露

Class:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }
 
  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
    );
}
    componentWillUnmount() {    
        ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id, this.handleStatusChange);
}
handleStatusChange(status) {   
     this.setState({isOnline: status.isOnline});
}
  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

Hooks:

import React, { useState, useEffect } from 'react';
 
function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
 
  useEffect(() => {
     function handleStatusChange(status) {
        setIsOnline(status.isOnline);    
     }    
     ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);    // Specify how to clean up after this effect:    
     return function cleanup() {              
           ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    
        };  
    });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

在class组件中,我们需要在componentDidMount生命周期函数中发起订阅,然后在componentWillMount生命周期中取消订阅。他们是相对应的,使得生命周期函数迫使我们拆分这些逻辑代码,即使这两部分都作用于相同的副作用。

而在hooks中,useEffect就看起来直观多了。在useEffect函数中返回一个函数就可以做到清除副作用。

为什么要在effect函数中返回一个函数?

​ 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

React 何时清除effect?

​ React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 在执行当前 effect 之前对上一个 effect 进行清除。

**注意:**并不是必须为 effect 中返回的函数命名。这里我们将其命名为 cleanup 是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。

使用多个effect可以分离关注点。

在class组件有多个副作用函数的时候,这些副作用函数会按照生命周期的不同,将代码逻辑分割放到不同的生命周期函数中。

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }
 
  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
 
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }
 
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
 
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

而如果在hooks中,它允许我们允许我们按照代码的用途分离他们,这样我们就可以将关注点分离,各自维护各自的代码逻辑。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {    document.title = `You clicked ${count} times`;
  });
 
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
 
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

为什么每次更新都要运行useEffect函数:

​ 这是因为在class组件中当props发生变化的时候,组件会继续展示原来的样子,而且因为在componentWillMount生命周期函数中做的操作导致内存泄漏。所以我们还需要一个componentDidUpdate函数来解决这个问题。但是在useEffect hooks函数中完全不必担心这些问题。并不需要特定的代码来处理更新逻辑,因为 useEffect 默认就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。

跳过Effect进行性能优化:

讲到性能优化,我们就要说说useEffect的第二参数,一个参数列表。我们知道在class组件中我们可以通过在 componentDidUpdate 中添加对 prevPropsprevState 的比较逻辑解,去做性能优化,这是很常见的需求,所以它被内置到了 useEffect 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); 

上述代码表示,这个useEffect函数的执行需要依赖于count值,如果count值没有发生变化,那么通知到React,跳过对该该函数的执行。

  • 如果希望useEffect只执行一次,也就是只在挂载的时候执行。那么我们就可以传递一个空数组作为第二个参数。这就告诉React不依赖于任何任何props和state,所以它永远都不会重复执行。effect 内部的 props 和 state 就会一直拥有其初始值,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得额外操作很方便。
//执行一次
useEffect(()=>{
  ...
},[])
  • 如果想useEffect,只在挂在和卸载时执行。那么可以给useEffect返回一个函数。return返回这个函数叫做清理函数,通常用于清除一些这个清理函数会在组件卸载的时候执行。
useEffect(()=>{
  console.log("挂载时候执行")
  return ()=>{
    consloe.log("卸载的时候执行")
  }
},[])
  • 如果想使用useEffect获取数据
function Reddit() {
  const [posts, setPosts] = useState([]);

  useEffect(async () => {
    const res = await fetch(
      "https://www.reddit.com/r/reactjs.json"
    );

    const json = await res.json();

    setPosts(json.data.children.map(c => c.data));
  }); // 这里没有传入第二个参数,你猜猜会发生什么?

注意到咱们没有将第二个参数传递给useEffect,在语法上可行,但在开发中,不能这样做

​ 不传递第二个参数会导致每次渲染都会运行useEffect。然后,当它运行时,它获取数据并更新状态。然后,一旦状态更新,组件将重新呈现,这将再次触发useEffect,这就是问题所在。

​ 为了解决这个问题,我们需要传递一个数组作为第二个参数,数组内容又是啥呢。

useEffect所依赖的唯一变量是setPosts。因此,咱们应该在这里传递数组[setPosts]。因为setPostsuseState返回的setter,所以不会在每次渲染时重新创建它,因此effect只会运行一次。

​ 接着扩展一下示例,以涵盖另一个常见问题:如何在某些内容发生更改时重新获取数据,例如用户ID,名称等。

​ 首先,咱们更改Reddit组件以接受subreddit作为一个prop,并基于该subreddit获取数据,只有当 prop 更改时才重新运行effect.

const [posts, setPosts] = useState([]);

useEffect(async () => {
  const res = await fetch(
    `https://www.reddit.com/r/${subreddit}.json`
  );

  const json = await res.json();
  setPosts(json.data.children.map(c => c.data));

  // 当`subreddit`改变时重新运行useEffect:
}, [subreddit, setPosts]);

四、useLayoutEffect

​ 它的函数签名与useEffect相同,但它会在所有的DOM变更之后同步调用effect,可以使用它来读取DOM布局兵同步触发渲染。在浏览器执行绘制之前,useLayoutEffect内部的更新计划将被同步刷新。尽可能使用标准的useEffect以避免阻塞视觉更新。

​ 如果你正在将代码从 class 组件迁移到使用 Hook 的函数组件,则需要注意 useLayoutEffectcomponentDidMountcomponentDidUpdate 的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect

​ 如果你使用服务端渲染,请记住,无论 useLayoutEffect 还是 useEffect 都无法在 Javascript 代码加载完成之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect 代码时会触发 React 告警。解决这个问题,需要将代码逻辑移至 useEffect 中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect 执行之前 HTML 都显示错乱的情况下)。

​ 若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,可以通过使用 showChild && <Child /> 进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, []) 延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。

五、区别详解

useEffect是异步执行,而且是在渲染被绘制到屏幕之后执行。
流程如下:

  • 你以某种方式触发了rerender(改变state,或者父组件发生rerender)
  • React渲染你的组件(调用组件函数)
  • 屏幕在视觉上更新(真实dom操作)
  • 然后useEffect运行

useLayoutEffect是同步执行,时机在渲染之后但在屏幕更新之前。

流程如下:

  • 你以某种方式触发了rerender(改变state,或者父组件发生rerender)
  • React渲染你的组件(调用组件函数)
  • useLayoutEffect运行,React等待它完成
  • 屏幕在视觉上更新(真实dom操作)

总结:

​ 基本上90%的情况下,都应该用useEffect,这个是在render结束后,你的callback函数执行,但是不会影响浏览器的绘制,是异步的。但是class的componentDidMount 和componentDidUpdate是同步的,在render结束后就运行,useEffect在大部分场景下都比class的方式性能更好.

​ useLayoutEffect是用在处理DOM的时候,当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用useLayoutEffect,否则可能会出现闪屏问题, useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制.

import React, { useEffect, useLayoutEffect, useRef } from "react";
import TweenMax from "gsap/TweenMax";
import './index.less';

const Animate = () => {
    const REl = useRef(null);
    useEffect(() => {
        /*下面这段代码的意思是当组件加载完成后,在0秒的时间内,将方块的横坐标位置移到600px的位置*/
        TweenMax.to(REl.current, 0, {x: 600})
    }, []);
    return (
        <div className='animate'>
            <div ref={REl} className="square">square</div>
        </div>
    );
};

export default Animate;

上述代码在运行的时候,会看到一个一闪而过的方块。说明组件先会render一次,然后再去执行useEffect里面的代码。改成useLayoutEffect之后,浏览器会等到useLayoutEffect里面的函数执行完毕之后才会去渲染浏览器。

  • useLayoutEffect总是比useEffect先执行
  • useLayoutEffect里的任务最好影响了Layout
  • 为了用户体验,优先使用useEffect(优先渲染)
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
useEffectuseLayoutEffectReact中的两个钩子函数,它们用于在组件渲染完成后执行一些副作用操作。它们的主要区别在于执行时机和对页面渲染的影响。 useEffect的回调函数会在浏览器执行绘制之后被调用,它是异步执行的,不会阻塞渲染过程。因此,如果副作用操作不涉及对DOM的操作或页面交互,或者可以接受一定的延迟,可以使用useEffectuseLayoutEffect的回调函数会在DOM更新之后,浏览器执行绘制之前被调用。它在执行回调函数时会产生阻塞效果,可能导致页面感觉卡顿。因此,如果副作用操作需要操作DOM或进行与界面交互相关的操作,可以考虑使用useLayoutEffect。 综上所述,优先使用useEffect,因为它是异步执行的,不会阻塞渲染。只有当需要进行DOM操作或与界面交互的操作时,才考虑使用useLayoutEffect。但需要注意,useLayoutEffect在服务端渲染时可能会导致首屏实际内容和服务端渲染出来的内容不一致,会有一个警告。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [useEffectuseLayoutEffect深入学习超大](https://blog.csdn.net/NinthMonee/article/details/113124449)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [useEffectuseLayoutEffect的基础知识和底层机制](https://blog.csdn.net/qq_52563729/article/details/131155613)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [useEffectuseLayoutEffect](https://blog.csdn.net/qq_37548296/article/details/126471059)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码搬运工_田先森

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值