查看原文
其他

你不知道的 React v18 的任务调度机制

洛城一别 React中文社区 2022-05-31

React的任务调度机制是我早就想要探究但直到现在才基本捋清楚的一个议题。因为想要弄清楚React中任务的调度,需要的前置知识还是蛮多的,首先你要对jsx->vNode->fiber树和hooks的实现有源码级的了解,其次要对scheduler这个独立的包有源码级的了解,然后你才有可能探究React的任务调度。所以从我想了解到基本了解,已经有大概两年多的时间了。

期间我读过一些社区关于任务调度的文章,但感觉都比较抽象,缺乏结合实例相关的讲解,导致我虽然理解了一部分,但无法把源码中的逻辑和日常开发中的代码对应上,进而无法深入。

因此我试图写一篇更具体易懂的关于React调度的文章。我的叙事逻辑是先说明一些最重要的概念具体指什么,然后在分析一些日常代码的具体调度逻辑时,点出React调度的关键行为,我觉得这样是更好的说明方式。

文章以最新的React v18@beta版本为背景,不过整体逻辑应该与v17版本的差别并不大。

任务、调度和更新的具体含义

任务

在React语境下谈调度任务时,任务是指指遍历整个fiber树,找到需要更新的fiber并执行更新的过程。

调度

调度的核心逻辑就是利用浏览器提供的queueMicrotask或者message channel api,把遍历fiber树这个任务压入浏览器微任务队列等待执行。

调度由useState生成的setState方法引发。

更新

这里我要讲的更新不是宏观的更新动作,而是一个更新实体,虽然初听有些怪异,但其实想一下就能明白,一次宏观更新动作有一个对象来记录本次更新相关信息是很自然的。

在React内部更新实体是这样的:

var update = {
    // 此时lane为1,代表同步更新的优先级(直接调用setCounter是同步的更新)
    lane: lane,
    // counter状态本次动作要被修改为的值,也就是1
    action: action,
    // 马上会被置为true
    hasEagerState: false,
    // 最新状态,也就是1
    eagerState: null,
    // 下一个任务,可能在下轮渲染前产生了多个更新,这时next就指向同一个状态的后一个update对象
    next: null
};

update对象上其实就是存储了更新的优先级以及要更新为的最新值。

update对象在setState时产生,产生后会挂在到组件fiber对应的hook上。

在最终重新执行组件函数,重新执行useState等hook时会消费update,把更新结果赋值给组件函数内的状态。

优先级

无论是任务、调度还是更新实体本身,都和优先级这个概念密切相关。我们只探究两种有代表性的优先级:最高优的同步优先级和较低优的transition优先级。

其中同步优先级是直接调用setState产生的,而transition任务是通过v18版本的新api,startTransition函数包裹setState产生的:

const [counter, setCounter] = useState(0);
const [isPending, startTransition] = useTransition();

startTransition(() => {
    // 这次更新产生的任务是transition优先级的
    setCounter(counter + 1)
})

React把同步任务的lane设为1,而transition任务的lane为64。

两种优先级任务的区别

同步任务和transition任务的区别在于遍历fiber树的过程是否可中断。

同步任务是会一次性遍历整个fiber树的,无论花费了多长时间。

但要注意同步优先级任务从宏观上并不是同步的。

同步任务这个说法很容易让人误解成整个更新过程是同步的,但实际上同步优先级的任务在整个更新过程中并不是同步的。

很容易验证这一点:

const Demo = () => {
    console.log('rerender');
    const [counter, setCounter] = useState(0);

    function handleClick() {
        setCounter(counter + 1)
        console.log('紧随setCounter后的代码');
    }

    return (
        <>
            <div>{counter}</div>
            <div onClick={handleClick}>点我发起同步优先级的更新任务</div>
        </>
    )
}

点击按钮,你会发现先log “紧随setCounter后的代码”后log “rerender”。这说明从生成update对象到重新执行组件函数并在commit phase更新DOM节点并不是一气呵成的。

实际上,任何优先级的任务都要被调度,而调度的核心逻辑就是加入浏览器的微任务队列,所以任何任务的执行从整体上来看都是异步的。

之所以叫同步优先级,是指任务执行时是同步执行完毕不可中断,而不是从发起到执行完毕全过程是同步的。

而transition优先级的任务的执行是可以中断的,执行任务也就是遍历fiber树的过程中,每次完毕完一个fiber都会检查执行时间是否超过5ms,如果超过了,那么就把剩余任务压入浏览器微任务队列,让出线程响应其他事件。

这种5ms一次的让步会一直持续到fiber树遍历完毕或者任务超时,任务超时则会被不再让步而是一次性遍历完剩余的fiber树并完成渲染DOM等所有具体更新。

两种优先级调度的区别

如果只论调度的核心逻辑,那么区别不大,都是用浏览器api加入浏览器微任务队列,只不过同步优先级的任务是用queueMicrotask这个api加入的,而transition优先级的任务是用message channel这个api加入的。

两种优先级更新的区别

同步优先级update的lane为1,hasEagerState为true,eagerState会是最新值。

transition优先级update的lane为64,hasEagerState为false,eagerState为null。

在整个更新阶段的最后,重新执行组件函数,重新执行useState等hook时会消费update。

这里需要说明下消费update的逻辑,会从fiber对应hook的queue上取到所有的update,实际上一次完整的更新流程中可能一个状态是可能有不同优先级的update的,这些update以环形链表的形式存储在hook的queue对象上。

每次更新流程都有一个renderLanes表示本次更新的赛道,所有update只要是这些赛道的子集就可以把update上的最新值返回给函数组件,也就意味着在本轮渲染更新。这个renderLanes是哪来的呢?通常来说renderLanes就是每次渲染时取所有update中最高优的那个。

比如本轮有同步更新,那renderLanes就是1,所有同步优先级的update都可以把自己上面维护的状态返回给组件函数并最终渲染到DOM上。

而本轮所有transition优先级的update就需要在queue上继续排队等着,无法在本轮更新到DOM上。

lane在更新过程中的作用

上面说了各个环节中优先级的作用,但是整个流程还没串起来,下面以lane为线索讲解下整个流程。

先说在同步更新的过程中,lane到底起了什么作用呢?首先,lane最重要的作用是表示优先级,同步任务的优先级用1表示。所以我们也就生产了的update上lane属性的值就是1。

我们知道一个组件对应一个fiber,更新任务是属于组件的,所以组件fiber上的lanes属性被设置为1,这从语义上是说组件有且仅有一个同步的更新待消费。

而组件函数上面可能还有父组件,一定还有根组件。而子组件的任务一定也是父组件的任务,所以这个lane会被不断往上合并,所有的父级fiber上的childLanes属性都会被设置为1。这意味着父级的fiber上有所有子树上未完成任务的优先级之和。

比如同步发起了一个同步任务,一个transition任务,那么Root Fiber上的childLanes属性就会是65。

在同步任务的消费任务被执行时,会从Root Fiber开始遍历。遍历每个节点时会检查两个属性:

  • 当前fiber的lanes属性,也就是表示当前fiber上的更新任务的优先级
  • 整个子树上最高优任务的优先级

这里其实就能领悟到lane的真正含义,也就是赛道。每个任务根据优先级会划分不同的赛道,比如同步优先级的用lane为1这个赛道,而transition优先级用lane为64的赛道。不同赛道是二进制的不同位。React中一共规划了31位表示31条赛道,位数越小的赛道优先级越高。

通常来说,不同优先级是位于不同的赛道的。但是一个赛道上可以有多个任务,比如同时有两个同步任务时,两个任务都是在lane为1的赛道上,都表示同步优先级。

了解了赛道的设计,我们可以从Root Fiber的childLanes 65就很容易解读出当前子树上有两个赛道,64赛道和1赛道。在遍历到某个fiber时,我们检查当前fiber的任务是不是属于最高优赛道。

从Root Fiber到发生setState的所有fiber都没有更新任务,所以会bail out,简单理解就是什么也不做。

到了发生setState的组件fiber,发现上面的lane和子树最高优lane一致,所以就会消费update。

低优让步于同步执行的高优update

本章我们探究一个问题:同步发起一个同步任务,一个transition任务,React是如何做到保证同步update先于transition update消费并渲染到DOM上的?

