React FC

FC 就是 Function Component.也就是函数式组件.

不变的

函数式组件每一次渲染都会存储当前的状态,他是不变的.
每一次渲染都有自己的 pops 和 state,也有自己的事件处理函数.

1
<p>you click {count} times</p>

当我们更新状态时,React 会重新渲染组件.每一次渲染都能拿到独立的 count 状态,这个状态是函数中的一个常量.
任意一次渲染中的 count 常量都不会随着时间改变.渲染输出会变是因为我们的组件被一次次调用.
而每次调用引起的渲染中,它包含的 count 值独立于其他渲染.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function App() {
const [count, setCount] = useState(0);

const showMessage = () => {
setTimeout(() => {
alert("Now count: " + count);
}, 3000);
};

return (
<div>
     <p>You clicked {count} times</p>     <button
onClick={() => setCount(count + 1)}
>
Click me
</button>     <button onClick={showMessage}>Print</button>   
</div>
);
}

点击 Print 后,再点击 click,打印的是点击 Print 时的 count 值,因为 count 是独立的,拿到的是那时候的.

useEffect 的依赖项

依赖项里进行的比较是浅比较。

无限循环

什么可能会造成无限循环呢?

  1. 没有依赖项
  2. 依赖项是函数,对象,数组
  3. 依赖项不正确

当依赖项是函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
const [count, setCount] = useState(0);

function logResult() {
return 2 + 2;
}
useEffect(() => {
setCount((count) => count + 1);
}, [logResult]); // 函数作为依赖项
return (
<div className="App">
<p> value of count: {count} </p>
</div>
);
}

看上去,应该是只在第一次渲染时增加 count。实际上会报错。
因为,依赖项进行的是浅比较,每次 render 都会重新定义函数的引用。就会触发 useEffect 的更新,进入无限循环。

解决

使用 useCallback。

1
2
3
4
5
6
const logResult = useCallback(() => {
return 2 + 2;
}, []);
useEffect(() => {
setCount((count) => count + 1);
}, [logResult]);

当依赖项是对象

1
2
3
4
5
6
7
8
9
10
11
12
const [count, setCount] = useState(0);
const person = { name: "Rue", age: 17 }; //创建一个对象
useEffect(() => {
// 每次增加count的值
// person的值发生了变化
setCount((count) => count + 1);
}, [person]); // 依赖项数组包含一个对象作为参数
return (
<div className="App">
<p> Value of {count} </p>
</div>
);

同样和依赖项是函数一样,依赖项是对象也因为浅比较,每次 render 都重新得到一个新的对象,出现无限循环的问题。

解决

使用 useMemo,确保对象在每次渲染期间不会改变;

1
2
3
4
const person = useMemo(() => ({ name: "Tom", age: 10 }), []);
useEffect(() => {
setCount((count) => count + 1);
}, [count]);

疑问: 如果是 useState 保存的对象作为依赖,该如何处理?

当依赖项是数组

1
2
3
4
5
6
const [count, setCount] = useState(0); //初始值为0。
const myArray = ["one", "two", "three"];

useEffect(() => {
setCount((count) => count + 1); // 和前面一样,增加Count的值
}, [myArray]); // 将数组变量传递给依赖项

无限循环的原因与上面无异

解决

使用 useRef。

1
2
3
4
5
6
const [count, setCount] = useState(0);
const { current: myArrray } = useRef(["one", "two", "three"]);

useEffect(() => {
setCount((count) => count + 1);
}, [myArray]);

注意

无限循环问题是在 useEffect 中 setState 了新值导致的,在其他地方是不会无限循环的。

Option 1 - 依赖于对象属性

在这个例子中,当前组件会从 props 获取一个对象,并在 useEffect 的依赖数组中使用这个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useEffect } from "react";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ team }) => {
const [players, setPlayers] = useState([]);

useEffect(() => {
if (team.active) {
getPlayers(team.id).then(setPlayers);
}
}, [team]);

return <Players team={team} players={players} />;
};

理想情况下,props 传入的 team 的内容是一样的话,其内存地址也会相同。但这其实是无法保证的。

所以,为了解决这个问题,我们可以只使用 team 对象里的一些属性,而不是使用整个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useEffect } from "react";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ team }) => {
const [players, setPlayers] = useState([]);

useEffect(() => {
if (team.active) {
getPlayers(team.id).then(setPlayers);
}
}, [team.id, team.active]);

return <Players team={team} players={players} />;
};

假设 team 对象中的 id 和 active 属性都是基本数据类型,effect 就只会在 id 或者 active 属性发生变化的时候执行(注意是或,不是且)。

team 对象如果是在组件内被创建的话也能够起到作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState, useEffect } from "react";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ id, name, active }) => {
// construct the object from props and/or state
const team = { id, name, active };
const [players, setPlayers] = useState([]);

useEffect(() => {
if (team.active) {
getPlayers(team.id).then(setPlayers);
}
}, [team.id, team.active]);

return <Players team={team} players={players} />;
};

所以,即使 team 对象在每次渲染过程中都重新被创建,也不会导致 effect 每次渲染都会执行,因为 useEffect 只依赖于 id 和 active 属性。

需要注意的是,这个方案不适用于依赖元素为数组的情况。

Option2 - 在内部创建对象

在上一个例子中,如果 effect 不是使用对象里的元素,而是以整个对象作为依赖会发生什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState, useEffect } from "react";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ id, name, active }) => {
// construct the object from props/state
const team = { id, name, active };
const [players, setPlayers] = useState([]);

useEffect(() => {
if (team.active) {
getPlayers(team).then(setPlayers);
}
}, [team]);

return <Players team={team} players={players} />;
};

