深入 ‘Strict Mode’ 的双重检查逻辑:它究竟能帮我们发现哪些潜在的‘纯函数’违反规则?

各位同仁,大家好。今天我们将深入探讨React的Strict Mode,一个在开发阶段至关重要的工具。它不仅仅是一个简单的警告机制,更是一个通过“双重检查”逻辑,帮助我们发现代码中潜在的、违反React核心设计原则——尤其是“纯函数”规则——的问题的强大助手。

作为一名编程专家,我深知在现代前端框架中,性能、可预测性和可维护性是何等重要。React通过其虚拟DOM和组件化的思想,极大地提升了开发效率。然而,这一切的基石,很大程度上依赖于组件的“纯粹性”。Strict Mode正是为了帮助我们维护这种纯粹性而生。

纯函数在React中的核心地位

在深入Strict Mode的双重检查之前,我们必须重温纯函数的概念及其在React中的重要性。

什么是纯函数?
一个纯函数必须满足两个条件:

  1. 相同的输入,相同的输出: 给定相同的参数,它总是返回相同的结果。
  2. 无副作用: 它不会修改任何外部状态(如全局变量、传入的参数对象、DOM),也不会执行诸如网络请求、定时器操作等改变程序外部环境的操作。

React为何推崇纯函数?
React组件(无论是函数组件的本体,还是类组件的render方法、getDerivedStateFromProps等)被设计为尽可能地像纯函数。原因如下:

  • 可预测性: 纯函数使得组件的行为更易于预测。当状态和属性确定时,输出也确定。
  • 可测试性: 纯函数易于隔离测试,因为它们不依赖外部状态,也不产生外部影响。
  • 性能优化: React可以安全地对纯组件进行缓存(memoization)。如果输入没有改变,React知道无需重新渲染。
  • 并发模式(Concurrent Mode)的基石: 这是最关键的一点。React的未来方向是并发渲染。在并发模式下,React可能会在后台多次渲染组件、暂停渲染、甚至丢弃某些渲染结果。如果渲染过程存在副作用,这些不确定性的渲染行为将导致难以追踪的bug,甚至数据不一致。纯函数确保了即使渲染被多次执行或中断,也不会对应用产生负面影响。

简而言之,当我们在React中编写组件时,我们期望以下操作是纯粹的:

  • 函数组件的本体: 这是你编写JSX的地方,也是useStateuseReducer的初始化函数,以及useMemouseCallback的工厂函数执行的地方。
  • 类组件的render方法。
  • useStateuseReducer的初始化函数。
  • useReducer的reducer函数。
  • 类组件的getDerivedStateFromProps方法。
  • 类组件的shouldComponentUpdate方法。

这些函数或方法的目标是根据输入(props、state)计算出下一个UI状态,而不是去修改外部世界。

Strict Mode的“双重检查”机制解析

Strict Mode(严格模式)是一个只在开发环境下生效的工具,它不会影响生产环境的性能或行为。它的核心机制之一,就是对某些特定函数和方法进行“双重调用”

当你在应用中启用Strict Mode时,React会在开发模式下有意地执行某些渲染相关逻辑两次。它的目的不是让你的代码真的运行两次,而是通过这种方式,放大那些在预期为纯函数的地方引入的副作用,从而更容易地暴露它们。如果你的代码在第一次执行时产生了副作用,那么第二次执行时,这个副作用可能会被再次触发,或者依赖于第一次副作用的结果,从而导致意想不到的行为或错误,进而引起你的注意。

哪些生命周期方法和Hook会被双重调用?

