constructor 会执行两次?- 浅淡 React StrictMode

9,640 阅读7分钟

前言

StrictModeReact16.3 版本新增的一个组件,按照官方的说法:

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

我相信很多人和我一样都知道 StrictMode 类似 JavaScript 中的 use strict, 可以让 React 程序在更严格的条件下运行,也已经在项目中使用了它,但是还是不太了解 StrictMode 怎样检测出程序的问题。下面就通过一个问题抛砖引玉,给大家简单的介绍一下 StrictMode

问题

大家可以先看一下上面的这段简单的代码,思考一下最终在页面上展示的值也就是 App 组件里 state.id 的值会是多少?

我相信大家对 React 有了解的话就会不假思索地说出 state.id = 1 这个答案,这个答案在生产环境中确实是正确答案,毕竟在 React 的官方文档中告诉了我们 Class Componentconstructor 只会在渲染时执行一次。但是你在开发环境中运行的时候,显示的答案并不如我们所想,来一起康康 运行的结果 吧:

一个大大的 2 出现在了我们的屏幕上。为什么会是 2,不是说好的 constructor 只会执行一次吗?难道 React 骗了我们?

这个问题出现的“罪魁祸首”就是 React.StrictMode,它在开发环境中将 constructor 函数调用了两次,至于为什么调用两次?其实为了检测意外的副作用,通过调用两次的方式将一些隐藏地比较深的副作用放大,让开发者更好的发现它。详情的内容可以查看下面的介绍。

StrictMode 功能介绍

1、检测意外的副作用

从概念上讲,React 分两个阶段工作:

渲染 阶段会确定需要进行哪些更改,比如 DOM。在此阶段,React 调用 render,然后将结果与上次渲染的结果进行比较。 提交 阶段发生在当 React 应用变化时。(对于 React DOM 来说,会发生在 React 插入,更新及删除 DOM 节点的时候。)在此阶段,React 还会调用 componentDidMountcomponentDidUpdate 之类的生命周期方法。 提交阶段通常会很快,但渲染过程可能很慢。因此,即将推出的 concurrent 模式 (默认情况下未启用) 将渲染工作分解为多个部分,对任务进行暂停和恢复操作以避免阻塞浏览器。这意味着 React 可以在提交之前多次调用渲染阶段生命周期的方法,或者在不提交的情况下调用它们(由于出现错误或更高优先级的任务使其中断)。

渲染阶段的生命周期包括以下 class 组件方法:

- constructor
- componentWillMount (or UNSAFE_componentWillMount)
- componentWillReceiveProps (or UNSAFE_componentWillReceiveProps)
- componentWillUpdate (or UNSAFE_componentWillUpdate)
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- setState 更新函数(第一个参数)

因为上述方法可能会被多次调用,所以不要在它们内部编写副作用相关的代码,这点非常重要。忽略此规则可能会导致各种问题的产生,包括内存泄漏和或出现无效的应用程序状态。不幸的是,这些问题很难被发现,因为它们通常具有非确定性。

严格模式不能自动检测到你的副作用,但它可以帮助你发现它们,使它们更具确定性。通过在开发环境下故意重复调用以下方法来实现的该操作:

- class 组件的 constructor 方法
- render 方法
- setState 更新函数 (第一个参数)
- 静态的 getDerivedStateFromProps 生命周期方法

以上这段话全都来自于 React官方中文文档,因为文档上已经对于 StrictMode 是为什么要检测意外的副作用以及怎么检测意外的副作用介绍得实在太详细了,笔者觉得没啥好补充的了,就直接搬运了过来😂。

render 方法为例,简单看下 StrictMode 关于检测意外的副作用的实现:

真的就像文档介绍的那么简单,仅仅只是对方法调用了两次,没有任何的比较之类的动作。

2、识别不安全的生命周期

React16.3 版本中将一些生命周期方法列为了不安全的生命周期。至于为什么这些生命周期方法是不安全的,可以参考这篇博客的开头,主要还是考虑到了使用这些生命周期的代码在 React 的未来版本中更有可能出现 bug。

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

StrictMode 就可以帮助我们检测代码中是否有使用到这些不安全的生命周期方法。在 Class Component 实例化完成后,会去组件实例上寻找有没有 componentWillMountUNSAFE_componentWillMount 这些不安全的生命周期的方法,有就 push 到一个数组里,最后统一在控制台发出警告️。代码实现比较简单,感兴趣可以看下ReactStrictModeWarnings.js 中的 recordUnsafeLifecycleWarningsflushPendingUnsafeLifecycleWarnings 这两个方法。

3、对于使用废弃的 findDOMNode 方法的警告

React 支持用 findDOMNode 来在给定 class 实例的情况下在树中搜索 DOM 节点。通常你不需要这样做,因为你可以将 ref 直接绑定到 DOM 节点。

findDOMNode 也可用于 class 组件,但它违反了抽象原则,它使得父组件需要单独渲染子组件。它会产生重构危险,你不能更改组件的实现细节,因为父组件可能正在访问它的 DOM 节点。findDOMNode 只返回第一个子节点,但是使用 Fragments,组件可以渲染多个 DOM 节点。findDOMNode 是一个只读一次的 API。调用该方法只会返回第一次查询的结果。如果子组件渲染了不同的节点,则无法跟踪此更改。因此,findDOMNode 仅在组件返回单个且不可变的 DOM 节点时才有效。

这段话也来自官方文档,因为findDOMNode 的这些问题,所以React 决定在 StrictMode 中废弃它,在调用 findDOMNode 会去判断是否在 StrictMode 模式下,有则在控制台打印出警告。

4、检测过时的 context API

因为旧的 Context API 在 context 的值有更新时,没办法保证所有子节点一定能更新(因为中间父组件的 shouldComponentUpdate 返回 false,那么使用到该值的后代组件不会进行更新)的问题,所以 React 在 16.3 版本提出了新的 Context API,所以在 StrictMode 中会检测应用中是否使用到了过时的 Context API

由于老的 Context API 会在 Context 提供者上绑定 childContextTypesgetChildContext 以及在 在 Context 的使用者上绑定用来访问 context 的 contextTypes 属性,所以 StrictMode 只要在组件实例化完成后判断实例上有没有这几个属性就能判断是否使用了老的 Context API,然后作出统一的警告。

5、对于使用字符串 ref API 的警告

这部分内容虽然官方文档上有提到,但是经过笔者的实验,并不能在 StrictMode 下对使用了 string ref API 的行为在控制台产生警告,所以就不在这里多做提及。

写在最后

StrictMode 确实可以帮助我们让 React 程序运行地更好,更健壮,这个毋庸置疑。但是笔者认为在检测意外的副作用这一点上 React 做的对开发者不够友好吧,虽然对于一部分方法调用两次可以更容易发现出意外的副作用,但是对于刚接触 React 的人或者对 StrictMode 了解不够的人来说,在开发的时候更可能会认为是 React 出了问题或者自己的写法有问题,导致了重复调用,浪费不必要的 debug 时间。这一方面可能需要做出更友好的提示。