当然两个任务谁先发起是会导致流程区别的,两个任务是否更新同一个状态也是有区别的。所以本章会分四个小节来谈论。

先同步任务后transition任务的调度逻辑

我们举例来说明这个过程

const Demo = () => {
    console.log('rerender');
    const [counter, setCounter] = useState(0);
    const [isPending, startTransition] = useTransition();

    function handleClick() {
        startTransition(() => {
            // 这次更新产生的任务是transition优先级的
            setCounter(counter + 1)
        })
    }

    return (
        <>
            <div>{counter}</div>
            <div onClick={handleClick}>点我发起transition优先级的更新任务</div>
        </>
    )
}

这个过程看似仅有一个transition优先级的任务,实则是先发起同步任务后发起transtion任务。

因为startTransition这个方法实际上会先把isPending状态置以同步优先级更新为true,表示进入transition状态。

而后会产生把isPending置为false和更新counter两个transition优先级的任务。

简单理解,startTransition方法内部:

// 极简的startTransition
function startTransition(callback) {
    // 这个被赋予同步优先级
    setPending(true);
    // 这个被赋予transition优先级
    setPending(false);
    // 开发者传入的更新也被赋予transition优先级
    callback();
}

第一个同步优先级的任务生成后会被压入微任务队列,等待执行。压入队列,等待执行就叫作被schedule完毕。

走到setPending(false)这个transition任务,这里React会判断当前最高优的任务和当前被schedule的任务是否是相同的优先级。是的话就不会schedule这个任务,而是直接return掉。

return掉仅仅代表任务没有执行计划,但是update本身还是存储在组件fiber上,我们看看isPending这个状态在setPending(false)后fiber上是如何存储相关的更新信息的。

首先一个状态一定是对应一个hook的。比如counter这个状态对应组件函数中的第一个useState hook,而hook是key为memoizedState的对象,它以链表的形式存储在组件fiber上:

// 组件fiber
{
    // 第一个hook,也就是useState hook相关状态信息
    memoizedState: {
        memoizedState: 0,
        // counter的queue先不关注,重点关注第二个状态也就是isPending.
        queue: unshown,
        // 第二个hook,也就是useTransition hook对应的状态信息
        next: {
            // 当前状态(更新发生前)为false
            memoizedState: false,
            // 这里存储即将发生的更新队列
            queue: {
                // pending.next存储当前要进行的更新
                pending: {
                    "lane": 64,
                    "action"false,
                    "hasEagerState"false,
                    "eagerState": null,
                    // next上的才是即将要发生的更新
                    "next": {
                        "lane": 1,
                        "action"true,
                        "hasEagerState"true,
                        "eagerState"true,
                        // 这里是首尾循环引用
                        "next": ...
                    }
                }
            }
        }
    }
}

其实决定同步更新的关键的是第二个hook上的queue.pending.next,它代表着本次要进行的更新,这里是同步的所以本次更新会把hook.memoizedState状态置为true并赋值给isPending。

同步的更新完毕后会再次schedule遍历任务,遍历到存在与树上最高优任务同等优先级的fiber时会停止遍历并再次来到更新,这次更新就是消费两个transition update了。

第一个transition update也就是counter置为1的更新,它的关键是baseQueue.next也就是:

// 本次消费的update
{
    action: 1
    eagerState: null
    hasEagerState: false
    lane: 64
}

这里会根据action和reducer计算出newState,当然这里调用时没有用reducer,所以newState就是action,所以hook.memoizedState会被修改为1并返回。

第二个transition update被消费的逻辑是一样的。

调度指的就是把遍历整个fiber树加入到微任务队列。有两个关键点:

  • 同步任务的调度动作是用queueMicrotask这个api加入的,并且遍历树的动作不可中断。而transition任务的调度是用message channel这个api加入的,并且遍历树的过程可以中断。
  • 同步任务schedule而未消费前的状态叫作pending,存在pending时,不会再schedule新的任务。

所以先发起同步任务后发起transition任务时的调度逻辑一句话就是:同步任务会阻塞其他任何任务的调度,直到同步更新被消费完毕后才会调度低优任务。

先transition任务后同步任务的调度逻辑

第一节只是预热,这一节我们真正探索下高优打断的机制,怎么保证高优任务先于低优任务渲染。