| 类型 | 受影响的方法/Hook | 具体行为 | 严格模式下的行为 “`
import React, { useState, useEffect, useReducer, useCallback, useMemo, useRef } from ‘react’;
import ReactDOM from ‘react-dom/client’;

// 假设这是你的App组件
function App() {
return (

{/* 所有的测试组件都将放在这里 */}

Strict Mode 双重检查演示








);
}

// ————————————————————————————————
// 1. 纯函数违反规则:在渲染阶段产生副作用
// ————————————————————————————————
let externalCounter = 0; // 外部状态

function PureFunctionViolationInRender() {
// 错误示范:在组件渲染时修改外部状态
// 在严格模式下,这个 console.log 和 externalCounter++ 会被执行两次
// 导致计数器增加两次,或者日志打印两次。
console.log(“PureFunctionViolationInRender: 组件正在渲染…”);
externalCounter++;
console.log(`PureFunctionViolationInRender: 外部计数器被修改为 ${externalCounter}`);

// 错误示范:在渲染时直接修改DOM (通常会报错,或者行为异常)
// document.title = `Counter: ${externalCounter}`; // 这也会在严格模式下导致双重执行

// 渲染时进行网络请求 (会被执行两次)
// fetch(‘https://api.example.com/data’)
// .then(res => res.json())
// .then(data => console.log(‘Fetched data in render:’, data));

const [localState] = useState(0); // 即使组件没有状态更新,渲染函数也会因父组件或自身初次挂载而被双重调用

return (

1. 渲染阶段的纯函数违反 (PureFunctionViolationInRender)

**Bad Practice**: Modifying `externalCounter` or `document.title` directly in the render body.
In Strict Mode, this component’s body (render logic) will run twice,
leading to `externalCounter` being incremented twice per “intended” render,
or `document.title` being set redundantly.

外部计数器 (在渲染时被修改): {externalCounter}

Check console logs for “组件正在渲染…” and “外部计数器被修改为” messages.
You’ll see them appear twice for a single conceptual render.

);
}

// 修复方案:将副作用移动到 useEffect
function GoodPureFunctionInRender() {
const [localState, setLocalState] = useState(0);

useEffect(() => {
// 这是一个副作用,但它被正确地放置在 useEffect 中
console.log(“GoodPureFunctionInRender: 副作用在 useEffect 中执行.”);
document.title = `Good Counter: ${localState}`;

// 可以在这里进行网络请求
// fetch(‘https://api.example.com/data’).then(…);

return () => {
// 清理函数
console.log(“GoodPureFunctionInRender: 副作用清理.”);
};
}, [localState]); // 依赖项数组确保只在 localState 改变时执行

return (

1.1 修复后的渲染阶段 (GoodPureFunctionInRender)

**Good Practice**: Side effects are moved to `useEffect`.
The component’s render body remains pure.

);
}

// ————————————————————————————————
// 2. 纯函数违反规则:在 useState 的初始化函数中产生副作用
// ————————————————————————————————
let useStateInitializerCallCount = 0;

function createInitialStateWithSideEffect() {
useStateInitializerCallCount++;
console.log(`PureFunctionViolationInUseStateInitializer: useState 初始化函数被调用 ${useStateInitializerCallCount} 次`);
// 错误示范:这里不应该有副作用,比如修改外部变量、发送网络请求等
// 例如,如果你在这里生成一个随机ID并期望它只生成一次:
// return Math.random(); // 严格模式下会被调用两次,但只有第一次的值会被用作初始状态,第二次的值被丢弃,这本身不是bug,但如果这里有外部副作用就麻烦了
return { value: 0, id: Math.random().toFixed(4) }; // 严格模式下,id会生成两次,但只有第一次的id被实际采用
}

function PureFunctionViolationInUseStateInitializer() {
// 严格模式下,createInitialStateWithSideEffect 会被调用两次
const [state, setState] = useState(createInitialStateWithSideEffect);

return (

2. useState 初始化函数的纯函数违反

**Bad Practice**: `createInitialStateWithSideEffect` (the initializer for `useState`)
modifies `useStateInitializerCallCount` and logs to console.
In Strict Mode, `createInitialStateWithSideEffect` will be called twice.

`useState` 初始化函数调用次数 (在严格模式下应看到 2 次): {useStateInitializerCallCount}

初始状态 ID (第一次调用时生成): {state.id}

Check console logs. You’ll see “useState 初始化函数被调用” twice,
even though `state.id` only reflects the first call’s result.

);
}

// 修复方案:确保初始化函数是纯粹的
function GoodPureFunctionInUseStateInitializer() {
const createPureInitialState = () => {
// 纯函数:只计算并返回一个值,没有副作用
return { value: 0, id: Math.random().toFixed(4) };
};
const [state, setState] = useState(createPureInitialState);

// 如果需要副作用,请使用 useEffect
useEffect(() => {
console.log(“GoodPureFunctionInUseStateInitializer: 副作用在 useEffect 中.”);
}, []);

return (

2.1 修复后的 useState 初始化函数

**Good Practice**: The initializer `createPureInitialState` is pure.
Side effects (if any) are moved to `useEffect`.

初始状态 ID: {state.id}

);
}

// ————————————————————————————————
// 3. 纯函数违反规则:在 useReducer 的初始化函数中产生副作用
// ————————————————————————————————
let useReducerInitializerCallCount = 0;

function init(initialCount) {
useReducerInitializerCallCount++;
console.log(`PureFunctionViolationInUseReducerInitializer: useReducer 初始化函数被调用 ${useReducerInitializerCallCount} 次`);
// 错误示范:这里不应该有副作用
// 比如从 localStorage 读取数据并修改外部变量
// const savedCount = localStorage.getItem(‘myCount’); // 严格模式下会被调用两次
return { count: initialCount + 100, initializerId: Math.random().toFixed(4) };
}

function reducer(state, action) {
switch (action.type) {
case ‘increment’:
return { …state, count: state.count + 1 };
case ‘decrement’:
return { …state, count: state.count – 1 };
default:
throw new Error();
}
}

function PureFunctionViolationInUseReducerInitializer() {
// 严格模式下,init 函数会被调用两次
const [state, dispatch] = useReducer(reducer, 0, init);

return (

3. useReducer 初始化函数的纯函数违反

**Bad Practice**: `init` function for `useReducer` modifies `useReducerInitializerCallCount`.
In Strict Mode, `init` will be called twice.

`useReducer` 初始化函数调用次数 (在严格模式下应看到 2 次): {useReducerInitializerCallCount}

Count: {state.count}, Initializer ID: {state.initializerId}


Check console logs. You’ll see “useReducer 初始化函数被调用” twice.

);
}

// 修复方案:确保 useReducer 初始化函数是纯粹的
function GoodPureFunctionInUseReducerInitializer() {
const pureInit = (initialCount) => {
// 纯函数:只计算并返回一个值
return { count: initialCount + 100, initializerId: Math.random().toFixed(4) };
};
const [state, dispatch] = useReducer(reducer, 0, pureInit); // 使用纯粹的初始化函数

return (

3.1 修复后的 useReducer 初始化函数

**Good Practice**: The `pureInit` function is pure.

Count: {state.count}, Initializer ID: {state.initializerId}


);
}

// ————————————————————————————————
// 4. 纯函数违反规则:在 useReducer 的 reducer 函数中产生副作用(或修改原状态)
// ————————————————————————————————
let reducerSideEffectCount = 0;

function badReducer(state, action) {
reducerSideEffectCount++;
console.log(`PureFunctionViolationInUseReducerReducer: Reducer 函数被调用 ${reducerSideEffectCount} 次`);

switch (action.type) {
case ‘increment’:
// 错误示范1: 直接修改传入的 state (mutation)
// state.count = state.count + 1;
// return state;

// 错误示范2: 修改外部状态
console.log(“BadReducer: 正在尝试修改外部变量…”);
externalCounter++; // 修改外部状态
return { …state, count: state.count + 1 }; // 同时返回新状态

case ‘decrement’:
return { …state, count: state.count – 1 };
default:
throw new Error();
}
}

function PureFunctionViolationInUseReducerReducer() {
// 严格模式下,reducer 函数会被调用两次
// 第一次调用通常用于实际计算新状态
// 第二次调用是一个“空跑”,用于检测副作用。
// 如果 reducer 有副作用(如修改外部变量),第二次调用会再次触发副作用,
// 导致外部变量被修改两次,或者如果 reducer 不纯粹(比如随机数),则两次调用的结果会不一致。
const [state, dispatch] = useReducer(badReducer, { count: 0 });

return (

4. useReducer Reducer 函数的纯函数违反

**Bad Practice**: `badReducer` modifies `reducerSideEffectCount` and `externalCounter`
and logs to console.
In Strict Mode, `badReducer` will be called twice for each dispatch.

Reducer 函数调用次数 (每次 dispatch 在严格模式下应看到 2 次): {reducerSideEffectCount}

Count: {state.count}, 外部计数器 (被 reducer 修改): {externalCounter}


Click “Increment” and observe console logs and `externalCounter` value.
You’ll see the reducer run twice, incrementing `externalCounter` by 2,
even though `state.count` only increments by 1.

);
}

// 修复方案:确保 reducer 是纯粹的,不修改原始状态,不产生外部副作用
function goodReducer(state, action) {
switch (action.type) {
case ‘increment’:
// 正确示范:返回一个全新的状态对象,不修改原始 state
return { …state, count: state.count + 1 };
case ‘decrement’:
return { …state, count: state.count – 1 };
default:
throw new Error();
}
}

function GoodPureFunctionInUseReducerReducer() {
const [state, dispatch] = useReducer(goodReducer, { count: 0 });

return (

4.1 修复后的 useReducer Reducer 函数

**Good Practice**: `goodReducer` is a pure function. It returns new state objects
without mutating the original state or causing external side effects.

Count: {state.count}


);
}

// ————————————————————————————————
// 5. 纯函数违反规则:在 useMemo 或 useCallback 的工厂函数中产生副作用
// ————————————————————————————————
let memoOrCallbackFactoryCallCount = 0;

function PureFunctionViolationInUseMemoOrUseCallbackFactory() {
const [value, setValue] = useState(0);

// 错误示范:在 useMemo 的工厂函数中产生副作用
// 严格模式下,这个函数会被调用两次
const memoizedValue = useMemo(() => {
memoOrCallbackFactoryCallCount++;
console.log(`PureFunctionViolationInUseMemoOrUseCallbackFactory: useMemo 工厂函数被调用 ${memoOrCallbackFactoryCallCount} 次`);
// 错误示范:修改外部状态
externalCounter++;
return value * 2;
}, [value]); // 依赖项

// 错误示范:在 useCallback 的工厂函数中产生副作用
// 严格模式下,这个函数也会被调用两次
const memoizedCallback = useCallback(() => {
memoOrCallbackFactoryCallCount++;
console.log(`PureFunctionViolationInUseMemoOrUseCallbackFactory: useCallback 工厂函数被调用 ${memoOrCallbackFactoryCallCount} 次`);
// 错误示范:修改外部状态
externalCounter++;
alert(`Value is: ${value}`);
}, [value]); // 依赖项

return (

5. useMemo/useCallback 工厂函数的纯函数违反

**Bad Practice**: `useMemo` and `useCallback` factory functions
modify `memoOrCallbackFactoryCallCount` and `externalCounter`.
In Strict Mode, these factories will be called twice when their dependencies change.

`useMemo`/`useCallback` 工厂函数调用次数 (每次依赖变化在严格模式下应看到 2 次): {memoOrCallbackFactoryCallCount}

Memoized Value: {memoizedValue}, 外部计数器 (被 memo/callback 修改): {externalCounter}


Click “Increment Value” and observe console logs and `externalCounter`.
You’ll see `externalCounter` increment by 4 (2 from useMemo, 2 from useCallback)
for each dependency change, even though only one value update is intended.

);
}

// 修复方案:确保 useMemo/useCallback 的工厂函数是纯粹的
function GoodPureFunctionInUseMemoOrUseCallbackFactory() {
const [value, setValue] = useState(0);

const pureMemoizedValue = useMemo(() => {
// 纯函数:只计算并返回一个值
return value * 2;
}, [value]);

const pureMemoizedCallback = useCallback(() => {
// 纯函数:在调用时执行副作用,而不是在创建时
alert(`Pure Value is: ${value}`);
}, [value]);

useEffect(() => {
// 如果需要基于 memoizedValue 或 pureMemoizedCallback 的创建执行副作用,请在 useEffect 中进行
console.log(“GoodPureFunctionInUseMemoOrUseCallbackFactory: 副作用在 useEffect 中.”);
}, [pureMemoizedValue, pureMemoizedCallback]);

return (

5.1 修复后的 useMemo/useCallback 工厂函数

**Good Practice**: `useMemo` and `useCallback` factory functions are pure.
Side effects are deferred to `useEffect` or executed when the callback is explicitly invoked.

Memoized Value: {pureMemoizedValue}


);
}

// ————————————————————————————————
// 6. useEffect 副作用与清理函数的验证
// ————————————————————————————————
let effectRunCount = 0;
let cleanupRunCount = 0;

function EffectCleanupVerification() {
const [trigger, setTrigger] = useState(0);
const effectRef = useRef(null);

useEffect(() => {
effectRunCount++;
console.log(`EffectCleanupVerification: Effect runs (Mount or Update) – ${effectRunCount} times.`);
// 模拟订阅
effectRef.current = `Subscribed to trigger ${trigger}`;
console.log(effectRef.current);

return () => {
cleanupRunCount++;
console.log(`EffectCleanupVerification: Cleanup runs (Unmount or before next Effect) – ${cleanupRunCount} times.`);
// 模拟取消订阅
effectRef.current = null;
};
}, [trigger]); // 依赖项

return (

6. `useEffect` 副作用与清理函数的验证

**Strict Mode Behavior**: For `useEffect`, Strict Mode mounts the component,
then immediately unmounts it (running cleanup), and then mounts it again.
This simulates a fast unmount/remount cycle to ensure cleanup logic is robust.

Effect runs: {effectRunCount}

Cleanup runs: {cleanupRunCount}

Current subscription status: {effectRef.current ? effectRef.current : ‘Unsubscribed’}

When component first mounts, you’ll see:
Effect runs -> Cleanup runs -> Effect runs. (e.g., Effect=2, Cleanup=1)
Click “Trigger Effect Re-run”, and you’ll see:
Cleanup runs -> Effect runs -> Cleanup runs -> Effect runs. (e.g., Effect=4, Cleanup=3)
This ensures your cleanup properly reverses the effect.

);
}

// ————————————————————————————————
// 7. 类组件的纯函数违反
// ————————————————————————————————
let classComponentRenderCount = 0;
let classComponentConstructorCount = 0;
let classComponentDerivedStateCount = 0;

class ClassComponentPureFunctionViolation extends React.Component {
constructor(props) {
super(props);
classComponentConstructorCount++;
console.log(`ClassComponentPureFunctionViolation: Constructor called ${classComponentConstructorCount} times.`);
// 错误示范:在 constructor 中执行副作用,如修改外部变量
externalCounter++;
this.state = {
value: 0,
derivedValue: 0
};
}

static getDerivedStateFromProps(nextProps, prevState) {
classComponentDerivedStateCount++;
console.log(`ClassComponentPureFunctionViolation: getDerivedStateFromProps called ${classComponentDerivedStateCount} times.`);
// 错误示范:在 getDerivedStateFromProps 中执行副作用
externalCounter++;
return {
derivedValue: nextProps.propValue * 2 || prevState.derivedValue
};
}

shouldComponentUpdate(nextProps, nextState) {
// shouldComponentUpdate 也会被双重调用,但它的主要目的是性能优化,
// 如果它内部有副作用,同样会被放大。
console.log(“ClassComponentPureFunctionViolation: shouldComponentUpdate called.”);
return true;
}

render() {
classComponentRenderCount++;
console.log(`ClassComponentPureFunctionViolation: Render method called ${classComponentRenderCount} times.`);
// 错误示范:在 render 方法中执行副作用
externalCounter++;
return (

7. 类组件的纯函数违反

**Bad Practice**: `constructor`, `getDerivedStateFromProps`, and `render`
modify `externalCounter` and log to console.
In Strict Mode, these methods will be called twice.

Constructor calls: {classComponentConstructorCount}

getDerivedStateFromProps calls: {classComponentDerivedStateCount}

Render calls: {classComponentRenderCount}

外部计数器 (被类组件修改): {externalCounter}

Observe console logs and `externalCounter`. You’ll see `externalCounter`
incremented multiple times due to double invocation of these methods.

);
}
}

// 修复方案:确保类组件的方法是纯粹的
class GoodClassComponent extends React.Component {
constructor(props) {
super(props);
// 纯粹的 constructor:只初始化 state 和绑定方法
this.state = {
value: 0
};
}

static getDerivedStateFromProps(nextProps, prevState) {
// 纯粹的 getDerivedStateFromProps:只根据 props 和 state 计算并返回新 state
return {
derivedValue: nextProps.propValue * 2 || prevState.derivedValue
};
}

componentDidMount() {
// 副作用应放在 componentDidMount 或 componentDidUpdate 中
console.log(“GoodClassComponent: componentDidMount – 副作用在这里执行.”);
document.title = `Good Class Counter: ${this.state.value}`;
}

componentDidUpdate(prevProps, prevState) {
if (prevState.value !== this.state.value) {
console.log(“GoodClassComponent: componentDidUpdate – 副作用在这里执行.”);
document.title = `Good Class Counter: ${this.state.value}`;
}
}

render() {
// 纯粹的 render:只根据 state 和 props 返回 JSX
return (

7.1 修复后的类组件 (GoodClassComponent)

**Good Practice**: `constructor`, `getDerivedStateFromProps`, and `render` are pure.
Side effects are moved to `componentDidMount` or `componentDidUpdate`.

Value: {this.state.value}

);
}
}

// ————————————————————————————————
// 渲染根组件
// ————————————————————————————————
const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render();

// 注意:为了让所有示例都能在同一个页面运行,我将 Good 示例的调用注释掉了。
// 在实际开发中,你会将 Bad 示例修复为 Good 示例。

/*
// 如果想单独测试 Good 示例,可以这样:
root.render(

Strict Mode 双重检查演示 (修复后)






// 这个组件是演示 Strict Mode 行为的,本身没有“坏实践”需要修复



);
*/
“`

