异步渲染原理
大家好,我是万维读客的讲师曹欢欢。本节开始我们开始实现fiber的异步渲染能力,首先我们先学习理解react同步渲染和fiber异步渲染的机制和原理,分析react源代码是如何实现的。
同步渲染和异步渲染
可以先看下下面两个示例,这个是最早ReactConf 2017介绍fiber时给的示例:
这两个动画展示了同步渲染和异步fiber渲染效果对比,明显异步渲染的比较流畅。
那么具体是什么原因导致的呢?
通过前面我们实现react虚拟DOM树构建,我们知道同步渲染有以下特征:
- 深度优先递归遍历children,实现虚拟DOM
- 过程无法中断,中断就无法完成完整的虚拟DOM树构建
那么异步渲染就是为了解决这2个问题,就是基于Fiber架构实现可中断的虚拟DOM更新。
异步更新原理
我们知道react渲染过程有render阶段和commit阶段,同步渲染是这样的:
|浏览器事件a|-------render阶段---------|---commit阶段---|浏览器事件b|
PS:
- render阶段主要是diff虚拟DOM,不可中断;
- commit阶段主要是更新虚拟DOM到真实DOM节点;
而fiber架构的异步渲染是这样:
|浏览器事件a|render阶段1|浏览器事件b|render阶段2|render阶段3|---commit阶段---|浏览器事件c|
PS:
- render阶段1、render阶段2、render阶段3 每一步都只做部分的更新操作,操作完就让出主进程可以响应浏览器事件;
整体看render阶段是可以中断的;但是单个的render子阶段是不能中断的。
- commit阶段是同步的,不可中断;
我们来结合React源码看一下,可以参考下performConcurrentWorkOnRoot:https://github.com/facebook/react/blob/4f29ba1cc52061e439cede3813e100557b23a15c/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L824
源码分析
摘取部分performConcurrentWorkOnRoot代码如下:
// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
function performConcurrentWorkOnRoot(root, didTimeout) {
...
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
...
}
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
...
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
...
}
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
可以看到上面代码中performConcurrentWorkOnRoot 是调度器调度的函数,shouldTimeSlice是判断是否开启了时间分片,开启之后走异步渲染renderRootConcurrent,否则走同步渲染renderRootSync。
异步渲染函数renderRootConcurrent中最后是调用了workLoopConcurrent函数,这个函数里面就是来执行上面说的(render阶段1、render阶段2、render阶段3)render子阶段,workInProgress是要执行的子阶段,shouldYield是判断主线程是否空闲,是否有高优先级任务需要让出执行。
- should这个可以联想到ES6中的generator中的yield,通过yield让出执行,切换给协程执行,然后通过next方法再切换到主线程执行;
接着我们再看下performUnitOfWork函数:
function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
其中 beginWork 方法是用来执行处理DOM,当前的入参unitOfWork也就是上一步的workInProgress,是一个fiber节点。
beginWork执行完会返回下一个要执行的节点next,如果没有返回,表示render结束了,DOM Diff处理完成,进入到commit阶段。如果有next,则赋值给workInProgress,再回到workLoopConcurrent循环中执行,如果shouldYield中断的话,就等待调度器调度再下次执行。
fiber数据结构
细心地同学可能会发现异步渲染和同步渲染还有个差别就是底层数据变成了fiber节点,fiber节点的数据结构和我们前面createElement返回的虚拟DOM节点数据结构还是有很大差别的。
我们看下react源码中是什么样的,Fiber节点:https://github.com/facebook/react/blob/4f29ba1cc52061e439cede3813e100557b23a15c/packages/react-reconciler/src/ReactInternalTypes.js
部分代码如下:
// A Fiber is work on a Component that needs to be done or was done. There can
// be more than one per component.
export type Fiber = {|
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.
// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.
// Tag identifying the type of fiber.
tag: WorkTag,
// Unique identifier of this child.
key: null | string,
// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
elementType: any,
// The resolved function/class/ associated with this fiber.
type: any,
// The local state associated with this fiber.
stateNode: any,
// Conceptual aliases
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.
// Remaining fields belong to Fiber
// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
// so this is only the parent of the thing we're currently processing.
// It is conceptually the same as the return address of a stack frame.
return: Fiber | null,
// Singly Linked List Tree Structure.
child: Fiber | null,
sibling: Fiber | null,
index: number,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: Fiber | null,
这里面我们主要了解下fiber节点的这些属性,知道用来做什么,后面我们实现代码需要用到。主要看下:
- stateNode:存放当前的虚拟DOM节点
- return:父级的fiber节点
- child:子fiber节点
- sibling:兄弟fiber节点
- alternate:双缓冲结构中的影子fiber节点
参考
- ReactConf 2017:http://conf2017.reactjs.org/speakers/lin
- Fiber 代码贡献指南:https://github.com/facebook/react/issues/7942
- yield:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/yield
- performConcurrentWorkOnRoot 源码:https://github.com/facebook/react/blob/4f29ba1cc52061e439cede3813e100557b23a15c/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L824
- workLoopConcurrent源码:https://github.com/facebook/react/blob/4f29ba1cc52061e439cede3813e100557b23a15c/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L1824-L1829
- Fiber节点:https://github.com/facebook/react/blob/4f29ba1cc52061e439cede3813e100557b23a15c/packages/react-reconciler/src/ReactInternalTypes.js