这个例子就是高优打断低优的实例:

const Demo = () => {
    console.log('rerender');
    const [counter, setCounter] = useState(0);
    const [counter1, setCounter1] = useState(0);
    const [isPending, startTransition] = useTransition();

    function handleClick() {
        startTransition(() => {
            setCounter(counter + 1)
        })
        // 后发起的高优更新,React是如何实现抢占的呢?
        setCounter1(counter1 + 2);
    }

    return (
        <>
            <div>{counter}</div>
            <div onClick={handleClick}>点我发起低优抢占高优的更新</div>
        </>
    )
}

上一节我们已经讲过了一个transition任务实际上是由一个同步任务加两个transition任务组成的。

同步任务会先被调度并pending掉后续的所有调度,注意是所有,不止前面说过的两个transition任务,甚至后面setCounter1这个同步任务的调度也会被阻塞掉。

也就是说即使先transition任务后同步任务,也是会被transition任务内部的同步任务阻塞掉调度。

但是阻塞掉调度和阻塞不代表阻塞掉更新,同步任务被消费的那轮渲染,React会判断当前hook上update的lane,只要是和本次渲染的赛道内的update,都会消费掉,所以isPending会更新为true,而counter1会更新为2。

注意,这里我理解React每次渲染都对应一个renderLanes表示本次更新的赛道,只要next update的lane是renderLanes的子集,那么这个update就会被消费。

所以其实这里高优抢占低优并没有什么特别的逻辑。实际上,后发起的高优任务counter1最终会先于counter更新,根本不是自己抢占的,而是搭了transition任务内部把isPending置为true这个同步任务的顺风车。

同一个状态,先发起transition任务后发起同步任务

上节讲了先transition任务后同步任务的抢占逻辑,不过是搭transition任务内部的同步任务的顺风车。

那么如果说同步任务和transition任务更新同一个状态,transition更新会覆盖同步更新吗?

const Demo = () => {
    console.log('rerender');
    const [counter, setCounter] = useState(0);
    const [isPending, startTransition] = useTransition();

    function handleClick() {
        startTransition(() => {
            setCounter(counter + 1)
        })
        // 后发起的高优更新,React会让transition更新覆盖先完成的同步更新吗?
        setCounter(counter1 + 2);
    }

    return (
        <>
            <div>{counter}</div>
            <div onClick={handleClick}>点我发起</div>
        </>
    )
}

我们尝试了下,实际上,transition update虽然是后消费的,但并不会覆盖高优任务,点击后counter的结果是2。

那么内部的逻辑是怎样的呢?

第一轮更新是,counter状态对应的hook上,queue.pending.next是lane为64,action为1的更新,queue.pending.next.next才是lane为1,action为2的更新。

首先React会遍历整个pendingQueue,找到在renderLanes赛道中的,同步的更新,也就是把counter置为2。

而在第二轮rerender时,我们发现已经渲染到DOM上的,把counter置为2的update并没有清除,仅仅是lane变为了0,而0是任何lanes的子集,所以本次遍历queue时还是能通过isSubsetOfRenderLanes的判定,于是会采用了这个update的action作为newState,所以第二轮更新counter还是2。

但为什么已经完成更新的update仍会在queue上不清除,仅仅是lane从1变为0呢?

可以在updateReducer方法中看到这段代码:

 // This update does have sufficient priority.
        if (newBaseQueueLast !== null) {
          var _clone = {
            // This update is going to be committed so we never want uncommit
            // it. Using NoLane works because 0 is a subset of all bitmasks, so
            // this will never be skipped by the check above.
            lane: NoLane,
            action: update.action,
            hasEagerState: update.hasEagerState,
            eagerState: update.eagerState,
            next: null
          };
          newBaseQueueLast = newBaseQueueLast.next = _clone;
        } // Process this update.

这个clone就是原来的同步update lane变为0的复制,而newBaseQueueLast则是本轮没有被消费的transition update,这里会把复制体放到其next上。

然而回顾下第一个例子中,isPending置为true之后,hook的queue上就没有hasEagerState的update了,这里没有看到queue上有clone,所以再回头单步调试下第一个例子。

