新一年又开始了,React过去一年悄悄实现了Suspense,带来了React Hooks ,源码也大变换了。因此好有必须重新阅读一下,看它是如何实现这些功能,及未来准备开放的并发渲染功能。
React16早期将原来的diff过程拆分成两个阶段,第一个阶段叫 reconcile , 也就是原来diff虚拟DOM的过程。这阶段有大量叫reconcileXXX的方法参与其中,如reconcileSingleElement、reconcileSinglePortal、reconcileChildFibers、reconcileSingleTextNode、 reconcileChildrenArray、reconcileChildrenIterator、 reconcileChildFibers...会创建组件实例与真实DOM节点,执行一些轻量钩子。第二个阶段叫commit,在并发模式下,可能多次reconcile才有一次commit。commit就是将水面下的效果浮现出来,比如将节点插入到DOM树,修复节点的属性样式文本内容,执行如componentDidXXX这样的重量钩子,执行ref操作(这时可能涉及DOM操作)。
schedule源码剖析
现在,React在这两个阶段之前新添加了一个阶段,schedule。因为一个页面可能有多个ReactDOM.render,虚拟DOM树中也通常存在多个拥有自更新能力的组件(React Hooks的出现让无状态组件也具有自更新能力)。在并发模式下,组件setState不会立即更新视图,于是在一个时间段中,就有多个待更新的组件,这些组件叫root(渲染的起点),但谁是真正的nextRoot呢?需要一个调度算法进行决定。React根据当前时间给每个组件分配一个过期时间(相当优先级),数字越大,就越优先执行。
schedule的起点方法是scheduleWork。 ReactDOM.render, setState,forceUpdate, React Hooks的dispatchAction都要经过scheduleWork。
scheduleWork里面有一个scheduleWorkToRoot方法,负责将当前fiber及其alternate的过期时间推迟(或者叫加大)。由于在ReactFiber中,过期时间等价于优先级,换言之,一个组件在某个时间段setState频繁,那么它就越优先更新。
在并发模式下,setState后33ms执行(如果在动画中,为了保证流畅,增长到100ms间隔 )。如果更新的节点是一个受控组件(input),那么它是直接进入interactiveUpdates方法,不经过scheduleWork,是立即更新!React还有一个没登记到文档batchedUpdates方法,它可以让一大遍节点立即更新,并且无视shouldComponentUpdate return false!!类似batchedUpdates这样的特权方法,在React16中已经存在了许多了!
上面是ReactFiber的总流程。最下面的绿色方法都是贵族方法,拥有极高的优先执行权。
//by 司徒正美 QQ 370262116
function scheduleWork(fiber, expirationTime) {
const root = scheduleWorkToRoot(fiber, expirationTime);
if (root === null) {
return;
}
if (
!isWorking &&
nextRenderExpirationTime !== NoWork &&
expirationTime > nextRenderExpirationTime
) {
resetStack();
}
markPendingPriorityLevel(root, expirationTime);
if (
//如果在准备阶段或commitRoot阶段或渲染另一个节点
!isWorking ||
isCommitting ||
// ...unless this is a different root than the one we're rendering.
nextRoot !== root
) {
const rootExpirationTime = root.expirationTime;
requestWork(root, rootExpirationTime);
}
}
requestWork是决定以什么方式进入performWorkOnRoot。有四种情况,一如果已经开始渲染,那么就立即返回,二是直接进入performWorkOnRoot, 三是先进入performSyncWork,然后到performSync到performWork到performWorkOnRoot,四是异步方法,从scheduleCallbackWithExpirationTime进入performAsyncWork到performWork到performWorkOnRoot。
performWork就是用来裁定是同步渲染还是异步渲染。performWork的第一行就是findHighestPriorityRoot方法,将最高优先级的root挑出来,丢给performWorkOnRoot。
performWorkOnRoot是决定直接执行commitRoot还是 先执行renderRoot再执行commitRoot。
completeRoot只是一个renderRoot的一个简单包装,它执行了一些至少我们都没有用到ReactBatch的东西。
整个过程如下:
scheduleWork --> requestWork --> performWork --> findHighestPriorityRoot -->
performWorkOnRoot --> completeRoot --> renderRoot --> commitRoot
从scheduleWork到completeRoot,就是schedule阶段,决定哪个子树优先执行,何时执行。
renderRoot,就是reconcile阶段,让组件生成子级的虚拟DOM,生成组件实例与真实DOM,各种打tag( effectTag)。
commitRoot,就是commit阶段,更新视图,执行重型钩子与Ref。
commit阶级的源码分析
至于React在commit阶段是怎么更新,也是一个令人眼花的过程。上面讲过,在reconcile阶段,会对fiber进行打tag。fiber对象是新时代的虚拟DOM,它是用来承载着组件实例与真实DOM等重要数据。这些重要数据在更新过程是不需要重新生成的。但React希望能像git那样按分支开发,遇错回滚。于是fiber拥有一个辅助属性alternate,你可以叫他备胎,也可以叫替万鬼,也可以叫踩雷的。我们打开performUnitOfWork方法就知道了
function performUnitOfWork(workInProgress) {
var current = workInProgress.alternate;
next = beginWork(current, workInProgress, nextRenderExpirationTime);
//...
}
workInProgress为一个fiber,最开始时它是没有alternate,beginWork就是根据它是否有alternate决定执行mount还是update操作。而伟大的一开始就有alternate,它总是更新的,它的
//by 司徒正美 QQ 370262116
function createFiberRoot(containerInfo, isConcurrent, hydrate) {
var uninitializedFiber = createHostRootFiber(isConcurrent);
var root = {
current: uninitializedFiber,
containerInfo: containerInfo,
pendingChildren: null,
//...略
}
uninitializedFiber.stateNode = root;
return root;
}
从scheduleWork到completeRoot的root指的就是HostRootFiber的current属性,它本身为一个fiber。
在performWorkOnRoot中出现一个叫finishedWork的东西,它以后在许多方法中都露脸,通过调试可知,在renderRoot前,root是没有finishedWork属性,在renderRoot后就有这属性了。发现这个方法是在renderRoot中的onComplete方法中加上的。
var rootWorkInProgress = root.current.alternate;
// Ready to commit.
onComplete(root, rootWorkInProgress, expirationTime);
function onComplete(root, finishedWork, expirationTime) {
root.pendingCommitExpirationTime = expirationTime;
root.finishedWork = finishedWork;
}
那么root.current.alternate是怎么来的呢?发现只有createWorkInProgress能为fiber创建副本。renderRoot的前半部分有这几行
if (expirationTime !== nextRenderExpirationTime || root !== nextRoot || nextUnitOfWork === null) {
// Reset the stack and start working from the root.
resetStack();
nextRoot = root;
nextRenderExpirationTime = expirationTime;
nextUnitOfWork = createWorkInProgress(nextRoot.current, null, nextRenderExpirationTime);
root.pendingCommitExpirationTime = NoWork;
}
有了finishedWork,就可以在commitRoot中得到最重要的nextEffect。
firstEffect = finishedWork.firstEffect;
nextEffect = firstEffect
nextEffect是一个全局变量。ReactFiberScheduler.js这个文件2500行,定义大量全局变量与N多上百行的巨型函数,真是想骂人。
commit阶段就是四大commitXXX方法,commitBeforeMutationLifecycles, commitAllHostEffects, commitAllLifeCycles都会用到nextEffect。commitPassiveEffects则比较人道,直接将firstEffect bind了一下。commitPassiveEffects就是React Hooks中的useEffect,最晚才执行的钩子。
这时我们要研究finishedWork.firstEffect是怎么来的。finishedWork就是HostRootFiber的current对象经过createWorkInProgress产生的。在completeUnitOfWork中有这样一行:
var effectTag = workInProgress.effectTag;
// Skip both NoWork and PerformedWork tags when creating the effect list.
// PerformedWork effect is read by React DevTools but shouldn't be committed.
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = workInProgress;
} else {
returnFiber.firstEffect = workInProgress;
}
returnFiber.lastEffect = workInProgress;
}
每个父fiber会将它发生更新的孩子当为lastEffect或lastEffect.nextEffect属性存起来,本来它的孩子可能是数组结构,也可能数组包含数组,现在全部变成链表了。
completeUnitOfWork里面还有一个completeWork方法,它会将新生成的孩子立即添加到父节元素上。
var instance = createInstance(type, newProps, rootContainerInstance,
currentHostContext, workInProgress);
appendAllChildren(instance, workInProgress, false, false);
//-----------
//by 司徒正美 QQ 370262116
appendAllChildren = function (parent, workInProgress, needsVisibilityToggle, isHidden) {
// We only have the top Fiber that was created but we need recurse down its
// children to find all the terminal nodes.
var node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
} else if (node.tag === HostPortal) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
};
function appendInitialChild(parentInstance, child) {
parentInstance.appendChild(child);
}
因此在commit阶段,对于元素节点,它只有移动,删除,修改样式与文本这几种任务(副作用)
其他几个commitXXX就是很简单了。
commitBeforeMutationLifecycles就是执行getSnapshotBeforeUpdate
commitAllHostEffects执行DOM节点相关的操作
commitAllLifeCycles执行组件实例相关操作
Suspense与懒加载的实现
Suspense是一个虚拟组件,如果它正下方不是一个LazyComponent(通过React.lazy产生),那么它与Fragment, Profiler, StrictMode这几个组件没什么差别,不会渲染自身。
//官网上都是用动态import语句实现,其实使用Promise+setTimeout也能模抋
var LazyComponent = React.lazy(function(){
return new Promise(function(resolve){
setTimeout(function(){
resolve()
}, 1500)//setTimeout模拟网络请求,方便能看到Loading
}).then(function(){ //then方法必须返回一个带default属性的对象
return {
default: function(){
return 这是动态组件
}
}
})
});
function App (){
return
Loading... }>
我们看fiber如何处理它的,从renderRoot到workLoop到performUnitOfWork。performUnitOfWork会逐个处理所有fiber。performUnitOfWork又分为beginWork与completeWork两个阶段。beginWork遇到SuspenseComponent时,就丢到updateSuspenseComponent方法,一开始进入if ((workInProgress.effectTag & DidCapture) === NoEffect) {} 分支,这时nextDidTimeout为false,于是直接解析其子节点LazyComponent。
child = next = mountChildFibers(workInProgress, null,
nextPrimaryChildren, renderExpirationTime);
mountChildFibers里面遇到LazyComponent,会调用mountLazyComponent处理,mountLazyComponent又调用readLazyComponentType处理。readLazyComponentType会读取组件的_status属性,决定是返回result组件,还是抛错。基本上除了Resolved情况下,都抛错了。
function readLazyComponentType(lazyComponent) {
var status = lazyComponent._status;
var result = lazyComponent._result;
switch (status) {
case Resolved:
var Component = result;
return Component;
case Rejected:
var error = result;
throw error;
case Pending:
var thenable = result;
throw thenable;
default:
lazyComponent._status = Pending;
var ctor = lazyComponent._ctor;
var _thenable = ctor();
_thenable.then(function (moduleObject) {
if (lazyComponent._status === Pending) {
var defaultExport = moduleObject.default; {
if (defaultExport === undefined) {
warning$1(false, 'lazy: Expected the result of a dynamic import() call. ' + 'Instead received: %s\n\nYour code should look like: \n ' + "const MyComponent = lazy(() => import('./MyComponent'))", moduleObject);
}
}
lazyComponent._status = Resolved;
lazyComponent._result = defaultExport;
}
}, function (error) {
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
lazyComponent._result = error;
}
});
lazyComponent._result = _thenable;
throw _thenable;
}
}
抛错了怎么办?不用担心,workLoop外面是包着try catch。
do {
try {
workLoop(isYieldy);
} catch (thrownValue) {
if (nextUnitOfWork === null) {
// This is a fatal error.
didFatal = true;
onUncaughtError(thrownValue);
} else {
var sourceFiber = nextUnitOfWork;
var returnFiber = sourceFiber.return;
if (returnFiber === null) {
didFatal = true;
onUncaughtError(thrownValue);
} else {
//lazyComponent组件的抛错会在这里被接住
throwException(root, returnFiber, sourceFiber, thrownValue, nextRenderExpirationTime);
//继续处理子节点或兄弟节点
nextUnitOfWork = completeUnitOfWork(sourceFiber);
//nextUnitOfWork为lazyComponent的父节点SuspenseComponent
continue;
}
}
}
break;
} while (true);
我们看到一个有处理的处理,continue语句之前nextUnitOfWork为lazyComponent的父节点SuspenseComponent,于是SuspenseComponent又跑到workLoop中,又到performUnitOfWork再到beginWork再到updateSuspenseComponent。这时程序会进入updateSuspenseComponent的另一分支,nextDidTimeout为true,这个分支中会取得SuspenseComponent的fallback函数,解析得到里面内容,于是这时Loading就出来了!!!!
在throwException中这样的语句
//by 司徒正美 https://rubylouvre.github.io/nanachi/
if (_workInProgress.tag === SuspenseComponent &&
shouldCaptureSuspense(_workInProgress.alternate, _workInProgress)) {
// Found the nearest boundary.
// If the boundary is not in concurrent mode, we should not suspend, and
// likewise, when the promise resolves, we should ping synchronously.
var pingTime = (_workInProgress.mode & ConcurrentMode) === NoEffect ? Sync : renderExpirationTime;
// Attach a listener to the promise to "ping" the root and retry.
var onResolveOrReject = retrySuspendedRoot.bind(null, root, _workInProgress, sourceFiber, pingTime);
if (enableSchedulerTracing) {
onResolveOrReject = unstable_wrap(onResolveOrReject);
}
thenable.then(onResolveOrReject, onResolveOrReject);
}
而retrySuspendedRoot会重新调起scheduleWorkToRoot,开始新一轮的渲染流程,这次Promise返回 的内容会取代fallback生成的节点,实现lazyload效果!