### 代码解释与运行效果

上面的代码中,我创建了多个组件,每个组件都演示了一种在React中违反纯函数规则的常见场景,以及`Strict Mode`如何通过“双重检查”来揭示这些问题。

当你运行这段代码并在浏览器控制台中观察时,你会发现:

1. **`PureFunctionViolationInRender`**:
* 即使组件只被“概念上”渲染了一次,控制台的日志“组件正在渲染…”和“外部计数器被修改为”会打印**两次**。
* `externalCounter`会以你预期速度的**两倍**增长。这揭示了你在渲染逻辑中不应该执行副作用,因为渲染可能会被重复执行。

2. **`PureFunctionViolationInUseStateInitializer`**:
* `useState`的初始化函数`createInitialStateWithSideEffect`会打印两次日志,并且`useStateInitializerCallCount`会显示为2。
* 尽管它被调用了两次,但`state.id`仍然只显示第一次调用的结果,因为React只会使用第一次调用的返回值作为实际的初始状态。但这依然表明,如果你的初始化函数有外部副作用,它会意外地执行两次。

3. **`PureFunctionViolationInUseReducerInitializer`**:
* 类似地,`useReducer`的初始化函数`init`也会被调用两次,`useReducerInitializerCallCount`会显示为2。
* 这强调了`useReducer`的初始化函数也必须是纯函数。

