一、最大的改变或优化:
从依赖与内部堆栈的同步模型 --> 具有链表和指针到异步模型
如果你只依赖于[内置]调用堆栈,它将继续工作直到堆栈为空。。。 如果我们可以随意中断调用堆栈并手动操作堆栈帧,那不是很好吗?这就是 React Fiber 的目的。 Fiber 是堆栈的重新实现,专门用于 React 组件。 你可以将单个 Fiber 视为一个虚拟堆栈帧。
官网上的两张图片,非常的形象
fiber架构之前:
fiber架构之后
二、fiber对象 链式结构
fiber的链式结构很重要,对理解后面的diff算法很关键。
1. 概况、图解
fiber仅仅是一个对象,表征reconciliation阶段所能拆分的最小工作单元。通过child和sibling表征当前工作单元的下一个工作单元,return表示处理完成后返回结果所要合并的目标,通常指向父节点。整个结构是一个链表树。每个工作单元(fiber)执行完成后,都会查看是否还继续拥有主线程时间片,如果有继续下一个,如果没有则先处理其他高优先级事务,等主线程空闲下来继续执行。
2. 代码模拟实现
// fiber 结构
function fiberNode(instance){
this.instance = instance;
this.child = null;
this.sibling = null;
this.return = null;
}
// 构造链式结构
function link(parent,element){
if(element === null) element = [];
return parent.child.reduceRight((pre,current)=>{
var node = new current()
node.return = parent;
node.sibling = pre;
return node;
},null)
}
// 得到children数组,建立链式结构
function doWork(node){
var children = node.instance.render();
return link(node, children)
}
// 遍历dom树,构造fiber-tree
function walk(o){
var root = o;
var current = 0;
while(true){
let child = doWork(current)
// 1. 如果有子元素
if(child){
current = child;
continue;
}
// 2. 如果我们回到了根节点,退出函数
if (current === root) {
return;
}
// 3. 如果没有兄弟元素
while(!current.sibling){
// 如果回到里根节点,推出
if(!current.return || current.return === root){
return;
}
// 设置父节点为当活跃元素
current = current.return;
}
// 4. 如果有兄弟节点
current = current.sibling;
}
}
三、更新过程
一次更新过程过程分为render阶段和commit阶段。其中render阶段是可以根据任务的优先级和时间片段是否用完来中断、恢复。而commit是需要一鼓作气完成的。
1、render过程
render阶段主要工作就是对fiber树的一次深度优先遍历,并逐个收集effect,形成能够快速访问的单向链表(effect-list);并形成一个两棵通过alternate指针互相引用的缓存结构(复用fiber节点,减少创建、销毁的性能损耗)
render阶段主要工作:
- 更新节点状态和属性
- diff算法,计算出需要执行的 DOM 更新
- 收集钩子函数,形成快速访问的副作用的单向链表(effect-list)
下面是我基于源码(v17),整理的主要函数调用逻辑。
- 我们可以看到核心函数是beginWork,这个函数的主要作用就是根据当前fiber节点,和element元素计算出新的fiber(diff算法见下)。
- 第二个核心方法就是completeUnitOfWork,在这个函数中,主要就是通过firstEffect、lastEffect、nextEffect三个指针构成能够快速访问的单向链表。每个fiber元素的firstEffect指针都指向最先完成都叶子节点(即如图的Grandchild1),lastEffect指针指向孩子节点的最后一个兄弟节点。当然如果当前节点不存在副作用,则跳过。effect-list后面commit过程需要使用,很关键。
2、commit过程
最核心的就是三个do-while遍历上一步构造的effect-list单向链表。这三个循环分别对应了突变前、突变、突变后三个过程的生命周期方法。
3、调度原理
上面讲的render、commit过程都是铺垫,最重要的还是React的调度。
在开始讲调度之前,我们先回顾一下React里的一次更新。
- 首先从
updateContainer函数开始,这是所有更新的入口 - 将当前更新Update对象加入当前fiber对象的链表中,表现函数:
ensureUpdate - 然后重点来了,进入
scheduleUpdateOnRoot。顾名思义,就是所有的更新都是从Root开始调度。 - 在
scheduleUpdateOnRoot函数中,进行一些优先级的判断。如果当前更新是首渲,则执行performSyncWorkOnRoot(开始遍历、计算虚拟节点);其他情况就是另一个关键函数:ensureRootIsSchedule(开始调度了)
我们继续讲ensureRootIsSchedule函数,主要作用就是在Root发起一个调度。
- 和root上的原来的回调任务优先级做比较,相同(比如多个setState合并,最终渲染一次),则复用原来的回调任务并取消老回调任务;若是不同,则取消老回调任务,并新建回调任务
- 判断当前回调的优先级,分别调度(scheduleCallback或scheduleSyncCallback)不同优先级的任务(preformSyncWorkOnRoot或preformCurrentWorkOnRoo),并返回更新root
继续深入下去,看看scheduleCallback函数,这个函数就是来介绍如何处理回调任务的,最终是如何构造两个Head结构的任务队列(timeQueue,callbackQueue),源码表现在schedule/SchedulerDOM.js。
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
var startTime;
var timeout;
// 根据优先级计算超时时间
// ...
var expirationTime = startTime + timeout;
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
// 当前taskQueue空闲,且当前的新任务是最早的延时任务,那么执行当前的延时任务
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
// 有更早、更紧急的延时任务,所以先取消之前的延时调度
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}
以上我们可以看到最关键的两个函数:
requestHostTimeout、requestHostCallback,我们来一一讲讲。
1. 先讲requestHostTimeout
// 1. 就是利用定时器来做延时调用
function requestHostTimeout(callback, ms) {
taskTimeoutID = setTimeout(() => {
callback(getCurrentTime());
}, ms);
}
// 2. 关键是参数callback回调handleTimeout函数
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
// 最主要做的事情,遍历timerQueue,将过期的timerTask推入taskQueue。
advanceTimers(currentTime);
// 现在过期的延时任务已经全部转移至taskQueue中来=了,开始执行taskQueue中的任务
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
Å;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
// 回调自己,查看是否还有timerQueue需要转移至taskQueue
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
// 3. 来重点将advanceTimers函数
function advanceTimers(currentTime) {
// Check for tasks that are no longer delayed and add them to the queue.
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
// Timer was cancelled.
pop(timerQueue);
}
else if (timer.startTime <= currentTime) {
// Timer fired. Transfer to the task queue.
// 将到期的任务转移到taskQueue中执行
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
// 省略非主要代码
}
// 没到延迟时间的,继续等待执行
else {
// Remaining timers are pending.
return;
}
timer = peek(timerQueue);
}
}
小结:
requestHostTimeout作用就是将timerQueue中的任务,按照优先级(延时时间)分批转移至taskQueue中。在次过程中有不断的查看taskQueue,一旦taskQueue中有同步任务,就会进入requestHostCallback,暂停延时任务处理。 2. 我们继续说说requestHostCallback
// 定义一个消息管道
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
// 1.回调函数(调度的更新)作为宏任务执行。如果当前空闲,触发消息管道的回调函数
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
}
// 2. 触发回调函数(宏任务)
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// Yield after `yieldInterval` ms, regardless of where we are in the vsync
// cycle. This means there's always time remaining at the beginning of
// the message event.
// yieldInterval就是一帧的时间( 1000 / fps )
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// If there's more work, schedule the next message event at the end
// of the preceding one.
// taskQueue中还有任务,但是超过一帧的时间了,所以重新发起一个任务
port.postMessage(null);
}
} catch (error) {
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
};
// 3. 执行回调函数flushwork
function flushWork(hasTimeRemaining, initialTime) {
// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false;
// 如果在执行延时任务,则取消
if (isHostTimeoutScheduled) {
// We scheduled a timeout but it's no longer needed. Cancel it.
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
return workLoop(hasTimeRemaining, initialTime);
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
}
}
// 4. 遍历执行taskQueue中的任务,直到到达时间deadTime
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
// 执行前,判断并转移延时队列中的任务到当前队列
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// This currentTask hasn't expired, and we've reached the deadline.
// 重点,这里判断执行完任务后是否有时间执行下一个
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
小结:利用MessageChannl消息管道进行任务的分解。在一帧的时间内经可能多的执行taskQueue中的任务,若超过一帧的时间,则重新发起一个调度的宏任务(postMessage)。 参考: