为啥在 React 组件中调用了两次 Promise.then 而不是 console.log?

Posted

技术标签:

【中文标题】为啥在 React 组件中调用了两次 Promise.then 而不是 console.log?【英文标题】:Why is `Promise.then` called twice in a React component but not the console.log?为什么在 React 组件中调用了两次 Promise.then 而不是 console.log? 【发布时间】:2021-09-18 08:18:59 【问题描述】:

我对以下组件的输出感到非常困惑:

import  StrictMode  from "react"
import ReactDOM from "react-dom"

function Test(): React.ReactElement 
    console.log('render')
    Promise.resolve()
        .then(() => console.log('then ' + Math.random()))
    return <></>


ReactDOM.render(
  <StrictMode>
    <Test />
  </StrictMode>,
  document.getElementById("root")
)

它至少在 Chrome 和 Firefox 中产生以下输出:

00:46:30.264 render
00:46:30.267 then 0.5430663800781927
00:46:30.267 then 0.9667426372511254

我更希望看到相同数量的消息。我错过了什么?

复制:https://codesandbox.io/s/elegant-frost-dmcsl

编辑:我知道严格模式会导致额外的渲染,但如前所述,我会期待相同数量的消息。

编辑 2:以下两个答案都很棒。我想在这里引用@user56reinstatemonica8 的评论:

相关:Community feedback on console silencing

【问题讨论】:

请添加所有的代码来重现此问题,而不仅仅是沙箱的链接。 &lt;StrictMode&gt; 实际上是您问题的关键。 是的,但为什么render 只打印一次? 【参考方案1】:

在 React strict mode 中,react 可能会运行渲染多次次,这可以部分解释你所看到的。

但您正确地想如果是这样并且多次调用 render,为什么render 也没有打印两次

在某些情况下,React 会修改 console.log() 等控制台方法以使日志静音。这是一个引用:

从 React 17 开始,React 自动修改控制台 像 console.log() 这样的方法在第二次调用中使日志静音 生命周期函数。但是,它可能会导致不良行为 在某些情况下可以使用变通方法。

显然,当从 Promise 回调调用 console.log 时,它不会这样做。但是当它从渲染中调用时它会这样做。 @trincot 的回答中有更多详细信息。

【讨论】:

...这也是为什么你不应该从渲染方法内部安排异步回调(或者更糟糕的是,启动异步工作),而只能从效果中 - 效果只会运行一次。 @Bergi 这就是为什么随机框架不应该接触系统函数(通常用于调试)来做意想不到的事情的原因。【参考方案2】:

当启用严格模式(仅在开发模式下)时,您的渲染函数会第二次运行,但正如 here 所讨论的,React 将在此期间修补 console 方法(调用 disableLogs();)第二次(同步)运行,使其不输出。

changelog 显示此代码已插入 packages/react-reconciler/src/ReactFiberBeginWork.js 以暂时禁止日志(带有注释的插入):

  if (__DEV__) 
    ReactCurrentOwner.current = workInProgress;
    setIsRendering(true);
    nextChildren = renderWithHooks(
      current,
      workInProgress,
      render,
      nextProps,
      ref,
      renderExpirationTime,
    );
    if (
      debugRenderPhaseSideEffectsForStrictMode &&
      workInProgress.mode & StrictMode
    ) 
      disableLogs();       // <--
      try                 // <--
        nextChildren = renderWithHooks(
          current,
          workInProgress,
          render,
          nextProps,
          ref,
          renderExpirationTime,
        );
       finally           // <--
        reenableLogs();    // <--
                          // <--

这是你的代码的一个版本,它证明它确实运行了两次:

var i = 0;
var myconsolelog = console.log; // Work around React's monkeypatching 

function Test(): React.ReactElement 
    i++;
    myconsolelog(i + ". render"); // will output twice now!
    Promise.resolve(i)
        .then((i) => console.log(i + ". then " + Math.random()));
    return <></>;

在我看来,这种日志抑制是一个非常糟糕的设计选择。

【讨论】:

是的,在这个changelog 中看到,在renderWithHooks 的调用周围看到disableLogsreenableLogs 对。添加信息以回答。 是的...不知道为什么他们觉得需要猴子补丁控制台方法。我知道这第二次渲染可能是一个惊喜......但恕我直言,最好写一条调试消息“开始对组件 X 进行第二次渲染,请参阅https://blah 了解为什么会在开发模式下发生这种情况”,即他们应该添加了他们对为什么日志重复而不是使它们静音的解释......鉴于用户可以检测到双重渲染的许多其他方式,隐藏它并没有真正意义它... 是的,或者为了减少噪音,在第二个渲染周期开始时,他们可以执行console.groupCollapsed('Starting dev-only second render, see [link] for info')之类的操作,最后运行console.groupEnd,然后捕获所有第二个渲染日志消息折叠的手风琴。最小的噪音,没有隐藏的惊喜,清楚发生了什么。 相关:Community feedback on console silencing 我同意,这个猴子补丁是一个糟糕的选择。也许,他们的开发人员经常使用console.log? ;)【参考方案3】:

如果它可能对任何人有所帮助,您可以对 Object.defineProperties 进行猴子修补以忽略对 console 对象所做的任何更改,从而有效地防止 ReactDOM 对 console.log 进行猴子修补。

const defineProperties = Object.defineProperties;
Object.defineProperties = function (o, props) 
  return o === console ? o : defineProperties(o, props);
;

确保仅将其置于开发模式(例如,process.env.NODE_ENV === 'development' 时置于 create-react-app 中),这样它就不会在生产构建中结束。

【讨论】:

以上是关于为啥在 React 组件中调用了两次 Promise.then 而不是 console.log?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 ngOnInit 被调用两次?

由于严格模式,我的 React 组件渲染了两次

为啥在 Swift 中 CollectionView 函数被调用了两次?

didUpdateToLocation 调用了两次,好的。为啥 oldLocation 两次都为零?

libevent:为啥 evutil_make_socket_nonblocking 在他们的示例中调用了两次

尽管我们在父组件中更改了两次属性的值,但 ngOnChanges 没有被调用两次