考虑下面的片段.使用React 18时,count在每次渲染时都会打印两次到控制台,但使用React 17时,它只打印一次.

React 18 Example:

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>
);
<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>

React 17 Example

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")
);
<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>

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

推荐答案

TL;DR

当组件封装在StrictMode中时,React会运行某些函数两次,以帮助开发人员发现代码中的错误.

在React 18和React 17中都会发生这种情况,但在React 17中,React会自动使第二次调用中的日志(log)静音,所以你没有在React 17中遇到这种情况.

如果提取console.log并使用提取的别名进行日志(log)记录,那么两个版本的行为都会相似.

const log = console.log;

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

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);
<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>

Note:
In React 17, React automatically modifies the console methods like console.log() to silence the logs in the second call to lifecycle functions. However, it may cause undesired behavior in certain cases where a workaround can be used.
Starting from React 18, React does not suppress any logs. However, if you have React DevTools installed, the logs from the second call will appear slightly dimmed. React DevTools also offers a setting (off by default) to suppress them completely.
Source

现在,让我们深入了解在严格模式下实际发生了什么,以及它如何有帮助.


严格模式

严格模式是一个工具,有助于识别在使用React(如不纯渲染)时可能导致problems的编码模式.

In 严格模式 in development时,React将运行以下功能两次:

  • 功能组件
  • 初始化者
  • 更新器

这是因为您的组件、初始值设定项和;更新程序必须是100,但如果不是double-invoking,则可能有助于弥补这个错误.如果它们是纯的,那么代码中的逻辑不会受到任何影响.

Note: React只使用其中一个调用的结果,而忽略另一个调用的结果.

在下面的例子中,观察组件、初始化器和;当包装在StrictMode中时,更新程序在开发期间都会运行两次(snippet使用React的开发构建).

// Extracting console.log in a variable because we're using React 17
const log = console.log; 

