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 = 1
2.组件渲染3.DOM渲染完成,副作用逻辑执行,返回清除副作用函数clear,
命名为clear1
4.传入props.id = 2
5.组件渲染6.组件渲染完成,clear1执行7.副作用逻辑执行,返回另一个clear函数,命名为clear2
8.组件销毁,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表面上看起来简单,但使用起来一点也不简单。更多的知识,在接下来的文章中,结合其他案例理解。