超性感的React Hooks(四):useEffect

简介: 在function组件中,每当DOM完成一次渲染,都会有对应的副作用执行,useEffect用于提供自定义的执行内容,它的第一个参数(作为函数传入)就是自定义的执行内容。为了避免反复执行,传入第二个参数(由监听值组成的数组)作为比较(浅比较)变化的依赖,比较之后值都保持不变时,副作用逻辑就不再执行。如果读懂了,顺手给我点个赞,然后那么这篇文章到这里就可以完结了。

微信图片_20220509202706.gif


1


想不想验证一下自己的React底子到底怎么样?或者验证一下自己的学习能力?


这里有一段介绍useEffect的文字,如果你能够从中领悟到useEffect的用法,那么恭喜你,你应该大概率是个天赋型选手。


那么试试看:


在function组件中,每当DOM完成一次渲染,都会有对应的副作用执行,useEffect用于提供自定义的执行内容,它的第一个参数(作为函数传入)就是自定义的执行内容。为了避免反复执行,传入第二个参数(由监听值组成的数组)作为比较(浅比较)变化的依赖,比较之后值都保持不变时,副作用逻辑就不再执行。


如果读懂了,顺手给我点个赞,然后那么这篇文章到这里就可以完结了。如果没有读懂,也没有关系,一起来学习一下。


首先,我们要抛开生命周期的固有思维。


许多朋友试图利用class语法中的生命周期来类比理解useEffect,也许他们认为,hooks只是语法糖而已。那么,即使正在使用hooks,也有可能对我上面这一段话表示不理解,甚至还会问:不类比生命周期,怎么学习hooks?


我不得不很明确的告诉大家,生命周期和useEffect是完全不同的。


2


什么是副作用effect


本来吃这个药💊,我只是想治个感冒而已,结果感冒虽然治好了,但是却过敏了。过敏便是副作用。


本来我只是想渲染DOM而已,可是当DOM渲染完成之后,我还要执行一段别的逻辑。这一段别的逻辑就是副作用。


在React中,如果利用得好,副作用可以帮助我们达到更多目的,应对更为复杂的场景。


当然,如果hold不住,也会变成灾难。


hooks的设计中,每一次DOM渲染完成,都会有当次渲染的副作用可以执行。而useEffect,是一种提供我们能够自定义副作用逻辑的方式


3


一个简单的案例。


现在有一个counter表示数字,我希望在DOM渲染完成之后的一秒钟,counter数字加1


每个React组件初始化时,DOM都会渲染一次副作用:完成后的一秒钟,counter加1

结合这个思路,代码实现如下:


import React, { useState, useEffect } from 'react';
import './style.scss';
export default function AnimateDemo() {
  const [counter, setCounter] = useState(0);
  // DOM渲染完成之后副作用执行
  useEffect(() => {
    setTimeout(() => {
      setCounter(counter + 1);
    }, 1000);
  });
  return (
    <div className="container">
      <div className="el">{counter}</div>
    </div>
  )
}


代码很简单,DOM渲染完成,执行一次setTimeout


可是执行效果呢,意料之外!如下图。


微信图片_20220509202709.gif


结果counter不停的在累加,怎么会这样?


结合之前的规则,梳理一下原因


DOM渲染完成,副作用逻辑执行副作用逻辑执行过程中,修改了counter,而counter是一个state值state改变,会导致组件重新渲染


于是,这里就成为了一个循环逻辑。这也是我之前提到过的灾难


要避免这种灾难怎么办?从最初的那段话中已经提到过,可以利用useEffect的第二个参数来帮助我们。


当第二个参数传入空数组,即没有传入比较变化的变量,则比较结果永远都保持不变,那么副作用逻辑就只能执行一次。


useEffect(() => {
  setTimeout(() => {
    setCounter(counter + 1);
  }, 300);
}, []);

于是,我们达到了目的。


实践中有许多这种类似的场景。例如:组件第一次初始化渲染之后,我们需要再次渲染从接口过来的数据。


因为数据不能第一时间获取到,因此无法作为初始渲染数据


const [list, setList] = useState(0);
// DOM渲染完成之后副作用执行
useEffect(() => {
  recordListApi().then(res => {
    setList(res.data);
  })
// 记得第二个参数的使用
}, []);

4


继续深化一下使用场景。


如果除了在组件加载的那个时候会请求数据,在其他时刻,我们还想点击刷新或者下拉刷新数据,应该怎么办?


常规的思维是定义一个请求数据的方法,每次想要刷新的时候执行这个方法即可。而在hooks中的思维则不同:


创造一个变量,来作为变化值,实现目的的同时防止循环执行


代码如下:


import React, { useState, useEffect } from 'react';
import './style.scss';
export default function AnimateDemo() {
  const [list, setList] = useState(0);
  const [loading, setLoading] = useState(true);
  // DOM渲染完成之后副作用执行
  useEffect(() => {
    if (loading) {
      recordListApi().then(res => {
        setList(res.data);
      })
    }
  }, [loading]);
  return (
    <div className="container">
      <button onClick={() => setLoading(true)}>点击刷新</button>
      <FlatList data={list} />
    </div>
  )
}

