为什么会无限重新渲染?

fezl
  • 35

代码如下:

import React, { useState, useEffect } from 'react';

function EffectTest() {
    const [count, setCount] = useState(0);
    useEffect(() => {
        console.log(count);
        setTimeout(() => {
            setCount(1);
            setCount(0);
        });
    });
    return (
        <div>
            <span>{count}</span>
        </div>
    )
}

export default EffectTest;

控制台:0,1,0,1,0,1......(无限循环)

求大佬解答,感激不尽。

回复
阅读 894
2 个回答
前来填坑了,更新时间 2021-5-9

(1)问题描述: 以下代码为什么会打印两个 0 和两个 1

function Foo() {
    const [num, setNum] = useState(0);
    console.log(num);
    setTimeout(() => {
        setNum(1);
    });
    return (
        <div>
            {num}
        </div>
    )
}

由于react真正在运行时,真正的打印的结果是一个 0,和两个 1,前面会多打印出一个零应该是和 hot reladong机制有关,实际上直接在浏览器中引入umd版本是不会多打印一个零的,所以我们现在的问题就是 为什么以上代码会打印一个零和两个一

(2)需要的前置知识

要理解接下来的内容,需要你至少对react的原理已经有了一定的了解,如果还不知道的话可以去通读一下 react技术揭秘

(3)更新终止

  1. 更新何时会终止

    • 没有更多的dispatchAction来派发更新,比如如下代码,仅在第一次渲染后就没有更多任务了

    image.png

  2. 我们的代码终止属于那种情况:不难看出我们的属于第二种因为我们只要运行Foo Component setTimeout中的dispatchAction是一直在派发更新的

(4)相关描述的解释(接下来分析代码运行会用到

  1. 我把每次渲染在图中分成了三个阶段

    • 渲染前: 也就是上轮渲染两颗树最终的形态,其中首次渲染时两棵树都为空
    • 渲染后: 此阶段也就是,在render阶段中构建出新的workInProgress树的过程,在此过程中如果没有达到终止条件,就会调度该次更新 会把两棵树的lanes都设置为为相关的lanes,然后render阶段完成后workInProgress的lanes又会被置为NoLanes,current树上的lanes信息会被保留
    • 两棵树交换: 此阶段处于commit阶段完成后,对两棵树进行交换
  2. lanes: 在记录组件的lanes时我们把有值记为 1, 没有值(也就是NoLanes)记为 0

(5)更新流程(在此过程中我们会记录两个fiber树的变化,两颗fiber树中Foo组件的lanes和setTimeout派发的任务的任务队列

  1. 初次渲染 num 0 渲染到页面,然渲染完成后,两棵树交换,并在执行Foo Component的时候setTimout将 setNum(1)推入任务队列(渲染后的workInProgress树中的Foo组件对应的fiber节点的lanes一定为NoLanes也就是 0,原因通过render阶段的所有workInPorgress节点的lanes都会被赋值为NoLanes
    image.png
  2. 出队setNum(1)并执行,此时的队列为空,虽然此时的两棵树的lanes都为NoLanes但是前后状态并不相同所以达不到终止条件(详情看(3)终止条件),会继续调度该更新,渲染后会将两颗fiber树的lanes设为有值,然后workInProgress树中的lanes又会被置为NoLanes,最有交换两棵树(详细看(4)相关解释),并且由于又执行了一次Foo Component所以在此向任务队列中推进去一个setNum(1)
    image.png

3.队列中任然不为空,再次出队setNum(1)并且执行,在dispatchAction中由于此时workInProgress树中的lanes不为NoLanes所以即使前后的状态并没有改变,也并没有触发更新的终止条件所以会继续进行接下来的render和commit阶段,所以又再次执行了一次Foo Component,但是这次执行Foo Componet有所不同,由于Foo Component的父组件HostRoot没有更新所以Foo Component更新前后的props没有变化,且在执行到组件内部useState的时候react发现前后的状态并没有改变
所以不会将didReceiveUpdate 置为true, 然后didReceiveUpdate 的值将一直保持为false 所以会执行Foo Component的 bailoutHooks逻辑将current Fiber树中Foo Component的lanes置为NoLanes,所以完成render阶段后两颗树中的Foo Component的lanes都为 NoLanes
image.png
可以发现此时两颗树的lanes都为NoLanes了,不过由于再次执行了Function Component setTimeout又再次向任务队列中推入了一个 setNum(1)

4.任务队列中仍然不为空,出队setNum(1)执行,在dispatchAction中发现两棵树的lanes都为NoLanes且前后的状态都一样,直接return不会再调度该次任务,到此整个更新流程就完整的结束了

(6)打印信息分析

根据(5)中的更新流程我们可以分析一下打印信息其中
(5)-1中初次渲染时,num与初始值相等也就是0然后被打印出来
(5)-2 dispatchAction触发新的更新,并且action中的 1替代了原来的 0此次执行Foo Component是 num为 1然后被打印出来
(5)-3中没有触发终止条件又执行了一次 Foo Component但是此次的 num并没有变化还是 1,然后被打印出来
(5)-4中触发终止条件,直接退出

(7)更多

在(5)-3中虽然前后state都没有变化但是还是进行了render和commit阶段,会不会有性能浪费,实际上在(5)-3中执行了Foo Component的 bailout逻辑后此时出触发该更新的fiber节点,也就是Foo Component本身的lanes也会被设置为NoLanes所以本次更新并没有实质性的更新所以在commit阶段是不用进行commit工作


beforeMutation -> scheduleEffect -> layout -> effect执行 -> dispatchAction -> beforeMutation 永远无法停下,而加上dependenciesArr react发现dependencies没有变所以会在dispatchAction之前停止,还有一个细节是setCount是放在settimeout中的不在BatchedExecuteContext中,不然是只会参考最有一个setCount的值
为useEffect加上dependenciesArr

import React, { useState, useEffect } from 'react';

function EffectTest() {
    const [count, setCount] = useState(0);
    useEffect(() => {
        console.log(count);
        setTimeout(() => {
            setCount(1);
            setCount(0);
        });
    },[]);
    return (
        <div>
            <span>{count}</span>
        </div>
    )
}

export default EffectTest;

因为组件一直在重新渲染。

你可以观察我写的 demo

你知道吗?

宣传栏