略微探究React StrictMode两次渲染的问题

略微探究React StrictMode两次渲染的问题今天讨论了个问题,整个过程蛮有意思,作为一个小知识点记下~ 问题就是在开发过程中发现组件会渲染两次,结果知道了是strict mode故意为之,但是继续下去发现组件外层cosnole却只打印1次,咦?

🐒 今天和小伙伴讨论了个问题,整个过程还蛮有意思的,作为一个小知识点记下吧~

问题:

下面这段代码为什么会输出两个1 ?

function App() {
  setTimeout(() =>{
    console.log(1)
  }, 1)
  return 100
}

ReactDOM.render(
  <React.StrictMode> <App /> </React.StrictMode>, 
  document.getElementById('root')
);

虽然这段代码看上去很奇怪,但是我一试果然输出了两次。

这个问题的答案简单一搜就搜出来了:

知乎:www.zhihu.com/question/38…

github issue: github.com/facebook/re…

This is not a bug. And you’ll have the same behavior in Strict Mode too. We intentionally double-call render-phase lifecycles in development only (and function components using Hooks) to help people find issues caused by side effects in render. In our experience, them firing twice is enough for people to notice and fix such bugs.

If component output is always a function of props and state (and not outer scope variables, like in your example), the double rendering in development should have no observable effect.

大概意思就是 在concurrent mode 和 strict mode 的 开发阶段,为了帮助开发者定位问题,react会故意的两次调用render阶段,他们认为这样能引起开发者的注意。

换句话说,上面这段代码是存在问题的:setTimeout是一个Side Effect,但是却没有写在useEffect中。react在用这种行为(2次调用timeout)提醒开发者

比如你把timeout换成Promise也是一样:

function App() {
  // debugger;
  Promise.resolve(1).then(() => {
    console.log('testing',);
  })
  return 100
}

所以就理解成,没有写在useEffect的中的异步副作用都会被触发两次, 目的就是引起开发者的注意。这也符合严格模式的理念:严格模式

在App组件里面debugger之后也发现了确实是走了两遍的render阶段。所以这个问题就这样解决了?

旧的问题才下眉头,新的问题很快就却上心头了:

我把这个结论讲给了小伙伴,小伙伴在自己验证的过程中发现了新的问题:

function App() {
  console.log('render...')
  setTimeout(() =>{
    console.log(1)
  }, 1)
  return 100
}

为了验证App被调用了两次,很自然的想到了用console来验证,就像上面的代码中那样。但是,结果并不符合预期,结果是这样:

render...
1
1

这就很奇怪了,为什么timeout的console走了2次而外层的console就走了一次?不是说走两遍render阶段?那岂不是外层的console也应该走两次才对?

这个问题真的百思不得7姐~

我甚至推测,react是不是在strict mode下做了语法分析:当分析该语句是side effect的时候就插入一个同样的语句,最终执行的代码并不是开发者编写的代码。

但是这种猜测很快就被推翻了,因为:1)这种行为和官方解释不符合;2)刚才在debug的过程中看的的也不是这样;

最终还是在万能的stackOverflow中发现了问题的解答(为了表示感谢,我决定把我的电脑的苹果标志用stackOverflow的贴纸代替,并说一句yyds):

略微探究React StrictMode两次渲染的问题

Why is Promise.then called twice in a React component but not the console.log?(这个提问者遇到了几乎一样的问题)

这是高赞回答:

In React strict mode react may run render multiple times, which could partly explain what you see.

But you correctly wondered if that was the case and render was called multiple times, why was render not printed twice too?

React modifies the console methods like console.log() to silence the logs in some cases. Here is a quote:

Starting with 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.

Apparently, it doesn’t do so when the console.log is called from Promise callback. But it does so when it is called from render. More details about this are in the answer by @trincot.

简单意译下,他说:

strict mode的两次渲染只能部分解释这个问题。但你其实想知道为什么不打印两次?React其实改变了console方法,让它在一些情况下不打印。他引用了一段引文,说是从React17开始,console方法被修改了,在第二次调用生命周期方法的时候不打印,但这确实引起了一些不在期望内的行为。

这篇stackOverflow的第二个高赞回答更细节一点:

There is a second run of your render function when strict mode is enabled (only in development mode), but as discussed here, React will monkey patch console methods (calling disableLogs();) for the duration of that second (synchronous) run, so that it does not output.

他指出,在development mode且strict mode的第二次render的时候为console打了补丁(猴子补丁),这导致了console不输出。而timeout中回调的代码没有在React Render上下文中,所以不受影响。

在回答的最后,答主还不忘喷一句:”In my opinion, this log-suppression is a really bad design choice.”

if (__DEV__) {
  // ...
  if (
    debugRenderPhaseSideEffectsForStrictMode &&
    workInProgress.mode & StrictMode
  ) {
    disableLogs();       // <-- 猴子补丁 修改 console
    try {                 
      // ...
    } finally {          
      reenableLogs();    // <-- 恢复 console
    }                    
  }
}

上面的伪代码中,disableLogs修改 console,reenableLogs又恢复了console。

其实,具体React中disableLogs也比较简单,就是把原来的info,log等方法都暂存起来,下面代码里面的prev开头的变量就。然后利用Object.defineProperties把这些方法都改成disableLogs,同时层级深度disabledDepth 计数加一,这个变量也是为了恢复方法reenableLogs中和disableLogs成对生效用的(disableLogs就不贴了)。

let disabledDepth = 0;
let prevLog;
let prevInfo;
let prevWarn;
let prevError;
let prevGroup;
let prevGroupCollapsed;
let prevGroupEnd;

function disabledLog() {}
disabledLog.__reactDisabledLog = true;

export function disableLogs(): void {
  if (__DEV__) {
    if (disabledDepth === 0) {
      /* eslint-disable react-internal/no-production-logging */
      prevLog = console.log;
      prevInfo = console.info;
      prevWarn = console.warn;
      prevError = console.error;
      prevGroup = console.group;
      prevGroupCollapsed = console.groupCollapsed;
      prevGroupEnd = console.groupEnd;
      // https://github.com/facebook/react/issues/19099
      const props = {
        configurable: true,
        enumerable: true,
        value: disabledLog,
        writable: true,
      };
      // $FlowFixMe Flow thinks console is immutable.
      Object.defineProperties(console, {
        info: props,
        log: props,
        warn: props,
        error: props,
        group: props,
        groupCollapsed: props,
        groupEnd: props,
      });
      /* eslint-enable react-internal/no-production-logging */
    }
    disabledDepth++;
  }
}

所以,继续大胆的验证下,如果把APP中的console换成alert,或者把原生的console引用起来使用,就应该能得到预期的结果:

const myPrint = console.log;

function App() {
  myPrint('render...')
  setTimeout(() =>{
    console.log(1)
  }, 1)
  return 100
}

经验证,果然也!

那么,到这里,问题就清楚了,两个要点:

  1. strict mode的开发模式下确实会渲染两次
  2. 第二次渲染中console会经历修改和还原,这导致第二次渲染的console不会输出;

大家中秋快乐~ 🥮 🌕 👬

今天的文章略微探究React StrictMode两次渲染的问题分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/17674.html

(0)
编程小号编程小号

相关推荐

发表回复

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