赞
踩
2021SC@SDUSC
在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象。根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用setState更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI。整个过程是一气呵成,不能被打断的。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。为此React 16之后就有了scheduler进行时间片的调度,给每个task一定的时间,如果在这个时间内没执行完,也要交出执行权给浏览器进行绘制和重排,所以异步可中断的更新需要一定的数据结构在内存中来保存dom的信息,这个数据结构就是Fiber(虚拟dom)。
Fiber自带属性如下(已添加注解):
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { //保存节点的信息 this.tag = tag;//对应组件的类型 this.key = key;//key属性 this.elementType = null;//元素类型 this.type = null;//func或者class this.stateNode = null;//真实dom节点 //连接成fiber树 this.return = null;//指向父节点 this.child = null;//指向child this.sibling = null;//指向兄弟节点 this.index = 0; this.ref = null; //用来计算state this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null; this.mode = mode; //effect相关 this.effectTag = NoEffect; this.nextEffect = null; this.firstEffect = null; this.lastEffect = null; //优先级相关的属性 this.lanes = NoLanes; this.childLanes = NoLanes; //current和workInProgress的指针 this.alternate = null; }
Fiber可以保存真实的dom,真实dom对应在内存中的Fiber节点会形成Fiber树,在第一次渲染之后,React 最终得到一个 Fiber 树,它反映了用于渲染 UI 的应用程序的状态。这棵树通常被称为 current Fiber(当前树) 。当 React 开始处理更新时,它会构建一个所谓的 workInProgress Fiber(工作过程树) ,它反映了要刷新到屏幕的未来状态。这两棵树会通过alternate相互连接,因为每个 Fiber上都有个alternate属性,也指向一个 Fiber,创建 WorkInProgress 节点时优先取alternate,没有的话就创建一个。
举个简单的例子吧(例子来源于网络,如果作者读到请联系我,我会加上出处):
function App() {
return (
<div>
xiao
<p>chen</p>
</div>
)
}
ReactDOM.render(<App />, document.getElementById("root"));
生成rootFiber
1.在mount时:会创建fiberRoot和rootFiber,然后根据jsx对象创建Fiber节点,节点连接成current Fiber树。
2.在update时:会根据新的状态形成的jsx(ClassComponent的render或者FuncComponent的返回值)和current Fiber对比形成一颗叫workInProgress的Fiber树(diff算法),然后将fiberRoot的current指向workInProgress树,此时workInProgress就变成了current Fiber。
注:
1.fiberRoot:指整个应用的根节点,只存在一个
2.rootFiber:ReactDOM.render或者ReactDOM.unstable_createRoot创建出来的应用的节点,可以存在多个。
我们现在知道了存在current Fiber和workInProgress Fiber两颗Fiber树,Fiber双缓存指的就是,在经过reconcile(diff)形成了新的workInProgress Fiber然后将workInProgress Fiber切换成current Fiber应用到真实dom中,存在双Fiber的好处是在内存中形成视图的描述,在最后应用到dom中,减少了对dom的操作。通俗来说:你可以将 workInProgress 树想象成从旧树中 Fork 出来的功能分支,你在这新分支中添加或移除特性,即使是操作失误也不会影响旧的分支。当你这个分支经过了测试和完善,就可以合并到旧分支,将其替换掉。
对于JSX:JSX是JavaScript XML,是React提供的语法糖。 通过它我们就可以很方便的在js代码中书写html片段。
作用:React中用JSX来创建虚拟DOM(元素)。注意:JSX不是字符串,也不是HTML/XML标签,本质上还是javascript;
因此它不包含schedule,reconcile,render所需的相关信息,而组件在更新中的优先级,state,被打上的用于render的标记,这些内容都包含在fiber节点中。所以,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。
render阶段开始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。
performSyncWorkOnRoot:
// This is the entry point for synchronous tasks that don't go // through Scheduler function performSyncWorkOnRoot(root) { invariant( (executionContext & (RenderContext | CommitContext)) === NoContext, 'Should not already be working.', ); flushPassiveEffects(); let lanes; let exitStatus; if ( root === workInProgressRoot && includesSomeLane(root.expiredLanes, workInProgressRootRenderLanes) ) { // There's a partial tree, and at least one of its lanes has expired. Finish // rendering it before rendering the rest of the expired work. lanes = workInProgressRootRenderLanes; exitStatus = renderRootSync(root, lanes); if ( includesSomeLane( workInProgressRootIncludedLanes, workInProgressRootUpdatedLanes, ) ) { // The render included lanes that were updated during the render phase. // For example, when unhiding a hidden tree, we include all the lanes // that were previously skipped when the tree was hidden. That set of // lanes is a superset of the lanes we started rendering with. // // Note that this only happens when part of the tree is rendered // concurrently. If the whole tree is rendered synchronously, then there // are no interleaved events. lanes = getNextLanes(root, lanes); exitStatus = renderRootSync(root, lanes); } } else { lanes = getNextLanes(root, NoLanes); exitStatus = renderRootSync(root, lanes); } if (root.tag !== LegacyRoot && exitStatus === RootErrored) { executionContext |= RetryAfterError; // If an error occurred during hydration, // discard server response and fall back to client side render. if (root.hydrate) { root.hydrate = false; clearContainer(root.containerInfo); } // If something threw an error, try rendering one more time. We'll render // synchronously to block concurrent data mutations, and we'll includes // all pending updates are included. If it still fails after the second // attempt, we'll give up and commit the resulting tree. lanes = getLanesToRetrySynchronouslyOnError(root); if (lanes !== NoLanes) { exitStatus = renderRootSync(root, lanes); } } if (exitStatus === RootFatalErrored) { const fatalError = workInProgressRootFatalError; prepareFreshStack(root, NoLanes); markRootSuspended(root, lanes); ensureRootIsScheduled(root, now()); throw fatalError; } // We now have a consistent tree. Because this is a sync render, we // will commit it even if something suspended. const finishedWork: Fiber = (root.current.alternate: any); root.finishedWork = finishedWork; root.finishedLanes = lanes; commitRoot(root); // Before exiting, make sure there's a callback scheduled for the next // pending level. ensureRootIsScheduled(root, now()); return null; }
performConcurrentWorkOnRoot
// This is the entry point for every concurrent task, i.e. anything that // goes through Scheduler. function performConcurrentWorkOnRoot(root, didTimeout) { // Since we know we're in a React event, we can clear the current // event time. The next update will compute a new event time. currentEventTime = NoTimestamp; currentEventWipLanes = NoLanes; currentEventPendingLanes = NoLanes; invariant( (executionContext & (RenderContext | CommitContext)) === NoContext, 'Should not already be working.', ); // Flush any pending passive effects before deciding which lanes to work on, // in case they schedule additional work. flushPassiveEffects(); // Determine the next expiration time to work on, using the fields stored // on the root. let lanes = getNextLanes( root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, ); if (lanes === NoLanes) { return null; } // TODO: We only check `didTimeout` defensively, to account for a Scheduler // bug where `shouldYield` sometimes returns `true` even if `didTimeout` is // true, which leads to an infinite loop. Once the bug in Scheduler is // fixed, we can remove this, since we track expiration ourselves. if (didTimeout) { // Something expired. Flush synchronously until there's no expired // work left. markRootExpired(root, lanes); // This will schedule a synchronous callback. ensureRootIsScheduled(root, now()); return null; } const originalCallbackNode = root.callbackNode; let exitStatus = renderRootConcurrent(root, lanes); if ( includesSomeLane( workInProgressRootIncludedLanes, workInProgressRootUpdatedLanes, ) ) { // The render included lanes that were updated during the render phase. // For example, when unhiding a hidden tree, we include all the lanes // that were previously skipped when the tree was hidden. That set of // lanes is a superset of the lanes we started rendering with. // // So we'll throw out the current work and restart. prepareFreshStack(root, NoLanes); } else if (exitStatus !== RootIncomplete) { if (exitStatus === RootErrored) { executionContext |= RetryAfterError; // If an error occurred during hydration, // discard server response and fall back to client side render. if (root.hydrate) { root.hydrate = false; clearContainer(root.containerInfo); } // If something threw an error, try rendering one more time. We'll render // synchronously to block concurrent data mutations, and we'll includes // all pending updates are included. If it still fails after the second // attempt, we'll give up and commit the resulting tree. lanes = getLanesToRetrySynchronouslyOnError(root); if (lanes !== NoLanes) { exitStatus = renderRootSync(root, lanes); } } if (exitStatus === RootFatalErrored) { const fatalError = workInProgressRootFatalError; prepareFreshStack(root, NoLanes); markRootSuspended(root, lanes); ensureRootIsScheduled(root, now()); throw fatalError; } // We now have a consistent tree. The next step is either to commit it, // or, if something suspended, wait to commit it after a timeout. const finishedWork: Fiber = (root.current.alternate: any); root.finishedWork = finishedWork; root.finishedLanes = lanes; finishConcurrentRender(root, finishedWork, exitStatus, lanes); } ensureRootIsScheduled(root, now()); if (root.callbackNode === originalCallbackNode) { // The task node scheduled for this root is the same one that's // currently executed. Need to return a continuation. return performConcurrentWorkOnRoot.bind(null, root); } return null; }
我们现在暂时还不需要理解这两个方法,只需要知道在这两个方法中会调用如下两个方法:
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
可以看到,他们唯一的区别是是否调用shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历。
这里的参数workInProgress代表当前已创建的workInProgress fiber。
performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树
我们知道Fiber Reconciler是从Stack Reconciler重构而来(React 15及以前的 Reconciler 在React 16之后被命名为Stack Reconciler。Stack Reconciler 运作的过程是不能被打断的,必须一条道走到黑, Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行),通过遍历的方式实现可中断的递归,所以performUnitOfWork的工作可以分为两部分:“递”和“归”。
(一)“递”阶段
首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法。
该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。
当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。
(二)“归”阶段
在“归”阶段会调用completeWork处理Fiber节点。
当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。
如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。
“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。
关于beginwork 和 completeWork的讲解我们放到下一篇去讲解
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。