注意观察loading的使用。这里使用了两个方式来阻止副作用与state引起的循环执行。


useEffect中传入第二个参数副作用逻辑内部自己判断状态


这一段需要结合实践经验理解,没有对应实践经验的可能会比较懵。以后回过头来理解也是可以的


5


再来看一眼文章头部的动态图。


微信图片_20220509202713.gif


想要实现的效果: 点击之后,执行第一段动画。 再之后的500ms,执行第二段动画

怎么办?


上一个例子中,我们人为的创建了一个变化量,来控制副作用逻辑的执行。这种方式在实践中非常有用。这个例子也可以借助这样的思维。重新梳理一下


变化量创建在state中通过某种方式(例如点击)控制变化量改变因为在state中,因此变化量改变,DOM渲染DOM渲染完成,副作用逻辑执行


那么根据这个思路,实现此案例的代码如下:


import React, { useState, useRef, useEffect } from 'react';
import anime from 'animejs';
import './style.scss';
export default function AnimateDemo() {
  const [anime01, setAnime01] = useState(false);
  const [anime02, setAnime02] = useState(false);
  const element = useRef<any>();
  useEffect(() => {
    anime01 && !anime02 && animate01();
    anime02 && !anime01 && animate02();
  }, [anime01, anime02]);
  function animate01() {
    if (element) {
      anime({
        targets: element.current,
        translateX: 400,
        backgroundColor: '#FF8F42',
        borderRadius: ['0%', '50%'],
        complete: () => {
          setAnime01(false);
        }
      })
    }
  }
  function animate02() {
    if (element) {
      anime({
        targets: element.current,
        translateX: 0,
        backgroundColor: '#FFF',
        borderRadius: ['50%', '0%'],
        easing: 'easeInOutQuad',
        complete: () => {
          setAnime02(false);
        }
      })
    }
  }
  function clickHandler() {
    setAnime01(true);
    setTimeout(setAnime02.bind(null, true), 500);
  }
  return (
    <div className="container" onClick={clickHandler}>
      <div className="el" ref={element} />
    </div>
  )
}

顺带使用useRef,比较简单,看一眼就能懂,详细的后续再介绍


6


受控组件


从广义上来理解:组件外部能控制组件内部的状态,则表示该组件为受控组件。


外部想要控制内部的组件,就必须要往组件内部传入props。而通常情况下,受控组件内部又自己有维护自己的状态。例如input组件。


也就意味着,我们需要通过某种方式,要将外部进入的props与内部状态的state,转化为唯一数据源。这样才能没有冲突的控制状态变化。


换句话说,就是要利用props,去修改内部的state。


这是受控组件的核心思维。


利用生命周期的实现方式我就不再介绍了,因为今天的主场是useEffect。


一起来试试看:


import React, { useState, useEffect } from 'react';
interface Props {
  value: number,
  onChange: (num: number) => any
}
export default function Counter({ value, onChange }: Props) {
  const [count, setCount] = useState<number>(0);
  useEffect(() => {
    value && setCount(value);
  }, [value]);
  return [
    <div key="a">{count}</div>,
    <button key="b" onClick={() => onChange(count + 1)}>
      点击+1
    </button>
  ]
}


正是本系列文章第一篇中的demo。是不是很简单。



7


最后一个至关重要的知识点,也是最难理解的一个点。


在hooks中是如何清除副作用的?


在生命周期函数中,我们使用componentWillUnmount来执行组件销毁之前想要做的事情。但是如果在hooks中,你用类比的方式来理解清除副作用,那么你可能永远都理解不了hooks的工作机制了。


一起来看看官网的案例


 useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
    };
  });


官网的介绍中,副作用回调函数中返回一个函数,用于副作用的清理工作。那么这个函数式什么时候执行的呢?与componentWillUnmount一样,整个过程中只执行一次吗?当然不是!


为了便于理解,将上面的代码稍微改造一下:


useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
  function clear() {
    ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
  }
  return clear;
});


假设在组件的使用过程中,外部传入的props参数id,改变了两次,第一次传入id: 1, 第二次传入id: 2


那么我们来梳理一下整个过程:


1.传入props.id = 12.组件渲染3.DOM渲染完成,副作用逻辑执行,返回清除副作用函数clear,命名为clear14.传入props.id = 25.组件渲染6.组件渲染完成,clear1执行7.副作用逻辑执行,返回另一个clear函数,命名为clear28.组件销毁,clear2执行

执行过程有点绕,因为与你印象中的执行过程似乎不一样。其实关键的地方就在于clear函数的执行,它的特征如下:


每次副作用执行,都会返回一个新的clear函数clear函数会在下一次副作用逻辑之前执行(DOM渲染完成之后)组件销毁也会执行一次


理解了这个特点,对于useEffect的使用你应该已经领先大多数人了。


关键我们要思考的是:clear1执行的时候,访问了props.id,那么这个props.id的值是神马呢, 1还是2?


这又是为什么?


如果想不明白,回过头去看看我的文章中,关于闭包的讲解。



8


一个思考题:下面代码中,console.log的打印顺序会是怎么样的?


import React, { useState, useEffect } from 'react';
import './style.scss';
export default function AnimateDemo() {
  const [counter, setCounter] = useState(0);
  useEffect(() => {
    const timer = setTimeout(() => {
      setCounter(counter + 1);
    }, 300);
    console.log('effect:', timer);
    return () => {
      console.log('clear:', timer);
      clearTimeout(timer);
    }
  });
  console.log('before render');
  return (
    <div className="container">
      <div className="el">{counter}</div>
    </div>
  )
}


9


关于useEffect,还有另外一个知识点。


试想:如果副作用逻辑太复杂了怎么办?为了更好的控制副作用逻辑的执行,我们不得不传入大量的变化值变量。


useEffect(() => {
    // todo
  }, [index, counter, pand, corder, max, min, zindex]);


明显这样的代码并不优雅,非常容易出错。


react hooks 提供了一种解耦方案,我们可以使用多个useEffect来执行不同的副作用逻辑。


调整一下之前的一个案例。


import React, { useState, useRef, useEffect } from 'react';
import anime from 'animejs';
import './style.scss';
export default function AnimateDemo() {
  const [anime01, setAnime01] = useState(false);
  const [anime02, setAnime02] = useState(false);
  const element = useRef<any>();
  useEffect(() => {
    anime01 && animate01();
  }, [anime01]);
  useEffect(() => {
    anime02 && animate02();
  }, [anime02]);
  function animate01() {
    if (element) {
      anime({
        targets: element.current,
        translateX: 400,
        backgroundColor: '#FF8F42',
        borderRadius: ['0%', '50%'],
        complete: () => {
          setAnime01(false);
        }
      })
    }
  }
  function animate02() {
    if (element) {
      anime({
        targets: element.current,
        translateX: 0,
        backgroundColor: '#FFF',
        borderRadius: ['50%', '0%'],
        easing: 'easeInOutQuad',
        complete: () => {
          setAnime02(false);
        }
      })
    }
  }
  function clickHandler() {
    setAnime01(true);
    setTimeout(setAnime02.bind(null, true), 500);
  }
  return (
    <div className="container" onClick={clickHandler}>
      <div className="el" ref={element} />
    </div>
  )
}


重点关注useEffect的变化,你会发现,逻辑更简单了,实现了同样的效果。


这样的解耦方案,能够更方便的让我们管理复杂代码逻辑。避免相互之间的干扰。


useEffect表面上看起来简单,但使用起来一点也不简单。更多的知识,在接下来的文章中,结合其他案例理解。

相关文章
|
22天前
|
前端开发 JavaScript API
探究 React Hooks:如何利用全新 API 优化组件逻辑复用与状态管理
本文深入探讨React Hooks的使用方法,通过全新API优化组件逻辑复用和状态管理,提升开发效率和代码可维护性。
|
24天前
|
前端开发
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
21 3
|
29天前
|
前端开发 JavaScript
深入探索React Hooks:从useState到useEffect
深入探索React Hooks:从useState到useEffect
|
1月前
|
前端开发 JavaScript 开发者
“揭秘React Hooks的神秘面纱:如何掌握这些改变游戏规则的超能力以打造无敌前端应用”
【10月更文挑战第25天】React Hooks 自 2018 年推出以来,已成为 React 功能组件的重要组成部分。本文全面解析了 React Hooks 的核心概念,包括 `useState` 和 `useEffect` 的使用方法,并提供了最佳实践,如避免过度使用 Hooks、保持 Hooks 调用顺序一致、使用 `useReducer` 管理复杂状态逻辑、自定义 Hooks 封装复用逻辑等,帮助开发者更高效地使用 Hooks,构建健壮且易于维护的 React 应用。
34 2
|
2月前
|
前端开发 开发者
React 提供的其他重要 Hooks
【10月更文挑战第20天】React 提供了一系列强大的 Hooks,除了 `useRef` 之外,还有许多其他重要的 Hooks,它们共同构成了函数式组件开发的基础。
37 6
|
18天前
|
前端开发 JavaScript
React Hooks 深入解析
React Hooks 深入解析
20 0
|
18天前
|
前端开发
React Hooks:从基础到进阶的深入理解
React Hooks:从基础到进阶的深入理解
25 0
|
21天前
|
缓存 前端开发 开发者
深入理解React Hooks,打造高效响应式UI
深入理解React Hooks,打造高效响应式UI
29 0
|
2月前
|
前端开发 JavaScript 开发者
React Hooks
10月更文挑战第13天
37 1
|
1月前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
68 9