Suspend 挂起详解
1 )概述
- 在react的更新过程当中,它的任务是可以被挂起的,也就是 Suspend
- 关于 Suspend
- 字面意思就是挂起
- 在某次更新的任务更新完成之后,暂时不提交
- 在 react更新中,分为两个阶段,首先是render阶段
- 主要就是包含
performUnitOfWork
以及completeUnitOfWork
- 对拿到的 reactElement 进行一个向下一层一层渲染
- 这个过程呢叫做
beginWork
, 在这个过程中 - 从一个root节点开始渲染渲染到某一层的最终的一个子节点之后
- 然后再由这个子节点往上返回,并且渲染它的兄弟节点
- 最终回到root这个过程,然后把一个 reactElement 形成的树最终渲染成一棵完整的fiber树
- 在这个过程当中,会根据传入的props以及每个节点的不同的类型来给它创建不同的实例
- 以及进行一些 diff 的内容, 这个过程为什么要叫 render 阶段呢?
- 因为它完全不会影响我们目前的整个 dom 树,它不会更新任何 dom 节点的特性
- 在更新的过程当中,会形成一个effect的链
- 这些effect的链就代表着在真正commit的阶段是要去把哪些dom节点进行一个更新
- 主要就是包含
- 在 render 阶段完成了之后,就进入commit阶段
- 把render阶段能够发现的所有需要更新的节点提交到dom的更新上面,来完成一个UI的更新
- 在 suspend 当中,完成了 render 阶段的所有任务, 但是暂时把这个任务停在 render 阶段不提交
- 也就是把最终要修改 dom 的任务给它停掉,就不去做这个事情
- 那么这个更新过程就叫做被 Suspend 的,也就是被挂起了
- 在 react更新中,分为两个阶段,首先是render阶段
- 这个更新可能在下次更新中再次被执行
2 )更新过程回顾
一开始
root
|
| current
↓
RootFiber -----alternate-----> workInProgress
| |
| |
↓ ↓
childFiber childWIP (childFiber 的 workInProgress)
- 在执行更新的过程当中,一开始有一个root节点,这个root节点它是一个fiber对象
- 它有一个属性叫 current 指向了 RootFiber
- 这个 RootFiber 在更新完一次之后,它会有一个childFiber
- 在要执行一个更新的过程当中,会把 rootFiber 去创建一个叫做 workInProgress 这么一个对象
- 通过这个fiber对象的 alternate 属性去指向这个 workInProgress
- 这个 workInProgress 跟 RootFiber 是两个完全不同的对象,只不过它们对象里面的有一些引用的属性是一样的
- 比如说 memorizedState,还有 memorizedProps 这些常用的属性,然后在整个更新的过程当中
- 都会通过 workInProgress 去创建它的 childFiber 的 workInProgress
- 也就是说目前的 root 的 current 指向的这个 RouterFiber 以及它的childFiber
- 跟在更新过程当中使用的 workInProgress 和 它的 childFiber 的 workInProgress 都是新的对象,不会相互影响
更新完成之后
root
\
\
\
\
\
\
\
\
\
\
\
\ current
\
\
\
\
\
\
\
\
\
\
\
\
↘
RootFiber -----alternate-----> workInProgress
| |
| |
↓ ↓
childFiber childWIP (childFiber 的 workInProgress)
- 在整个更新完成之后,在执行了 commitRoot 之后,会做一个操作,叫做把 root.current 的指向变成 workInProgress
- 这个时候因为已经把 workInProgress 上收集到的所有更新提交到 dom 上面了
- 提交到 dom 上面之后,这个 workInProgress 跟 dom 的对应关系才是一一对应的
- 而之前的rootFiber已经是一个过时的版本了, 如果当前的 root.current 还指向它的话,跟我们实际的dom的对应关系就不对了
- 所以这时候,root 就直接修改它的 current 指向 workInProgress,就可以变成一个最新的状态
- 这个 rootFiber 还会依然存在,存在于 workInProgress.alternate 上面
- 后续要去继续更新的时候,又会从 workInProgress 上创建一个新的 workInProgress
- 因为旧的 workInProgress 现在已经变成 root.current了,这个操作在哪里做呢?
- 在commitRoot里面,完成了第二次commit,也就是去 commitAllLifeCycles 修改了所有dom的更新之后
- 它会执行一句代码,叫做
root.current = finishedWork
(packages/react-reconciler/src/ReactFiberScheduler.js#L730) - 这个 finishedWork 就是传入 commitRoot 的时候
- 在render阶段更新完成的那个 workInProgress 对象
- 需要注意它们两个对象 workInProgress 和 current,虽然是大部分属性都是一样的
- 它们最大的区别就是两个完全独立的对象
- 修改了 root.current 的指向之后,就代表 workInProgress 它的更新已经被提交了
- 然后变成了一个新的状态,就存在 root.current 上面
- 这就是在 react当中,有一个叫做 double buffer 的一个概念
- 在更新完成之后,会修改root的指向来复用我们的workInProgress对象
3 )详解 Suspend
- 这个过程完成之后,看下 Suspend
- 在之前的
renderRoot
, 在workLoop
执行完成之后,会执行一堆判断
定位到 packages/react-reconciler/src/ReactFiberScheduler.js#L1381
// Yield back to main thread.
if (didFatal) {
const didCompleteRoot = false;
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
interruptedBy = null;
// There was a fatal error.
if (__DEV__) {
resetStackAfterFatalErrorInDev();
}
// `nextRoot` points to the in-progress root. A non-null value indicates
// that we're in the middle of an async render. Set it to null to indicate
// there's no more work to be done in the current batch.
nextRoot = null;
onFatal(root);
return;
}
if (nextUnitOfWork !== null) {
// There's still remaining async work in this tree, but we ran out of time
// in the current frame. Yield back to the renderer. Unless we're
// interrupted by a higher priority update, we'll continue later from where
// we left off.
const didCompleteRoot = false;
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
interruptedBy = null;
onYield(root);
return;
}
// We completed the whole tree.
const didCompleteRoot = true;
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
const rootWorkInProgress = root.current.alternate;
invariant(
rootWorkInProgress !== null,
'Finished root should have a work-in-progress. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// `nextRoot` points to the in-progress root. A non-null value indicates
// that we're in the middle of an async render. Set it to null to indicate
// there's no more work to be done in the current batch.
nextRoot = null;
interruptedBy = null;
if