• 深入理解React中fiber


    一、前言

    Fiber是对React核心算法的重写,Fiber是React内部定义的一种数据结构,将更新渲染耗时长的大任务,分为许多的小片。Fiber节点保存啦组件需要更新的状态和副作用,一个Fiber代表一个工作单元。

    二、Fiber在React做了什么

    在react中,主要做了下面这些操作:

    • 为每个增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任务

    • 增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行

    • dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行

    Fiber中的属性

    1. type Fiber = {
    2.   // 用于标记fiber的WorkTag类型,主要表示当前fiber代表的组件类型如FunctionComponent、ClassComponent等
    3.   tag: WorkTag,
    4.   // ReactElement里面的key
    5.   keynull | string,
    6.   // ReactElement.type,调用`createElement`的第一个参数
    7.   elementType: any,
    8.   // The resolved function/class/ associated with this fiber.
    9.   // 表示当前代表的节点类型
    10.   typeany,
    11.   // 表示当前FiberNode对应的element组件实例
    12.   stateNode: any,
    13.   // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
    14.   return: Fiber | null,
    15.   // 指向自己的第一个子节点
    16.   child: Fiber | null,
    17.   // 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
    18.   sibling: Fiber | null,
    19.   indexnumber,
    20.   refnull | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,
    21.   // 当前处理过程中的组件props对象
    22.   pendingProps: any,
    23.   // 上一次渲染完成之后的props
    24.   memoizedProps: any,
    25.   // 该Fiber对应的组件产生的Update会存放在这个队列里面
    26.   updateQueue: UpdateQueue<any> | null,
    27.   // 上一次渲染的时候的state
    28.   memoizedState: any,
    29.   // 一个列表,存放这个Fiber依赖的context
    30.   firstContextDependency: ContextDependency<mixed> | null,
    31.   mode: TypeOfMode,
    32.   // Effect
    33.   // 用来记录Side Effect
    34.   effectTag: SideEffectTag,
    35.   // 单链表用来快速查找下一个side effect
    36.   nextEffect: Fiber | null,
    37.   // 子树中第一个side effect
    38.   firstEffect: Fiber | null,
    39.   // 子树中最后一个side effect
    40.   lastEffect: Fiber | null,
    41.   // 代表任务在未来的哪个时间点应该被完成,之后版本改名为 lanes
    42.   expirationTime: ExpirationTime,
    43.   // 快速确定子树中是否有不在等待的变化
    44.   childExpirationTime: ExpirationTime,
    45.   // fiber的版本池,即记录fiber更新过程,便于恢复
    46.   alternate: Fiber | null,
    47. }

    三、从架构的角度理解Fiber

    增量渲染

    把一个渲染任务分解成多个,然后分散在多个帧。实现任务可以中断、可以恢复,并且可以给不同的任务赋予不同的优先级,最终实现更加丝滑的用户体验。

    React16之前,React的渲染和更新依赖分为Reconciler->Render,Reconciler对比新旧虚拟DOM(Document Object Model)的变化,Render将变化应用到视图。

    React16加多了一个Scheduler,用来调度更新的优先级。更新的流程变成:每一个更新的任务都被赋予一个优先级,Scheduler把优先级高的先Reconciler,如果有一个优先级比之前的任务更高的,之前的任务会中断,执行完后,新一轮调度之前被中断的任务会重新Reconciler,继续渲染。

    四、Fiber的concurrent模式

    在React中,异步渲染中“时间切片”、“优先级”是Scheduler的核心能力,Scheduler在源码目录中与react-dom是同级的。

    图片

    我们都知道浏览器的刷新频率是60Hz,每16.6ms会刷新一次,没开启Concurrent模式,可以看到浏览器的Task中灰色的那长条不可中断任务,调用了createRoot后,那条大任务被切割成许个个小任务。切割后的小任务工作量加起来跟之前那条大任务是一样的,这就是“时间切片”效果。

    如何实现时间切片

    在源代码中,搜索workLoopSync函数就可以看到。

    图片

    1.   function wrokLoopSync () {
    2.       while (workInProgress !== null) {
    3.         performUnitOfWork(workInProgress)
    4.       }
    5.     }

    同步渲染wrokLoopSync中while循环中触发下一个同步performUnitOfWork。

    图片

    异步渲染workLoopConcurrent中while循环触发也是performUnitOfWork,只不过多了一个shouldYield,这个是用来处理让出主进程的。

    初略理解:

    图片

    React根据浏览器的帧率计算出时间切片大小,结合当前时间计算每一个切片的到期时间,workLoopConcurrent中每一个循环都会判断是否到期,让出主线程。

    如何实现优先级调度

    Scheduler中的unstable_scheduleCallback函数是一个核心方法,处理任务的优先级执行不同的调度逻辑。

    在源码路径~/packages/scheduler/src/forks/Scheduler.js中可以看到这个方法。

    1. function unstable_scheduleCallback(
    2.   priorityLevel: PriorityLevel,
    3.   callback: Callback,
    4.   options?: {delay: number},
    5. ): Task {
    6.   //  获取当前时间
    7.   var currentTime = getCurrentTime();
    8.  // 任务的预期开始时间
    9.   var startTime;
    10.   // 处理options的入参
    11.   if (typeof options === 'object' && options !== null) {
    12.     var delay = options.delay;
    13.     // 如果定义了延迟时间,在加上这个延迟时间
    14.     if (typeof delay === 'number' && delay > 0) {
    15.       startTime = currentTime + delay;
    16.     } else {
    17.       startTime = currentTime;
    18.     }
    19.   } else {
    20.     startTime = currentTime;
    21.   }
    22.  // 处理exoirationTime的计算依据
    23.   var timeout;
    24.   // 根据priorityLevel给timeout赋值
    25.   switch (priorityLevel) {
    26.     case ImmediatePriority:
    27.       timeout = IMMEDIATE_PRIORITY_TIMEOUT;
    28.       break;
    29.     case UserBlockingPriority:
    30.       timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
    31.       break;
    32.     case IdlePriority:
    33.       timeout = IDLE_PRIORITY_TIMEOUT;
    34.       break;
    35.     case LowPriority:
    36.       timeout = LOW_PRIORITY_TIMEOUT;
    37.       break;
    38.     case NormalPriority:
    39.     default:
    40.       timeout = NORMAL_PRIORITY_TIMEOUT;
    41.       break;
    42.   }
    43.   // 优先级越高,timeout越小,expirationTime越小
    44.   var expirationTime = startTime + timeout;
    45.  // 创建任务对象
    46.   var newTask: Task = {
    47.     id: taskIdCounter++,
    48.     callback,
    49.     priorityLevel,
    50.     startTime,
    51.     expirationTime,
    52.     sortIndex: -1,
    53.   };
    54.   if (enableProfiling) {
    55.     newTask.isQueued = false;
    56.   }
    57.  // 如果当前时间小于开始时间,说明该任务可以延迟(还没过期)
    58.   if (startTime > currentTime) {
    59.     // 延迟任务
    60.     newTask.sortIndex = startTime;
    61.     push(timerQueue, newTask);
    62.     // 如果任务队列没有可以执行的任务,而且当前任务又是任务队列的第一个任务
    63.     if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
    64.       // All tasks are delayed, and this is the task with the earliest delay.
    65.       if (isHostTimeoutScheduled) {
    66.         // Cancel an existing timeout.
    67.         cancelHostTimeout();
    68.       } else {
    69.         isHostTimeoutScheduled = true;
    70.       }
    71.       // 派发一个延时任务,检查是否过期
    72.       requestHostTimeout(handleTimeout, startTime - currentTime);
    73.     }
    74.   } else {
    75.     // 处理任务过期逻辑
    76.     newTask.sortIndex = expirationTime;
    77.     // 过期任务推入taskQueue
    78.     push(taskQueue, newTask);
    79.     if (enableProfiling) {
    80.       markTaskStart(newTask, currentTime);
    81.       newTask.isQueued = true;
    82.     }
    83.     // Schedule a host callback, if needed. If we're already performing work,
    84.     // wait until the next time we yield.
    85.     if (!isHostCallbackScheduled && !isPerformingWork) {
    86.       isHostCallbackScheduled = true;
    87.       // 执行taskQueue中的任务
    88.       requestHostCallback();
    89.     }
    90.   }
    91.   return newTask;
    92. }

    这个函数大概意思:

    创建task,然后根据startTime任务的预期开始时间把task推入timerQueue或者taskQueue,最后根据timerQueue、taskQueue执行延时任务或者即时任务。

    从上面的函数可以看出几个关键信息:

    • expirationTime越小,任务优先级越高

    • timerQueue是用来存储待执行的任务

    • taskQueue是用开存储过期的任务

    五、Fiber中的一些函数

    createFiber

    mount过程中,创建了 rootFiber,是 react 应用的根 fiber。

    1. function createFiber(
    2.   tag: WorkTag,
    3.   pendingProps: mixed,
    4.   keynull | string,
    5.   mode: TypeOfMode,
    6. ): Fiber {
    7.   // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
    8.   return new FiberNode(tag, pendingProps, key, mode);
    9. }

    Fiber的深度遍历

    开始:Fiber 从最上面的 React 元素开始遍历,并为其创建一个 fiber 节点。

    子节点:然后,它转到子元素,为这个元素创建一个 fiber 节点。这样继续下去直到在没有孩子

    兄弟节点:现在,它检查是否有兄弟节点元素。如果有,它就遍历兄弟节点元素,然后再到兄弟姐妹的叶子元素。

    返回:如果没有兄弟节点,那么它就返回到父节点。

    createWorkInProgress

    更新过程,创建 workInProgress fiber,对其标记副作用。

    current Fiber 中每个 fiber 节点通过 alternate 字段,指向 workInProgress Fiber 中对应的 fiber 节点。同样 workInProgress Fiber 中的 fiber 节点的 alternate 字段也会指向 current Fiber 中对应的 fiber 节点。

    源代码路径~/packages/react-reconciler/src/ReactFiber.js

    图片

    window.requestIdleCallback()

    将在浏览器的空闲时段内调用的函数排队。方法提供 deadline,即任务执行限制时间,以切分任务,避免长时间执行,阻塞UI渲染而导致掉帧;

    【安排低优先级或非必要的函数在帧结束时的空闲时间被调用】

    requestAnimationFrame

    图片

    安排高优先级的函数在下一个动画帧之前被调用

    六、最后

     React Fiber scheduler将工作分为多个工作单元。它设置每个工作的优先级,并使暂停、重用和中止工作单元。

  • 相关阅读:
    C++类和对象(一)
    Timed out waiting for device dev-ttymxc3.device.
    Flask 实现增改及分页查询的完整 Demo
    Redis源码分析-存储原理与数据模型
    计算机网络安全隔离之网闸、光闸
    23. 图论 - 图的由来和构成
    STM32F4XX - 系统定时器(SysTick)设置
    Vue3 SFC 和 TSX 方式调用子组件中的函数
    Leetcode链表问题汇总
    微信小程序可拖拽视频播放案例
  • 原文地址:https://blog.csdn.net/2301_79226238/article/details/133342218