4. **`PureFunctionViolationInUseReducerReducer`**:
* 每次你点击按钮触发`dispatch`时,`badReducer`函数会打印两次日志,`reducerSideEffectCount`会以两倍的速度增长。
* 最重要的是,`externalCounter`也会因为reducer中的副作用而**额外**增加。这意味着,如果你在reducer中不小心修改了外部状态,或者对传入的`state`进行了原地修改(mutation),那么在严格模式下,这些不纯的操作会被放大,导致状态行为异常。

5. **`PureFunctionViolationInUseMemoOrUseCallbackFactory`**:
* 每次你点击“Increment Value”按钮,`useMemo`和`useCallback`的工厂函数都会被调用**两次**。
* 这会导致`memoOrCallbackFactoryCallCount`和`externalCounter`以两倍的速度增长,表明这些工厂函数也应是纯函数。它们应该只计算和返回一个值,而不是产生副作用。

6. **`EffectCleanupVerification`**:
* 当组件首次挂载时,你会看到`Effect runs`日志,紧接着是`Cleanup runs`日志,然后又是`Effect runs`日志。例如,`Effect runs: 2`, `Cleanup runs: 1`。
* 每次你点击“Trigger Effect Re-run”时,你会看到`Cleanup runs` -> `Effect runs` -> `Cleanup runs` -> `Effect runs`的序列。例如,`Effect runs`从2跳到4,`Cleanup runs`从1跳到3。
* 这个行为是`Strict Mode`模拟组件“快速卸载再挂载”来验证`useEffect`的清理逻辑是否健壮。如果你的清理函数没有正确地撤销效果,或者依赖于已不存在的状态,这种双重检查会帮助你发现问题。

