想不想验证一下自己的React底子到底怎么样?或者验证一下自己的学习能力?
这里有一段介绍useEffect的文字,如果你能够从中领悟到useEffect
的用法,那么恭喜你,你应该大概率是个天赋型选手。
那么试试看:
在function组件中,每当DOM完成一次渲染,都会有对应的副作用执行,useEffect用于提供自定义的执行内容,它的第一个参数(作为函数传入)就是自定义的执行内容。为了避免反复执行,传入第二个参数(由监听值组成的数组)作为比较(浅比较)变化的依赖,比较之后值都保持不变时,副作用逻辑就不再执行。
如果读懂了,顺手给我点个赞,然后那么这篇文章到这里就可以完结了。如果没有读懂,也没有关系,一起来学习一下。
首先,我们要抛开生命周期的固有思维。
许多朋友试图利用class语法中的生命周期来类比理解useEffect,也许他们认为,hooks只是语法糖而已。那么,即使正在使用hooks,也有可能对我上面这一段话表示不理解,甚至还会问:不类比生命周期,怎么学习hooks?
我不得不很明确的告诉大家,生命周期和useEffect是完全不同的。
什么是副作用effect
本来吃这个药?,我只是想治个感冒而已,结果感冒虽然治好了,但是却过敏了。过敏便是副作用。
本来我只是想渲染DOM而已,可是当DOM渲染完成之后,我还要执行一段别的逻辑。这一段别的逻辑就是副作用。
在React中,如果利用得好,副作用可以帮助我们达到更多目的,应对更为复杂的场景。
当然,如果hold不住,也会变成灾难。
hooks的设计中,每一次DOM渲染完成,都会有当次渲染的副作用可以执行。而useEffect,是一种提供我们能够自定义副作用逻辑的方式
一个简单的案例。
现在有一个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);
})
// 记得第二个参数的使用
}, []);
继续深化一下使用场景。
如果除了在组件加载的那个时候会请求数据,在其他时刻,我们还想点击刷新或者下拉刷新数据,应该怎么办?
常规的思维是定义一个请求数据的方法,每次想要刷新的时候执行这个方法即可。而在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中传入第二个参数•副作用逻辑内部自己判断状态
这一段需要结合实践经验理解,没有对应实践经验的可能会比较懵。以后回过头来理解也是可以的
再来看一眼文章头部的动态图。
想要实现的效果: 点击之后,执行第一段动画。 再之后的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,比较简单,看一眼就能懂,详细的后续再介绍
从广义上来理解:组件外部能控制组件内部的状态,则表示该组件为受控组件。
外部想要控制内部的组件,就必须要往组件内部传入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。是不是很简单。
最后一个至关重要的知识点,也是最难理解的一个点。
在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?
这又是为什么?
如果想不明白,回过头去看看我的文章中,关于闭包的讲解。
一个思考题:下面代码中,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>
)
}
关于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,我写了一本书,感兴趣的同学可点击阅读原文查看详情。