function App() {
  const [count, setCount] = React.useState(() => {
    log("初始化者 run twice");
    return 0;
  });

  log("Components run twice");

  const handleClick = () => {
    log("Event handlers don’t need to be pure, so they run only once");
    setCount((count) => {
      log("更新器 run twice");
      return count + 1;
    });
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);
<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>

Few notes from the above example:

  • 您可能已经注意到,当您第一次单击按钮时,更新器 run twice日志(log)只打印一次,但在随后的单击中,它会打印两次.但是你可以忽略这个行为,并假设它总是打印两次,但是如果你想要更多关于同一个的细节,你可以按照这个github issue.

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

  • 调用setCount updater函数两次并不意味着它现在会在每次单击no时增加count两次,因为它在两次调用更新程序时都使用相同的状态.因此,只要更新程序是纯函数,应用程序就不会受到调用次数的影响.

  • "更新程序"&;"初始值设定项"是React中的通用术语.状态更新程序&;州首创者只是众多人中的一个.其他更新程序是传递给useMemo的"回调"和"还原程序".另一个初始值设定项是useReducer个初始值设定项等,这should个函数都是纯函数,所以严格模式双重调用所有函数.查看以下示例:

const logger = console.log;

const countReducer = (count, incrementor) => {
  logger("更新器 [reducers] run twice");
  return count + incrementor;
};

function App() {
  const [count, incrementCount] = React.useReducer(
    countReducer,
    0,
    (initCount) => {
      logger("初始化者 run twice");
      return initCount;
    }
  );

  const doubleCount = React.useMemo(() => {
    logger("更新器 [useMemo callbacks] run twice");
    return count * 2;
  }, [count]);

  return (
    <div>
      <p>Double count: {doubleCount}</p>
      <button onClick={() => incrementCount(1)}>Increment</button>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
<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>

How is 严格模式 helpful?

让我们来看一个例子,严格模式可以帮助我们发现一个严重的错误.

// This example is in React 18 to highlight the fact that 
// the double invocation behavior is similar in both React 17 & 18.

function App() {
  const [todos, setTodos] = React.useState([
    { id: 1, text: "Learn JavaScript", isComplete: true },
    { id: 2, text: "Learn React", isComplete: false }
  ]);

  const handleTodoCompletion = (todoId) => {
    setTodos((todos) => {
      console.log(JSON.stringify(todos));
      return todos.map((todo) => {
        if (todo.id === todoId) {
          todo.isComplete = !todo.isComplete; // Mutation here
        }
        return todo;
      });
    });
  };

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <span
            style={{
              textDecoration: todo.isComplete ? "line-through" : "none"
            }}
          >
            {todo.text}
          </span>
          <button onClick={() => handleTodoCompletion(todo.id)}>
            Mark {todo.isComplete ? "Incomplete" : "Complete"}
          </button>
        </li>
      ))}
    </ul>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<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>

What's the problem with the above example?

你可能已经注意到,这些按钮没有按预期工作,它们没有切换isComplete布尔值,问题是传递给setTodos的更新程序函数是一个纯函数,因为它在todos状态下对对象进行了变异.由于更新程序被调用了两次,而且它不是一个纯函数,所以第二次调用将isComplete布尔值反转回其原始值.

Note:只是因为严格模式的双重调用,我们才能够抓住这个错误.如果我们 Select 退出严格模式,那么组件将按预期工作,但这并不意味着代码编写正确,它只会工作,因为组件是多么孤立,在现实世界中,像这样的Mutations 可能会导致严重问题.即使你幸运地摆脱了这种Mutations ,你仍然可能会遇到问题,因为目前更新程序依赖于这样一个事实,即每次点击只调用一次,但这是not个有react 的东西(with concurrency features in mind).

如果将更新程序设置为纯函数,它将解决以下问题:

setTodos((todos) => {
  logger(JSON.stringify(todos, null, 2));
  return todos.map((todo) =>
    todo.id === todoId ? { ...todo, isComplete: !todo.isComplete } : todo
  );
});

What's new with 严格模式 in React 18

在React 18中,StrictMode获得了一个额外的行为,以确保它与可重用状态兼容.启用严格模式时,React intentionally double-invokes effects (mount -> unmount -> mount) for newly mounted components.这是为了确保组件能够承受多次"安装"和"卸载".与其他严格模式行为一样,React只对开发构建执行此操作.

考虑下面的例子(Source):

function App(props) {
  React.useEffect(() => {
    console.log("效果设置代码运行");

    return () => {
      console.log("效果清理代码运行");
    };
  }, []);

  React.useLayoutEffect(() => {
    console.log("布局效果设置代码运行");

    return () => {
      console.log("布局效果清理代码运行");
    };
  }, []);
  
  console.log("React renders the component")
  
  return <h1>Strict Effects In React 18</h1>;
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
<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>

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

  • React渲染组件(twice, nothing new)
  • 安装组件
  • React simulates the component being hidden or unmounted
  • React simulates the component being shown again or remounted
    • 布局效果设置代码运行
    • 效果设置代码运行

参考阅读

Javascript相关问答推荐

Promise Chain中的第二个useState不更新

如何在bslib nav_insert之后更改导航标签的CSS类和样式?

Prisma具有至少一个值的多对多关系

从包含数百行的表中获取更改后的值(以表单形式发送到后端)的正确方法是什么?

当使用';字母而不是与';var#39;一起使用时,访问窗口为什么返回未定义的?

同一类的所有div';S的模式窗口

如何使用JS创建一个明暗功能按钮?

Reaction Redux&Quot;在派单错误中检测到状态Mutations

如何使用<;Link>;执行与JS Reaction中的";window.Location=/GameOverDied;";类似的操作?

Next.js无法从外部本地主机获取图像

如何修复错误&语法错误:不能在纯react 项目中JEST引发的模块&之外使用导入语句?

如何在尚未创建的鼠标悬停事件上访问和着色div?

Angel Auth Guard-用户只有在未登录时才能访问登录页面,只有在登录时才能访问其他页面

暂停后只有一次旋转JS

浮动标签效果移除时,所需的也被移除

Iterator.Next不是进行socket.end()调用后的函数

TypeScrip Types-向流程对象添加属性

如何使用逗号JS从数组中输出数据列表?

我如何判断css@layer是否在css和Javascript中受支持?

在移动设备上点击脚本元素时出现蓝框