0%

StrictMode引发的组件重复执行

前言

2022年3月29号,React 18正式版发布,小明也兴冲冲地开始hooks学习之旅。某天他使用codesandbox写了一个小demo时,他发现组件诡异地渲染了多次,函数组件代码如下:

count.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function Count() {
const [counter, setCounter] = useState(0);
useEffect(() => {
console.log("effect");
setTimeout(() => {
setCounter(counter + 1);
}, 3000);
});
console.log("before render");

return (
<div className="container">
<div className="el">{counter}</div>
</div>
);
}

实现的效果很简单,每隔三秒将count加1,很简单对不对?

打开这个 demo,同时打开控制台,你就可以看到如下输入:

CPT2204012224-580x267.gif

before rendereffect一开始都打印了两次,之后before render每次都诡异地打印了两次

React.StrictMode

这是为什么呢?经过一番查找,发现是React.StrictMode的锅。React 17文档中是这样描述 React.StrictMode:

StrictMode 是一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。

React 17文档中关于它的作用大概可以归为两类:

  • 检测副作用
  • 对于在应用中使用已经废弃、过时的方法会发出警告

对于第二点,相信大家都可以理解,毕竟React现在都发布18版了,不少以前的方法已经过时或者废弃了,在较新版本的React中再使用这些方法肯定时不安全的。

那么对于第一点呢?

这不得不提React 18文档对于StrictMode的描述:

React offers a “Strict Mode” in which it calls each component’s function twice during development.

大意就是在开发者模式中,StrictMode会将相应组件执行两次,这下重复执行的疑惑解决了。

但新的疑问产生了,文档中一直提的副作用又是啥?副作用这个词在React 17文档中提到的次数很多,如何理解它呢?

在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响 – 维基百科

举个通俗易懂的例子:张三不小心感冒了,鼻塞流涕,医生给他开了感冒药,感冒药的作用就是让我们的身体恢复健康,但是服用的过程中,张三感冒的症状的确是减轻了,但同时他感到浑身乏力、嗜睡,这就副作用,即意料之外的结果。

事实上,React 18的文档提到的更多的是purity,即纯度,这其实是函数式编程的理念,这与React 17文档中提到的无副作用是一个意思,react hooks函数式组件实际上就是函数式编程理念的体现。编写纯函数带来了一定的心智负担,但随着开发者对其接受度的提高,新文档中大量使用了purity进行相关描述。文档中提到,纯函数带来了以下优势:

  • 多环境运行。例如可以运行在服务端,因为同样的输入,总是对应同样的输出,因此组件可以被其他人复用;
  • 减少重复渲染。如果函数组件的输入没有改变,直接复用就好啦,不需要重复渲染。
  • 随时中断渲染。在渲染层级较深的组件树时,数据发生了改变,那么React可以马上重新开始渲染,而不用等待过时的渲染完成。

因此StrictMode就是在开发中帮助我们进行检测,保证我们编写的函数组件都是 '纯' 的,这也就解释了为什么开头提到的为什么组件会执行两次,StrictMode会多执行一次,两次执行的结果相同,证明我们编写的的确是纯函数。

实例

以下列举了一些引发StrictMode的其他例子

  1. 在函数内部修改一个已经存在的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let guest = 0;

function Cup() {
// Bad: changing a preexisting variable!
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
)
}

很明显,当这个组件执行多次,guest的值是逐渐增长的,回想一下纯函数的定义,上述函数组件不是纯函数,因此StrictMode会进行警告。

  1. useState

下面的组件定义了counter变量,我们给useState传入了初始化函数,组件首次运行时会执行一次这个函数,返回的值作为counter的初始值,在之后的执行中这个初始化函数会被忽略。

此外,在使用setState改变counter的值时,我们为其提供了一个updater函数,当点击按钮时会更新counter的值。

因此,理论上首先会打印一次initializer,然后每按一次按钮,都会打印一次updater

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 } from "react";

export default function Count2() {
const [counter, setCounter] = useState(() => {
console.log('initializer')
return 0
});

const handleClick = function() {
setCounter(() => {
console.log('updater')
return counter+1
})
}

return (
<div className="container">
<div className="el">{counter}</div>
<button onClick={handleClick}>增加</button>
</div>
);
}

demo

而事实上,StrictMode会帮我们把这两个函数都调用两次,保证其为纯函数。

总结

React.StrictMode在开发模式下会重复调用组件,保证我们编写的组件

  • 检测意外的副作用,确保函数组件时纯函数
  • 对于在应用中使用已经废弃、过时的方法会发出警告
  • 仅在开发者模式下运行,不影响生产构建

    参考

    Side Effects: (un)intended consequences

# 略微探究React StrictMode两次渲染的问题

严格模式