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
参考阅读