team 对象是在每次渲染下重新创建的,所以 useEffect 在每次重新渲染时都会执行。

幸运的是,react-hooks/exhaustive-depsESLint rule 提示道:

The 'team' object makes the dependencies of useEffect Hook change on every render. To fix this, wrap the initialization of 'team' in its own useMemo() Hook.

在我们使用 ESLint 推荐的使用 useMemo Hook 之前,我们可以尝试更加简单的操作。我们可以尝试两次创建 team 对象,一个用于传递给 Player 子组件,一个用于 useEffect 内部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useEffect } from "react";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ id, name, active }) => {
const [players, setPlayers] = useState([]);

useEffect(() => {
// recreate the `team` object within `useEffect`
// from props/state
const team = { id, name, active };

if (team.active) {
getPlayers(team).then(setPlayers);
}
}, [id, name, active]);

const team = { id, name, active };

return <Players team={team} players={players} />;
};

现在 team 对象是在 useEffect 内部被创建,并且只在 effect 要被执行的时候被创建。id、name、active 作为依赖,只有当这些值发生变化的时候 effect 才会执行。

创建对象相对来说开销是比较小的,所以在 useEffect 中重新创建一个 team 对象是可以接受的。
优化 useEffect 所带来的性能提升远远大于创建两个对象所带来的性能损耗。

Option3 - 记忆对象

然而,如果创建对象或数组的开销是昂贵的,那重复创建对象就会比执行多次 effect 更糟糕。在这种情况下,我们需要“缓存”创建的对象或数组,这样当其中的数据没有发生变化时,这个对象或数组就不会在渲染过程中发生变化。这个过程称为“记忆”(memoization),我们通过 useMemo 来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState, useEffect, useMemo } from "react";
import { createTeam } from "../utils";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ id, name, active }) => {
// memoize calling `createTeam` because it's
// an expensive operation
const team = useMemo(
() => createTeam({ id, name, active }),
[id, name, active]
);
const [players, setPlayers] = useState([]);

useEffect(() => {
if (team.active) {
getPlayers(team).then(setPlayers);
}
}, [team]);

return <Players team={team} players={players} />;
};

假设这个 createTeam()方法开销昂贵,那我们自然会希望它执行的次数越少越好。useMemo Hook 可以实现只有在 id、name 或者 active 在渲染过程中发生变化时,才会再次创建 team。但如果在 Team 组件重新渲染过程中,以上属性没有一个发生变化,team 对象就会是同一个对象。因为是同一个对象,我们就可以放心地使用 useEffect,不用担心会执行不必要的次数。

Option 4 - 自己创建

如果以上方案都无法解决问题怎么办?比如从 props 中传入了对象或者数组,这个对象或数组会成为 useEffect 的依赖数组元素,而且我们并不知道创建一个新的 team 对象所需的属性。但我们还是想要实现在组件渲染过程中“缓存”这个对象的值。

在这种情况下,我们可以使用 useRef Hook 替代 useMemo Hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useEffect, useRef } from "react";
import isDeepEqual from "fast-deep-equal/react";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ team }) => {
const [players, setPlayers] = useState([]);
const teamRef = useRef(team);

if (!isDeepEqual(teamRef.current, team)) {
teamRef.current = team;
}

useEffect(() => {
if (team.active) {
getPlayers(team).then(setPlayers);
}
}, [teamRef.current]);

return <Players team={team} players={players} />;
};

这里使用了 isDeepEqual 来判断 teamRef.current 和 team 的值是否一致,而不是比较两者的内存地址。所以,即使在每次渲染过程中 team 对象是一个新的对象,如果它的内容是一致的,isDeepEqual()也会返回 true。

所以当两者在做深比较的时候,如果内容一致,isDeepEqual 会返回 true,teamRef.current 会继续指向原本 team 对象的内存地址。那依赖数组里的元素 teamRef.current 就没有发生变化,useEffect 也不会再执行。如果 isDeepEqual 返回 false,teamRef.current 就会被赋予新的 team 值,effect 就会执行。

如果你发现自己遇到了 Option4 这样的情况,我建议安装 react-usenpm 包,使用其中的 useDeepCompareEffect Hook 来解决问题,还能够避免 react-hooks/exhaustive-depslint rule 报错。

总结

大多数情况下,我能用 Option1 来解决问题。如果 Option1 无法解决,我就会使用 react-use 包里的 helper Hook 来解决问题。

多次调用同一个 setState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function App() {
const [num, setNum] = React.useState(0); // 声明一个变量 num

const add = () => {
setNum(num + 1);
console.log(num);
setNum(num + 2);
console.log(num);
};

return (
<div>
<div>{num}</div>
<button onClick={add}>加1</button>
</div>
);
}

这样只会计算最后一个 setNum。
如果想依赖上一次的可以使用函数写法。

1
2
3
4
5
const add = () => {
setNum((n) => n + 1);
// 一些操作
setNum((n) => n + 2);
};

这种写法是传入的就不是直接的值,而是一个 function,它接受前面的 state,我们可以根据这个 stat 来做一些处理以后,再返回一个 state。
所以我们这里调用完以后,页面上就会成功显示加 3 后的结果了。(但是一定要记住我说的:生效的只会是最后一个setState,如果你在最后仍写的是setNum(x),最后都会以 x 的值作为渲染)