严格模式与 React 18 的工作方式是否不同?

Dev*_*per 11 javascript reactjs react-strictmode

考虑下面的片段。在 React 18 中,count每次渲染都会在控制台上打印两次,但在 React 17 中只打印一次。

反应 18 示例:

function App() {
  const [count, setCount] = React.useState(0);
  console.log(count);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
Run Code Online (Sandbox Code Playgroud)
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div id="root"></div>
Run Code Online (Sandbox Code Playgroud)

反应 17 示例

function App() {
  const [count, setCount] = React.useState(0);
  console.log(count);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);
Run Code Online (Sandbox Code Playgroud)
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="root"></div>
Run Code Online (Sandbox Code Playgroud)

我知道这与什么有关,StrictMode但我不确定是什么。而且我一直不清楚严格模式是如何工作的以及它的目的是什么,所以如果有人也能强调这一点,我将不胜感激。

Som*_*jee 33

长话短说

\n

当组件被包装在 中时StrictMode,React 会运行某些函数两次,以帮助开发人员捕获代码中的错误。

\n

这种情况在 React 18 和 React 17 中都会发生,但您在后者中没有遇到这种情况的原因是因为在 React 17 中,React 自动在第二次调用中静默日志。

\n

如果您提取console.log并使用提取的别名进行记录,那么两个版本都会得到类似的行为。

\n

\r\n
\r\n
const log = console.log;\n\nfunction App() {\n  const [count, setCount] = React.useState(0);\n  log(count);\n  return <button onClick={() => setCount(count + 1)}>{count}</button>;\n}\n\nReactDOM.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n  document.getElementById("root")\n);
Run Code Online (Sandbox Code Playgroud)\r\n
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>\n<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>\n<div id="root"></div>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

\n

注意:\n
\n在 React 17 中,React 会自动修改控制台方法,例如console.log()在第二次调用生命周期函数时静默日志。但是,在某些可以使用解决方法的情况下,它可能会导致不良行为。\n
\n从 React 18 开始,React 不会抑制任何日志。但是,如果您安装了 React DevTools,第二次调用的日志将稍微变暗。React DevTools 还提供了一个设置(默认情况下关闭)来完全抑制它们。\n
\n源代码

\n
\n

现在让我们深入了解严格模式下实际发生的情况以及它有何帮助。

\n
\n

严格模式

\n

严格模式是一个工具,可帮助识别在使用 React 时可能导致问题的编码模式,例如不纯渲染。

\n

在开发的严格模式下,React 会运行以下函数两次:

\n
    \n
  • 功能组件
  • \n
  • 初始化器
  • \n
  • 更新程序
  • \n
\n

这是因为您的组件、初始化程序和更新程序需要是纯函数,但如果它们不是\xe2\x80\x99t,那么双重调用它们可能有助于发现此错误。如果它们是纯粹的,那么代码中的逻辑就不会受到任何影响。

\n

注意: React 仅使用其中一个调用的结果,并忽略另一个调用的结果。

\n

在下面的示例中,观察到组件、初始化程序和更新程序在开发过程中包装时都运行两次StrictMode(代码片段使用 React 的开发版本)。

\n

\r\n
\r\n
// Extracting console.log in a variable because we\'re using React 17\nconst log = console.log; \n\nfunction App() {\n  const [count, setCount] = React.useState(() => {\n    log("Initializers run twice");\n    return 0;\n  });\n\n  log("Components run twice");\n\n  const handleClick = () => {\n    log("Event handlers don\xe2\x80\x99t need to be pure, so they run only once");\n    setCount((count) => {\n      log("Updaters run twice");\n      return count + 1;\n    });\n  };\n\n  return (\n    <div>\n      <p>Count: {count}</p>\n      <button onClick={handleClick}>Increment</button>\n    </div>\n  );\n}\n\nReactDOM.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>,\n  document.getElementById("root")\n);
Run Code Online (Sandbox Code Playgroud)\r\n
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>\n<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>\n<div id="root"></div>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

上面例子的一些注释:

\n
    \n
  • 您可能已经注意到,当您第一次单击该按钮时,Updaters run twice日志仅打印一次,但在后续单击时,它会打印两次。但是您可以忽略此行为并假设它总是打印两次,但如果您想了解更多详细信息,您可以关注此github 问题

    \n
  • \n
  • 我们必须提取console.log到一个单独的变量中才能获取打印的两个调用的日志,这是因为 React 17 自动静默第二次调用的日志(如 TL;DR 中所述)。如果您将 CDN 链接更新到 React 18,则不需要进行此提取。

    \n
  • \n
  • 调用setCount更新程序函数两次并不意味着它现在count每次点击都会增加两次,,因为它两次调用具有相同状态的更新程序。因此,只要您的更新程序是纯函数,您的应用程序就不会受到 no 的影响。它\xe2\x80\x99 调用的次数。

    \n
  • \n
  • “更新程序”和“初始化程序”是 React 中的通用术语。状态更新器和状态初始化器只是其中之一。其他更新程序是传递给“回调”useMemo和“缩减程序”。另一个初始化器是useReducer初始化器等。所有这些都应该是纯函数,因此严格模式双精度调用所有这些。看看这个例子:

    \n
  • \n
\n