首先没有走到这个clone,因为newBaseQueueLast为null,在第一行就进入不了这个分支。

而newBaseQueueLast这个变量是在遍历queue的时候初始化的,它会在每次遍历到需要跳过的update时更新自身为这个update的copy。

这里就是关键了,第一个例子中,queue的第一项是同步update,第二项才是transition update。这是因为pending置为true这个同步任务是先来的,置为false这个transition任务是后来的。

这就导致了第一个update就不能跳过,所以newBaseQueueLast仍为初始值null。所以就进不到我们本节开始贴的那段逻辑,同步update消费后就不在queue链表上了。

而我们最新的例子中,是transition update先来的,同步update是第二项。所以第一项跳过后,newBaseQueueLast会被置为transition update,所以轮到第二项同步任务时就会生成一个lane为0的copy,继续存在于链表上。

这也就导致了在第二次渲染时,还是采用的同步update的状态。

同一个状态,先发起同步任务后发起transition任务

const Demo = () => {
    console.log('rerender');
    const [counter, setCounter] = useState(0);
    const [isPending, startTransition] = useTransition();

    function handleClick() {
        setCounter(counter1 + 2);
        startTransition(() => {
            setCounter(counter + 1)
        })        
    }

    return (
        <>
            <div>{counter}</div>
            <div onClick={handleClick}>点我发起</div>
        </>
    )
}

这样做的话counter会是几呢?

实际上如果理解了上一节,这个例子想一下就知道了,最终counter会是1。

因为hook.queue上第一项是同步update,第二项是transition update。

第一次renderLanes为1的渲染,由于newBaseQueueLast为null,同步update消费完毕后不会存在于queue上。

第二次renderLanes为64的渲染,会消费掉transition update。

所以counter会先变为2,后变为1。

当然从页面上观察,这个过程很快,你是看不到的,偶尔能看到闪烁,但是单步调试的话,你能看到的确是这样的。

通过以上两节,我们看出,如果不同优先级的更新同步更新同一个状态,最终的状态是是由最后面的setState决定的。

低优任务让步于异步高优任务

在上一章所说的高优先于低优执行,本质上有两点:

  • 高优pending低优,也就是说因为被pending导致低优任务不能schedule
  • 存在高优任务时,当次渲染的renderLanes就是最高优的,所以不能消费低优update 但是这都是给同步高优让步的逻辑,那么在低优任务已经schedule后,执行中让步的时机发起高优任务,低优任务是否会让步呢?如果是,是按照什么逻辑让步的呢?

首先再说一遍低优任务执行中的让步机制。低优任务在执行中是可以打断的,任务的执行就是遍历fiber,而低优任务在遍历fiber时会检查执行时间,只要时间超过了5ms就会让出线程。这就是低优任务的执行中让步。

那么低优任务中断后,如有同步任务会怎么样?低优任务会像同步任务阻塞它那样阻塞回去吗?

构建这种例子我们是不能像上一章那样先写个低优后写个同步的,因为那样两个任务还是同步发起的,后面的同步任务是会搭transition任务内部自己的同步渲染的顺风车实现提前更新的,而我们这里要探究的是等低优任务已经在执行中,再发起高优任务,看看是否能抢占。

为了尽量快速打断,我的设计是低优任务用键入Input框来触发,而同步任务用点击按钮触发,只需要左手键入后立即右手点击按钮,这样就是低优执行后发起的高优任务了。

从实际结果来看,即使执行中的低优任务也会被高优任务抢占,具体表现为input框内容是后于counter的更新的。

然而我们不能仅凭表象下结论,还需要结合源码印证一下。之前之所以低优任务不会被schedule是在ensureRootIsScheduled这个方法中判断当前待完成任务的lane和本次新任务的最高lane,如果相等就会被return掉。上一章的所有例子中无论谁先谁后,当前待flush任务的优先级都是1,而RootFiber上lane是65,最快赛道也是1,所以后面的所有调度请求都会被return掉。

而这次我们当前待完成任务的优先级来看,pending同步赛道已经被消费掉,当前待完成任务的赛道变为了64。而新任务是1,所以当然不会被return而是会被schedule。

然后就通过scheduleMicrotask方法把flush任务加入了微任务队列。加入微任务队列后,等到transition任务执行再次让步时就轮到同步任务的flush了。