7. **`ClassComponentPureFunctionViolation`**:
* `constructor`、`getDerivedStateFromProps`和`render`方法在组件首次挂载时都会被调用两次。
* `externalCounter`会因此被多次修改,展示了类组件中这些核心方法也应遵循纯函数原则。

这些“双重调用”并不是bug,而是`Strict Mode`有意为之的行为,旨在通过重复执行来暴露那些不应该存在的副作用,从而在开发早期强制我们编写更健壮、更可预测的React代码。

### 纯函数规则的潜在违反者与Strict Mode的揭示

现在,我们来系统地梳理一下`Strict Mode`究竟能帮我们发现哪些潜在的纯函数违反规则。

#### 1. **渲染(Render)阶段的副作用**

* **潜在违反:**
* 在函数组件的顶层作用域或类组件的`render`方法中直接修改外部变量、全局对象(如`window`、`document`)、DOM、发起网络请求、设置定时器。
* 例如:`externalData.push(newItem);`、`document.title = ‘New Title’;`、`fetch(‘/api/data’);`。
* **Strict Mode如何揭示:**
* 它会双重调用函数组件的本体和类组件的`render`方法。
* 如果存在副作用,这些操作会被执行两次,导致外部状态被意外修改两次,或者网络请求被发送两次(可能导致数据重复或服务器压力),或者DOM操作出现异常。控制台的重复日志也会立即引起注意。
* **为何重要:** 渲染过程应该是纯粹的,只根据props和state计算UI。React可能会多次执行渲染,暂停或丢弃结果。副作用会使得这些行为变得不可预测和危险。
* **正确实践:** 将所有副作用封装在`useEffect`(函数组件)或`componentDidMount`/`componentDidUpdate`/`componentWillUnmount`(类组件)中。

