react16-Fiber架构:改变了之前react的组件渲染机制,新的架构使原来同步渲染的组件现在可以异步化,可中途中断渲染,执行更高优先级的任务,释放浏览器主线程。
React 核心算法的更新 —— 这次更新提供了一个从底层重写了 React 的 reconciliation 算法(译注:reconciliation 算法,是 React 用来比较两棵 DOM 树差异、从而判断哪一部分应当被更新的算法)。这次算法重写带来的主要特性是异步渲染。异步渲染的意义在于能够将渲染任务划分为多块。浏览器的渲染引擎是单线程的,这意味着几乎所有的行为都是同步发生的。React 16 使用原生的浏览器 API 来间歇性地检查当前是否还有其他任务需要完成,从而实现了对主线程和渲染过程的管理。在之前的版本中,React 会在计算 DOM 树的时候锁住整个线程。这个 reconciliation 的过程现在被称作 “stack reconciliation”。尽管 React 已经是以快而闻名了,但是锁住整个线程也会让一些应用运行得不是很流畅。16 这个版本通过不要求渲染过程在初始化后一次性完成修复了该问题。React 计算了 DOM 树的一部分,之后将暂停渲染,来看看主线程是否有任何的绘图或者更新需要去完成。一旦绘图和更新完成了,React 就会继续渲染。这个过程通过引入了一个新的,叫做 “fiber” 的数据结构完成,fiber 映射到了一个 React 实例并为该实例管理其渲染任务,它也知道它和其他 fiber 之间的关系。
react16以前的组件渲染方式存在一个问题,如果这是一个很大,层级很深的组件,react渲染它需要几十甚至几百毫秒,在这期间,react会一直占用浏览器主线程,任何其他的操作(包括用户的点击,鼠标移动等操作)都无法执行。好似一个潜水员,当它一头扎进水里,就要往最底层一直游,直到找到最底层的组件,然后他再上岸。在这期间,岸上发生的任何事,都不能对他进行干扰,如果有更重要的事情需要他去做(如用户操作),也必须得等他上岸。fiber架构—组件的渲染顺序:潜水员会每隔一段时间就上岸,看是否有更重要的事情要做。加入fiber的react将组件更新分为两个时期(phase 1 && phase 2),render前的生命周期为phase1,render后的生命周期为phase2。
phase1的生命周期是可以被打断的,每隔一段时间它会跳出当前渲染进程,去确定是否有其他更重要的任务。此过程,React 在 workingProgressTree (并不是真实的virtualDomTree)上复用 current 上的 Fiber 数据结构来一步步地构建新的 tree,标记需要更新的节点,放入队列中。phase2的生命周期是不可被打断的,React 将其所有的变更一次性更新到DOM上。这里最重要的是phase1这是时期所做的事。因此我们需要具体了解phase1的机制。
如果不被打断,那么phase1执行完会直接进入render函数,构建真实的virtualDomTree。如果组件再phase1过程中被打断,即当前组件只渲染到一半(也许是在willMount,也许是willUpdate~反正是在render之前的生命周期),那么react会怎么干呢? react会放弃当前组件所有干到一半的事情,去做更高优先级更重要的任务(当然,也可能是用户鼠标移动,或者其他react监听之外的任务)。当所有高优先级任务执行完之后,react通过callback回到之前渲染到一半的组件,从头开始渲染。
React 16 也会在必要的时候管理各个更新的优先级。这就允许了高优先级更新能够排到队列开头从而被首先处理。关于此的一个例子就是按键输入。鉴于应用流畅性的考虑,用户需要立即获得按键响应,因而相对于那些可以等待 100-200 毫秒的低优先级更新任务,按键输入拥有较高优先级。
- Fiber节点的数据结构
{ tag: TypeOfWork, // fiber的类型,下一节会介绍 alternate: Fiber|null, // 在fiber更新时克隆出的镜像fiber,对fiber的修改会标记在这个fiber上 return: Fiber|null, // 指向fiber树中的父节点 child: Fiber|null, // 指向第一个子节点 sibling: Fiber|null, // 指向兄弟节点 effectTag: TypeOfSideEffect, // side effect类型,下文会介绍 nextEffect: Fiber | null, // 单链表结构,方便遍历fiber树上有副作用的节点 pendingWorkPriority: PriorityLevel, // 标记子树上待更新任务的优先级 }
在实际的渲染过程中,Fiber节点构成了一颗树。这棵树在数据结构上是通过单链表的形式构成的,Fiber节点上的chlid
和sibling
属性分别指向了这个节点的第一个子节点和相邻的兄弟节点。这样就可以遍历整个Fiber树了。
- TypeOfWork:代表React中不同类型的fiber节点。
{ IndeterminateComponent: 0, // Before we know whether it is functional or class FunctionalComponent: 1, ClassComponent: 2, HostRoot: 3, // Root of a host tree. Could be nested inside another node. HostPortal: 4, // A subtree. Could be an entry point to a different renderer. HostComponent: 5, HostText: 6, CoroutineComponent: 7, CoroutineHandlerPhase: 8, YieldComponent: 9, Fragment: 10, }
ClassComponent:就是应用层面的React组件。ClassComponent是一个继承自React.Component的类的实例。
HostRoot:ReactDOM.render()时的根节点。
HostComponent:React中最常见的抽象节点,是ClassComponent的组成部分。具体的实现取决于React运行的平台。在浏览器环境下就代表DOM节点,可以理解为所谓的虚拟DOM节点。HostComponent中的Host就代码这种组件的具体操作逻辑是由Host环境注入的。
- TypeOfSideEffect:这是以二进制位表示的,可以多个叠加。
{ NoEffect: 0, PerformedWork: 1, Placement: 2, // 插入 Update: 4, // 更新 PlacementAndUpdate: 6, Deletion: 8, // 删除 ContentReset: 16, Callback: 32, Err: 64, Ref: 128, }
- setState:用户触发的
setState
开启的一次渲染
Component.prototype.setState = function (partialState, callback) { !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? invariant_1(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : void 0; this.updater.enqueueSetState(this, partialState, callback, 'setState'); };
setState
调用了this.updater.enqueueSetState
。updater是renderer在渲染的时候注入的对象,这个对象由reconciler提供。
var classComponentUpdater = { isMounted: isMounted, enqueueSetState: function (inst, payload, callback) { var fiber = get(inst); // 从全局拿到React组件实例对应的fiber var currentTime = recalculateCurrentTime(); var expirationTime = computeExpirationForFiber(currentTime, fiber); // 计算fiber的优先级 var update = createUpdate(expirationTime); update.payload = payload; if (callback !== undefined && callback !== null) { { warnOnInvalidCallback$1(callback, 'setState'); } update.callback = callback; } enqueueUpdate(fiber, update, expirationTime); // 向队列中推入需要更新的fiber scheduleWork$1(fiber, expirationTime); // 触发调度器调度一次新的更新 }, //... }
- performUnitOfWork:React 16保持了之前版本的事务风格,一个“work”会被分解为begin和complete两个阶段来完成。
function performUnitOfWork(workInProgress) { // 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. var current = workInProgress.alternate; // See if beginning this work spawns more work. startWorkTimer(workInProgress); { ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } if (true && replayFailedUnitOfWorkWithInvokeGuardedCallback) { stashedWorkInProgressProperties = assignFiberPropertiesInDEV(stashedWorkInProgressProperties, workInProgress); } var next = void 0; if (enableProfilerTimer) { if (workInProgress.mode & ProfileMode) { startBaseRenderTimer(); } next = beginWork(current, workInProgress, nextRenderExpirationTime); if (workInProgress.mode & ProfileMode) { // Update "base" time if the render wasn't bailed out on. recordElapsedBaseRenderTimeIfRunning(workInProgress); stopBaseRenderTimerIfRunning(); } } else { next = beginWork(current, workInProgress, nextRenderExpirationTime); } // ... if (next === null) { // If this doesn't spawn new work, complete the current work. next = completeUnitOfWork(workInProgress); } ReactCurrentOwner.current = null; return next; }
- beginWork:根据fiber节点不同的tag,调用对应的update方法。可以说是一个入口函数。
function beginWork(current, workInProgress, renderExpirationTime) { // ... switch (workInProgress.tag) { case ClassComponent: return updateClassComponent(current, workInProgress, renderExpirationTime); // ClassComponent对应的是React组件实例 case HostRoot: return updateHostRoot(current, workInProgress, renderExpirationTime); case HostComponent: return updateHostComponent(current, workInProgress, renderExpirationTime); // HostComponent对应的是一个视图层节点,在浏览器环境中就等于DOM节点 // ... } }
- updateClassComponent
updateHostComponent
:HostComponent没有生命周期钩子需要处理,这个函数主要做的就是调用reconcileChildren
对子节点进行diff。
- reconcileChildren:Virtul DOM diff
// TODO: Remove this and use reconcileChildrenAtExpirationTime directly. function reconcileChildren(current, workInProgress, nextChildren) { reconcileChildrenAtExpirationTime(current, workInProgress, nextChildren, workInProgress.expirationTime); } function reconcileChildrenAtExpirationTime(current, workInProgress, nextChildren, renderExpirationTime) { if (current === null) { // 首次渲染,创建子节点fiber实例 // If this is a fresh new component that hasn't been rendered yet, we // won't update its child set by applying minimal side-effects. Instead, // we will add them all to the child before it gets rendered. That means // we can optimize this reconciliation pass by not tracking side-effects. workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderExpirationTime); } else { // 未处理过子节点;处理过子节点被中断,丢弃之前的处理工作 // If the current child is the same as the work in progress, it means that // we haven't yet started any work on these children. Therefore, we use // the clone algorithm to create a copy of all the current children. // If we had any progressed work already, that is invalid at this point so // let's throw it out. workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderExpirationTime); } }
var reconcileChildFibers = ChildReconciler(true); var mountChildFibers = ChildReconciler(false);
mountChildFibers
、reconcileChildFibers
这两个函数其实是同一个函数,通过传入不同的参数“重载”而来的。
ChildReconciler是一个工厂函数,它接收shouldTrackSideEffects这个参数。reconcileChildFibers函数的目的是产出effect list,mountChildFibers是组件初始化时用的,所以不用clone fiber来diff,也不用产出effect list。ChildReconciler内部有很多helper函数,最终返回的函数叫reconcileChildFibers,这个函数实现了对子fiber节点的reconciliation。
总的,这个函数根据newChild的类型调用不同的方法。newChild可能是一个元素,也可能是一个数组(React16新特性)。如果是reconcile单个元素,以reconcileSingleElement为例比较key和type,如果相同,复用fiber,删除多余的元素(currentFirstChild的sibling),如果不同,调用createFiberFromElement,返回新创建的。
如果是string,reconcileSingleTextNode;如果是array,reconcileChildrenArray;如果是空,deleteRemainingChildren删除老的子元素
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, expirationTime) { // This function is not recursive. // If the top level item is an array, we treat it as a set of children, // not as a fragment. Nested arrays on the other hand will be treated as // fragment nodes. Recursion happens at the normal flow. // Handle top level unkeyed fragments as if they were arrays. // This leads to an ambiguity between <>{[...]}</> and <>...</>. // We treat the ambiguous cases above the same. var isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null; if (isUnkeyedTopLevelFragment) { newChild = newChild.props.children; } // Handle object types var isObject = typeof newChild === 'object' && newChild !== null; if (isObject) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, expirationTime)); case REACT_PORTAL_TYPE: return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, expirationTime)); } } if (typeof newChild === 'string' || typeof newChild === 'number') { return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, expirationTime)); } if (isArray$1(newChild)) { return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, expirationTime); } if (getIteratorFn(newChild)) { return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, expirationTime); } if (isObject) { throwOnInvalidObjectType(returnFiber, newChild); } { if (typeof newChild === 'function') { warnOnFunctionType(); } } // ... // Remaining cases are all treated as empty. return deleteRemainingChildren(returnFiber, currentFirstChild); }
React的reconcile算法采用的是层次遍历,这种算法是建立在一个节点的插入、删除、移动等操作都是在节点树的同一层级中进行这个假设下的。所以reconcile算法的核心就是如何diff两个子节点数组。
- reconcileChildrenArray:React16的diff算法采用和来自社区的两端同时比较法同样结构的算法
因为fiber树是单链表结构,没有子节点数组这样的数据结构,也就没有可以供两端同时比较的尾部游标。所以React的这个算法是一个简化的两端比较法,只从头部开始比较。第一次遍历新数组,对上了,新老index都++,比较新老数组哪些元素是一样的,(通过updateSlot,比较key),如果是同样的就update。第一次遍历玩了,如果新数组遍历完了,那就可以把老数组中剩余的fiber删除了。如果老数组完了新数组还没完,那就把新数组剩下的都插入。如果这些情况都不是,就把所有老数组元素按key放map里,然后遍历新数组,插入老数组的元素,这是移动的情况。最后再删除没有被上述情况涉及的元素(也就是老数组中有新数组中无的元素,上面的删除只是fast path,特殊情况)
- completeUnitOfWork
completeUnitOfWork
是complete阶段的入口。complete阶段的作用就是在一个节点diff完成之后,对它进行一些收尾工作,主要是更新props和调用生命周期方法等等。completeUnitOfWork
主要的逻辑是调用completeWork
完成收尾,然后将当前子树的effect list插入到HostRoot的effect list中。
- completeWork:complete阶段主要工作都是在
completeWork
中完成的
completeWork主要是完成reconciliation阶段的扫尾工作,重点是对HostComponent的props进行diff,并标记更新。reconciliation阶段主要负责产出effect list。reconcile的过程相当于是一个纯函数,输入是fiber节点,输出一个effect list。side-effects是在commit阶段被应用到UI中的,这样就将side-effects从reconciliation中隔离开了。因为纯函数的可预测性,让我们可以随时中断reconciliation阶段的执行,而不用担心side-effects给让组件状态和实际UI产生不一致。
- commitRoot
reconciliation阶段结束之后,我们需要将effect list更新到UI中。这就是commit节点的工作。commit这个阶段有点像Git的commit概念。在缓冲区中的代码改动只有在commit之后才会被添加到Git的Object store中。