而同步任务的flush是同步的,一旦获得线程就会执行完毕并渲染到DOM上。

这就是低优任务被异步高优任务的抢占。

低优任务的中断恢复机制

既然低优任务是可以中断的,那么是如何恢复的呢?

我们知道任务是遍历fiber,中断一定是遍历到某个fiber后检查执行时间发现超时后中断。

而听到恢复这个说法,很自然想到应该是从中断的fiber处恢复。那么实际来看是否是这样呢?我们需要验证一下。

我们还是以上一章的例子的基础上,在setCounter时打点,在低优任务被打断后打点,在低优任务恢复执行后打点。

然而实际上看到的确是每次打断恢复都是从根部从头开始遍历的。而不是脑补的从打断处继续。

饥饿问题

什么是饥饿问题?其实我们已经看到,先发起的低优任务会对高优任务做出很多让步:

  • 同步的高优update会和transition任务内部的pending update一起被消费并更新到DOM。
  • 异步的高优任务会在低优任务执行中让出线程的机会中抢过线程,完成更新。

这还仅仅是让步高优更新,其实不只是更新状态,任何任务都有可能通过响应事件的方式在低优任务让出线程的窗口期抢过线程。

而如果这个任务是耗时的,低优更新可能会延迟了很长时间才最终渲染到页面上并执行相应的effect,如果这个时间超出可以接受的阈值,那么这种情况就被称为饿死。

为了避免饿死,React做了哪些操作呢?

实际上React执行任务有两种模式,一种是renderRootConcurrent方法对应的可中断执行模式,一种是renderRootSync对应的不可中断模式。

React通过让低优任务切换到不可中断的执行模式来避免饿死。那么是如何切换的呢?实际上React有三个开关可以让任务的执行切换到不可中断模式,这里我讲解其中两种。

用expiredLanes避免首次执行即超时

lane的过期时间就是创建update后设定的。

更精确来说,第一次transition任务在schedule时会因为已经schedule了同步的isPending任务而被直接取消掉,但就在取消前,lane为64的所有update的过期时间已经设置好了。

也就是说,React会在第一次企图schedule的时候就给这个要被schedule的lane设置好过期时间。transition优先级的过期时间是currentTime+5000。

而第一次轮到transition任务执行的时候,会在performConcurrentWorkOnRoot中检查当前要flush的lane是不是属于expireLanes,是的话则直接进入不可中断模式。

这段逻辑很好证实,只需要在发起任务同时来一段耗时任务就可以了。就像这样:

const Demo = () => {
    console.log('rerender');
    const [counter, setCounter] = useState(0);
    const [isPending, startTransition] = useTransition();

    function handleClick() {
        startTransition(() => {
            setCounter(counter + 1)
        });
        let result = 0,
            len = 10000000000;
        while (len--) {
            result += len;
        }
    }

    return (
        <>
            <div>{counter}</div>
            <div onClick={handleClick}>构造一个过期的transition lane</div>
        </>
    )
}

点击后的流程:

  • 同步的执行时机内就设置好了transition lane的过期时间,也就是当前时间+5000。
  • while循环这块的耗时任务。
  • 第一轮渲染,同步的update也就是isPending置为true,被消费。
  • 第二轮渲染,由于第二步的耗时任务,所以transition lane已经过期,root fiber上的expireLanes会|=这个64赛道。
  • performConcurrentWorkOnRoot中,检测到当前要flush的64赛道是expireLanes的子集,所以进入不可中断模式,同步完成更新。

流程梳理完毕,大家思考下这种设计可以避免哪些情况的饿死呢?

实际上就是避免第一次轮到执行就已经超时的情况,这种情况通常也就发生于和另一个同步任务同步发起,但是那个同步任务耗时超过5s。

用expirationTime避免执行中让步导致的超时

这个情况就更熟悉了,具体场景就如<低优任务让步与异步高优任务>这一章节中所讲。

如果让步给的高优任务是耗时的,那么就会导致饿死。

所以React会给task一个expirationTime属性表明task的过期时间。

这里的task是scheduler这个包中的概念。一个task是一个对象:

// scheduler.development.js
var newTask = {
    id: taskIdCounter++,
    callback: callback,
    priorityLevel: priorityLevel,
    startTime: startTime,
    expirationTime: expirationTime,
    sortIndex: -1
};

对象上最重要是callback,在这里callback就是performConcurrentWorkOnRoot方法。这个方法之前说了就是切换两种遍历模式的。

在首次轮到transition任务开始执行后,task就会设置好过期时间。并且让步后重新执行scheduler这个包中workLoop方法时,都会检查任务是否已经过期了。

如果已经过期,调用performConcurrentWorkOnRoot时传入的参数就会改变,performConcurrentWorkOnRoot方法也就知道任务过期,就会切换到不可中断模式。

好了,到这里我提个问题,你明白为什么有两处需要防止超时的设计吗?它们不会重复吗?

答案就在两小节的标题中,expiredLanes解决的是执行前已经超时的情况,而expirationTime解决的执行中让步导致超时情况。两者是缺一不可的。

尾言

关于贴源码和调用栈

本文的讲解逻辑就像开篇说的那样,是以例子以及重要概念为核心的,并没有贴出完整的源码流程。从我个人来看,那种逐个讲解源码方法的文章非常难懂,不利于理解问题的本质。

然而如果读者大致理解之后,想自己打debugger,完整走完某个例子的流程来进行验证文章内容的话,逐个贴源码方法这种形式则又有用了,所以我在另一篇文章会贴出标准流程的完整调用栈和简单讲解。(先挖个坑)

原文: https://zhuanlan.zhihu.com/p/454432618

内容总结

本文讲了:

  • React的任务、更新和调度的概念。
  • 两种最常见的优先级更新整个流程的内部逻辑
  • 高优优先、高优抢占、什么情况会出现低优饿死以及如何避免低优饿死

没讲:

  • React最基本的东西如:fiber树、hook实现等,这些东西没法在讲任务的时候讲,因为要讲的话内容比较多,只能默认读者有了解,后面有机会会梳理成文章。
  • scheduler这个包实现的让步机制(其实就是中断while循环后把剩余压入微任务队列)

感谢阅读这篇万字长文,写完才发现文章竟然写的这么长,但是这些内容确实是一个个实实在在的具体问题,在完成文章前我自己也想知道,并且单步debug得出的。希望对大家也有帮助。文章如有错漏之处,或者有要补充的内容欢迎在评论区留言。


最后, 送人玫瑰,手留余香,觉得有收获的朋友可以点赞,关注一波 ,我们组建了高级前端交流群,如果您热爱技术,想一起讨论技术,交流进步,不管是面试题,工作中的问题,难点热点都可以在交流群交流,为了拿到大Offer,邀请您进群,入群就送前端精选100本电子书以及 阿里面试前端精选资料 添加 下方小助手二维码或者扫描二维码 就可以进群。让我们一起学习进步.



[极客前沿]-你不知道的 React 18 新特性

[极客前沿]-写给前端的 K8s 上手指南

[极客前沿]-写给前端的Docker上手指南

[面试必问]-你不知道的 React Hooks 那些糟心事

[面试必问]-一文彻底搞懂 React 调度机制原理

[面试必问]-一文彻底搞懂 React 合成事件原理

[面试必问]-全网最简单的React Hooks源码解析

[面试必问]-一文掌握 Webpack 编译流程

[面试必问]-一文深度剖析 Axios 源码

[面试必问]-一文掌握JavaScript函数式编程重点

[面试必问]-阿里,网易,滴滴,头条等20家面试真题

[面试必问]-全网最全 React16.0-16.8 特性总结

[架构分享]- 微前端qiankun+docker+nginx自动化部署

[架构分享]-石墨文档 Websocket 百万长连接技术实践

[自我提升]-Javascript条件逻辑设计重构

[自我提升]-送给React开发者十九条性能优化建议

[自我提升]-页面可视化工具的前世今生

[大前端之路]-连前端都看得懂的《Nginx 入门指南》

[软实力提升]-金三银四,如何写一份面试官心中的简历




觉得本文对你有帮助?请分享给更多人

关注「React中文社区」加星标,每天进步

   


点个赞👍🏻,顺便点个 在看 支持下我吧

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存