超性感的React Hooks(四):useEffect

时间:2022-07-22
本文章向大家介绍超性感的React Hooks(四):useEffect,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

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

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

结果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

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

想要实现的效果: 点击之后,执行第一段动画。 再之后的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表面上看起来简单,但使用起来一点也不简单。更多的知识,在接下来的文章中,结合其他案例理解。

本系列文章的所有案例,都可以在下面的地址中查看

https://github.com/advance-course/react-hooks

本系列文章为原创,请勿私自转载,转载请务必私信我

关于如何学好JavaScript,我写了一本书,感兴趣的同学可点击阅读原文查看详情。