\r\n
\r\n
const logger = console.log;\n\nconst countReducer = (count, incrementor) => {\n  logger("Updaters [reducers] run twice");\n  return count + incrementor;\n};\n\nfunction App() {\n  const [count, incrementCount] = React.useReducer(\n    countReducer,\n    0,\n    (initCount) => {\n      logger("Initializers run twice");\n      return initCount;\n    }\n  );\n\n  const doubleCount = React.useMemo(() => {\n    logger("Updaters [useMemo callbacks] run twice");\n    return count * 2;\n  }, [count]);\n\n  return (\n    <div>\n      <p>Double count: {doubleCount}</p>\n      <button onClick={() => incrementCount(1)}>Increment</button>\n    </div>\n  );\n}\n\nconst root = ReactDOM.createRoot(document.getElementById("root"));\nroot.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);
Run Code Online (Sandbox Code Playgroud)\r\n
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>\n<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>\n<div id="root"></div>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

严格模式有何帮助?

\n

让我们看一个示例,其中严格模式可以帮助我们发现严重错误。

\n

\r\n
\r\n
// This example is in React 18 to highlight the fact that \n// the double invocation behavior is similar in both React 17 & 18.\n\nfunction App() {\n  const [todos, setTodos] = React.useState([\n    { id: 1, text: "Learn JavaScript", isComplete: true },\n    { id: 2, text: "Learn React", isComplete: false }\n  ]);\n\n  const handleTodoCompletion = (todoId) => {\n    setTodos((todos) => {\n      console.log(JSON.stringify(todos));\n      return todos.map((todo) => {\n        if (todo.id === todoId) {\n          todo.isComplete = !todo.isComplete; // Mutation here\n        }\n        return todo;\n      });\n    });\n  };\n\n  return (\n    <ul>\n      {todos.map((todo) => (\n        <li key={todo.id}>\n          <span\n            style={{\n              textDecoration: todo.isComplete ? "line-through" : "none"\n            }}\n          >\n            {todo.text}\n          </span>\n          <button onClick={() => handleTodoCompletion(todo.id)}>\n            Mark {todo.isComplete ? "Incomplete" : "Complete"}\n          </button>\n        </li>\n      ))}\n    </ul>\n  );\n}\n\nconst root = ReactDOM.createRoot(document.getElementById("root"));\nroot.render(<App />);
Run Code Online (Sandbox Code Playgroud)\r\n
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>\n<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>\n<div id="root"></div>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

上面的例子有什么问题呢?

\n

您可能会注意到这些按钮无法按预期工作,它们不会切换布尔isComplete值,问题是传递给的更新器函数setTodos不是纯函数因为它会改变状态中的对象todos。由于更新程序被调用两次,并且它不是纯函数,因此第二次调用将isComplete布尔值反转回它\xe2\x80\x99s 的原始值。

\n

注意:只是因为严格模式的双重调用,我们才能够捕获这个错误。如果我们选择退出严格模式,那么该组件将幸运地按预期工作,但这并不意味着代码编写正确,它只能工作,因为组件是如何隔离的,在现实世界中,像这样的突变可能会导致严重的后果问题。即使您幸运地摆脱了此类突变,您仍然可能会遇到问题,因为目前更新程序依赖于每次点击仅调用一次的事实,但这不是React所保证的(考虑到并发功能) 。

\n

如果您将更新程序设置为纯函数,则可以解决该问题:

\n
setTodos((todos) => {\n  logger(JSON.stringify(todos, null, 2));\n  return todos.map((todo) =>\n    todo.id === todoId ? { ...todo, isComplete: !todo.isComplete } : todo\n  );\n});\n
Run Code Online (Sandbox Code Playgroud)\n

React 18 中严格模式的新增功能

\n

在 React 18 中,\xc2\xa0 StrictMode\xc2\xa0 获得额外的行为以确保其与可重用状态兼容。当启用严格模式时,\xc2\xa0 React 会故意为新安装的组件双重调用效果 (mount\xc2\xa0->\xc2\xa0unmount\xc2\xa0->\xc2\xa0mount)。这是为了确保组件能够多次“安装”和“卸载”。与其他严格模式行为一样,React 仅针对开发构建执行此操作。

\n

考虑下面的例子(来源):

\n

\r\n
\r\n
setTodos((todos) => {\n  logger(JSON.stringify(todos, null, 2));\n  return todos.map((todo) =>\n    todo.id === todoId ? { ...todo, isComplete: !todo.isComplete } : todo\n  );\n});\n
Run Code Online (Sandbox Code Playgroud)\r\n
function App(props) {\n  React.useEffect(() => {\n    console.log("Effect setup code runs");\n\n    return () => {\n      console.log("Effect cleanup code runs");\n    };\n  }, []);\n\n  React.useLayoutEffect(() => {\n    console.log("Layout effect setup code runs");\n\n    return () => {\n      console.log("Layout effect cleanup code runs");\n    };\n  }, []);\n  \n  console.log("React renders the component")\n  \n  return <h1>Strict Effects In React 18</h1>;\n}\n\nconst root = ReactDOM.createRoot(document.getElementById("root"));\nroot.render(\n  <React.StrictMode>\n    <App />\n  </React.StrictMode>\n);
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

上面的组件App声明了一些要在安装和卸载时运行的效果。在 React 18 之前,设置函数只会运行一次(在组件最初安装之后),清理函数也只会运行一次(在组件卸载之后)。但在 React 18 中StrictMode,会发生以下情况:

\n
    \n
  • React 渲染组件(两次,没有什么新内容
  • \n
  • React 安装组件\n
      \n
    • 布局效果设置代码运行
    • \n
    • 效果设置代码运行
    • \n
    \n
  • \n
  • React 模拟组件被隐藏或卸载\n
      \n
    • 布局效果清理代码运行
    • \n
    • 效果清理代码运行
    • \n
    \n
  • \n
  • React 模拟组件再次显示或重新安装\n
      \n
    • 布局效果设置代码运行
    • \n
    • 效果设置代码运行
    • \n
    \n
  • \n
\n

建议读物

\n\n