#### 2. **`useState`初始化函数的副作用**

* **潜在违反:**
* `useState`接受一个函数作为初始状态的参数,这个函数应该是一个纯函数,只计算并返回初始状态。如果它执行了修改外部变量、进行网络请求等副作用。
* 例如:`useState(() => { console.log(‘Side effect!’); return calculateInitialValue(); });`
* **Strict Mode如何揭示:**
* 它会双重调用`useState`的初始化函数。
* 副作用会执行两次。虽然React只使用第一次调用的结果作为初始状态,但重复的副作用仍然可能导致问题(例如,如果副作用是生成唯一ID并将其存储到外部,那么外部存储的ID将是第二次生成的,而不是实际使用的ID)。
* **为何重要:** 初始化函数在组件挂载时执行,也可能在React内部的某些预渲染阶段被调用。纯粹的初始化函数确保无论何时被调用,都能安全地提供初始值。
* **正确实践:** `useState`的初始化函数应仅包含纯粹的计算逻辑。如果需要副作用来初始化状态(如从localStorage读取),应在`useEffect`中处理,并在`useEffect`中调用`setState`来更新状态。

#### 3. **`useReducer`初始化函数和Reducer函数的副作用或状态突变**

* **潜在违反:**
* **初始化函数:** 类似于`useState`,`useReducer`的第三个参数`init`函数也应是纯函数。
* **Reducer函数:** `reducer(state, action)`函数必须是纯函数。它根据当前`state`和`action`计算并返回一个新的`state`对象,**绝不能直接修改传入的`state`对象,也不能产生外部副作用**。
* 例如:
* 初始化函数:`useReducer(reducer, initialArg, () => { externalLog.push(‘init’); return initialArg; });`
* Reducer函数:`case ‘UPDATE’: state.count++; return state;` (突变) 或 `case ‘FETCH_DATA’: fetch(‘/api’).then(…); return { …state, loading: true };` (副作用)
* **Strict Mode如何揭示:**
* 它会双重调用`useReducer`的`init`函数。
* 它会双重调用`reducer`函数。
* 如果`init`函数有副作用,它会执行两次。
* 如果`reducer`函数有副作用,它会执行两次。更严重的是,如果`reducer`函数直接修改了`state`(突变),第二次调用时,它将基于第一次调变异后的`state`再次进行修改,这会导致状态的不可预测性。如果reducer在两次调用中返回了不同的结果(例如,因为它内部使用了`Math.random()`而没有memoization,或者因为副作用导致外部数据改变),React会发出警告。
* **为何重要:** Reducer是状态更新的核心逻辑。不纯的reducer会使得状态管理变得混乱且难以调试,尤其是在并发模式下,React可能会多次尝试应用reducer。
* **正确实践:** 确保`init`和`reducer`函数都是纯函数。Reducer必须通过返回新对象来更新状态,而不是修改旧对象(保持状态的不可变性)。所有副作用都应在`useEffect`中处理。

