react-fiber架构

806 阅读7分钟

一、最大的改变或优化:

从依赖与内部堆栈的同步模型 --> 具有链表和指针到异步模型

如果你只依赖于[内置]调用堆栈,它将继续工作直到堆栈为空。。。 如果我们可以随意中断调用堆栈并手动操作堆栈帧,那不是很好吗?这就是 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阶段主要工作:

  1. 更新节点状态和属性
  2. diff算法,计算出需要执行的 DOM 更新
  3. 收集钩子函数,形成快速访问的副作用的单向链表(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里的一次更新。

  1. 首先从updateContainer函数开始,这是所有更新的入口
  2. 将当前更新Update对象加入当前fiber对象的链表中,表现函数:ensureUpdate
  3. 然后重点来了,进入scheduleUpdateOnRoot。顾名思义,就是所有的更新都是从Root开始调度。
  4. scheduleUpdateOnRoot函数中,进行一些优先级的判断。如果当前更新是首渲,则执行performSyncWorkOnRoot(开始遍历、计算虚拟节点);其他情况就是另一个关键函数:ensureRootIsSchedule(开始调度了)

我们继续讲ensureRootIsSchedule函数,主要作用就是在Root发起一个调度。

  1. 和root上的原来的回调任务优先级做比较,相同(比如多个setState合并,最终渲染一次),则复用原来的回调任务并取消老回调任务;若是不同,则取消老回调任务,并新建回调任务
  2. 判断当前回调的优先级,分别调度(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;
}

以上我们可以看到最关键的两个函数:requestHostTimeoutrequestHostCallback,我们来一一讲讲。

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)。 参考:

  1. 非常好的文章