#### 4. **`useMemo`和`useCallback`工厂函数的副作用**

* **潜在违反:**
* `useMemo`和`useCallback`接受一个“工厂函数”(即生成memoized值或回调的函数)。这个工厂函数也应该是一个纯函数。它应该只计算并返回一个值,而不是执行副作用。
* 例如:`useMemo(() => { console.log(‘Creating memoized value!’); externalCounter++; return someExpensiveCalculation(); }, [deps]);`
* **Strict Mode如何揭示:**
* 它会双重调用`useMemo`和`useCallback`的工厂函数。
* 如果工厂函数有副作用,这些副作用会执行两次。这通常会导致外部状态被意外修改两次,或者日志被打印两次。
* **为何重要:** 这些工厂函数可能在React重新渲染组件时被调用,也可能在React内部的某些预渲染阶段被调用。纯粹性确保它们能安全地生成memoized值或回调。
* **正确实践:** 确保`useMemo`和`useCallback`的工厂函数是纯粹的计算。如果需要基于memoized值的创建来执行副作用,请在`useEffect`中进行。

#### 5. **`useEffect`清理函数的验证**

* **潜在违反:**
* `useEffect`的清理函数(即`return`的函数)应该能够完全撤销effect函数所做的任何事情。如果清理函数没有正确地撤销副作用,或者它依赖于在effect执行后就不存在的某些状态。
* **Strict Mode如何揭示:**
* 它会模拟组件的“快速卸载和重新挂载”:
1. 组件正常挂载。
2. `useEffect`执行。
3. 组件立即卸载(`useEffect`的清理函数执行)。
4. 组件再次挂载。
5. `useEffect`再次执行。
* 通过这种方式,`Strict Mode`强制你验证你的清理逻辑是否健壮。如果你的清理函数不完整或有bug,这种快速的“卸载-挂载”循环会立即暴露出来,例如,资源没有被正确释放,或者订阅没有被正确取消。
* **为何重要:** 健壮的清理函数对于避免内存泄漏、不必要的网络请求或订阅、以及确保组件生命周期的正确管理至关重要。这对于可预测性和并发模式下的稳定性尤其关键。
* **正确实践:** 确保`useEffect`的清理函数总是能够完整地撤销其对应的effect所做的任何操作。

#### 6. **类组件的特定方法**

* **潜在违反:**
* **`constructor`:** 应该只用于初始化`state`和绑定方法。不应执行副作用。
* **`getDerivedStateFromProps`:** 应该是一个纯静态方法,只根据`props`和`state`计算并返回新的`state`。不应执行副作用。
* **`shouldComponentUpdate`:** 应该是一个纯方法,只根据`nextProps`和`nextState`返回`true`或`false`。不应执行副作用。
* **`render`:** 如前所述,应该是一个纯方法。
* **Strict Mode如何揭示:**
* 它会双重调用`constructor`、`getDerivedStateFromProps`和`render`方法。
* `setState`的更新函数(即`this.setState(prevState => …)`中的函数)也会被双重调用。
* 任何在这些方法中存在的副作用都会被双重执行,导致外部状态不一致或意外行为。
* **为何重要:** 这些方法是类组件生命周期的核心,它们的纯粹性对于组件的可预测性和React的内部优化至关重要。
* **正确实践:** 将副作用移动到`componentDidMount`、`componentDidUpdate`或`componentWillUnmount`中。

### 为什么“双重检查”如此重要?

`Strict Mode`的“双重检查”机制,远不止是为了在开发时提供一些恼人的控制台警告。它的深层意义在于:

1. **为并发模式铺平道路:** 这是最主要的原因。在React的并发模式下,组件的渲染过程可能不再是原子性的“一次性”操作。React可能会在后台“预渲染”组件,然后暂停,再继续,甚至如果更高优先级的更新出现,它可能会完全丢弃之前的预渲染结果,然后从头开始。如果渲染过程中存在副作用,这些副作用可能会在不完全的渲染中被触发,导致数据不一致、UI闪烁或更严重的逻辑错误。`Strict Mode`通过模拟这种不确定性,帮助开发者提前发现并修复这些问题。

2. **提高代码的可预测性:** 当你知道一个函数是纯粹的,你可以更容易地推理它的行为。它不会在你意想不到的时候改变外部世界。这使得调试和维护变得更加简单。

3. **增强组件的可测试性:** 纯粹的组件更容易进行单元测试,因为它们没有外部依赖或副作用,你只需要关注输入和输出。

4. **培养良好的编程习惯:** `Strict Mode`强制开发者养成将副作用与渲染逻辑分离的习惯,这不仅对React有益,对任何现代前端框架的开发都是一种最佳实践。它促使你思考“这个操作应该在哪里执行?”而不是“我可以在这里直接做吗?”

5. **避免难以追踪的Bug:** 副作用如果隐藏在渲染逻辑中,可能在大部分情况下工作正常,但在特定场景(例如,组件快速挂载/卸载,或者在某些边缘条件下React重新尝试渲染)下才会爆发。这种类型的bug往往是最难追踪和修复的。`Strict Mode`将这些潜在问题提前暴露。

### Strict Mode的局限性与最佳实践

尽管`Strict Mode`非常有用,但它并非万能药,也有其局限性:

* **仅限开发模式:** `Strict Mode`的所有检查和双重调用只在开发模式下运行,不会影响生产环境的性能。这意味着它是一种开发辅助工具,而不是运行时保证。
* **并非所有副作用都能检测:** 某些复杂的、依赖于特定时序的副作用可能无法通过简单的双重调用完全暴露。它主要针对那些在渲染或初始化阶段直接发生的副作用。
* **可能产生误报(很少见):** 极少数情况下,如果你的代码在严格模式下行为异常,但并非真正的bug(例如,你故意在渲染函数中生成一个随机数,但只使用第一次的值),这可能会让你感到困惑。但通常,如果严格模式报错,那就是一个值得关注的问题。

**最佳实践:**

* **始终在开发环境中使用`Strict Mode`。** 将你的整个应用包裹在“中,以便全面检查。
* **不要在渲染逻辑中执行副作用。** 将副作用(如DOM操作、网络请求、定时器、修改外部状态)移动到`useEffect`(或类组件的生命周期方法)中。
* **确保所有初始化函数和Reducer函数都是纯函数。** 它们应该只计算和返回一个值,不修改任何外部状态。
* **确保Reducers和`setState`的更新函数保持状态的不可变性。** 始终返回新对象,而不是修改旧对象。
* **彻底测试`useEffect`的清理函数。** 确保它能够完全撤销effect所做的所有操作。

### 严谨性与未来:React开发者的思维转变

`Strict Mode`的双重检查逻辑,实际上是对我们React开发者思维方式的一种引导和要求。它强制我们更加严谨地对待组件的生命周期和状态管理,将“计算UI”和“执行副作用”这两个截然不同的概念清晰地分离。

这种严谨性不仅有助于我们编写出当前更稳定、更可预测的代码,更是为React未来更强大的并发渲染能力做准备。当你的组件能够安全地在任意时间、任意次数被渲染而不产生意外影响时,你就真正掌握了React的精髓,并能够充分利用它带来的高性能和用户体验优势。拥抱`Strict Mode`,就是拥抱更健壮、更现代的React开发实践。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注