铺垫:调度策略

源码阅读 - 图1
这里借用探索 React 的内在 —— postMessage & Scheduler的一张图来总结下,整体调度的的一个架构图
scheduler.png
image.png
react的调度实现了自己的时间片控制(5ms),并且按照任务类型(延时和实时任务)加上任务优先级,通过消息通信类型宏任务实现了一套自己的调度方案,并且可以根据策略来终端和继续任务的执行

postMessage 如何运作?

主要就是通过 performWorkUntilDeadline 这个方法来实现一个递归的消息 发送-接收-处理 流程,来实现任务的处理

任务如何被处理?

一切都围绕着两个最小优先队列进行:

  • taskQueue
  • timerQueue

任务被按照一定的优先级规则进行预设,而这些预设的主要目的就是确认执行时机(timeoutForPriorityLevel)。
没当开始处理一系列任务的时候(flushWork),会产生一个 while 循环(workLoop)来不断地对队列中的内容进行处理,这期间还会逐步的将被递延任务从 timerQueue 中梳理(advanceTimers)到 taskQueue 中,使得任务能按预设的优先级有序的执行。甚至,对于更高阶的任务回调实现,还可以将任务“分段进行”(continuationCallback)。
而穿插在这整个过程中的一个原则是所有的任务都尽量不占用与用户感知最密切的浏览器任务(needsPainiting & isInputPending),当然,这一点能做得多极致也与浏览器的实现(navigator.scheduling)有关

React-Fiber

基于 React v16.8.6 版本源码:参考React-Fiberimage.png
expirationTime以前是越小代表优先级越高,后来变成越大越高,再后来废弃了,17.xx改用了lane

关键结构:work、Fiber、workTag、effectTag

Reconciliation 和 Scheduling

协调(Reconciliation):
简而言之,根据 diff 算法来比较虚拟 DOM,从而可以确认哪些部分的 React 元素需要更改。
调度(Scheduling):
可以简单理解为是一个确定在什么时候执行 work 的过程。

Render 阶段和 Commit 阶段

Current 树和 WorkInProgress 树

Effects list

effect list 可以理解为是一个存储 effectTag 副作用列表容器。它是由 fiber 节点和指针 nextEffect 构成的单链表结构,这其中还包括第一个节点 firstEffect,和最后一个节点 lastEffect。如下图所示:
image.png
React 采用深度优先搜索算法,在 render 阶段遍历 fiber 树时,把每一个有副作用的 fiber 筛选出来,最后构建生成一个只带副作用的 effect list 链表。
在 commit 阶段,React 拿到 effect list 数据后,通过遍历 effect list,并根据每一个 effect 节点的 effectTag 类型,从而对相应的 DOM 树执行更改。

React-Hooks

基于16.8.xx:参考React-Hooks
react-reconciler/src/ReactFiberHooks.js -> mountWorkInProgressHook, renderWithHooks
源码阅读 - 图6
关键词:ReactCurrentDispatcher.current、renderWithHooks、mountState、updateReducer、。。。

概念解释

current fiber树: 当完成一次渲染之后,会产生一个current树,current会在commit阶段替换成真实的Dom树。
workInProgress fiber树: 即将调和渲染的 fiber 树。再一次新的组件更新过程中,会从current复制一份作为workInProgress,更新完毕后,将当前的workInProgress树赋值给current树。
workInProgress.memoizedState: 在class组件中,memoizedState存放state信息,在function组件中,memoizedState在一次调和渲染过程中,以链表的形式存放hooks信息。
workInProgress.expirationTime: react用不同的expirationTime,来确定更新的优先级。
currentHook : 可以理解 current树上的指向的当前调度的 hooks节点。
workInProgressHook : 可以理解 workInProgress树上指向的当前调度的 hooks节点。

renderWithHooks函数主要作用:
首先先置空即将调和渲染的workInProgress树的memoizedState和updateQueue,为什么这么做,因为在接下来的函数组件执行过程中,要把新的hooks信息挂载到这两个属性上,然后在组件commit阶段,将workInProgress树替换成current树,替换真实的DOM元素节点。并在current树保存hooks信息。
然后根据当前函数组件是否是第一次渲染,赋予ReactCurrentDispatcher.current不同的hooks,终于和上面讲到的ReactCurrentDispatcher联系到一起。对于第一次渲染组件,那么用的是HooksDispatcherOnMount hooks对象。 对于渲染后,需要更新的函数组件,则是HooksDispatcherOnUpdate对象,那么两个不同就是通过current树上是否memoizedState(hook信息)来判断的。如果current不存在,证明是第一次渲染函数组件。
接下来,调用Component(props, secondArg);执行我们的函数组件,我们的函数组件在这里真正的被执行了,然后,我们写的hooks被依次执行,把hooks信息依次保存到workInProgress树上。 至于它是怎么保存的,我们马上会讲到。
接下来,也很重要,将ContextOnlyDispatcher赋值给 ReactCurrentDispatcher.current,由于js是单线程的,也就是说我们没有在函数组件中,调用的hooks,都是ContextOnlyDispatcher对象上hooks,我们看看ContextOnlyDispatcherhooks,到底是什么。
原来如此,react-hooks就是通过这种函数组件执行赋值不同的hooks对象方式,判断在hooks执行是否在函数组件内部,捕获并抛出异常的。
最后,重新置空一些变量比如currentHook,currentlyRenderingFiber,workInProgressHook等。

不同的hooks对象

HooksDispatcherOnMount 和 HooksDispatcherOnUpdate

  1. const HooksDispatcherOnMount = {
  2. useCallback: mountCallback,
  3. useEffect: mountEffect,
  4. useLayoutEffect: mountLayoutEffect,
  5. useMemo: mountMemo,
  6. useReducer: mountReducer,
  7. useRef: mountRef,
  8. useState: mountState,
  9. };
  10. const HooksDispatcherOnUpdate = {
  11. useCallback: updateCallback,
  12. useEffect: updateEffect,
  13. useLayoutEffect: updateLayoutEffect,
  14. useMemo: updateMemo,
  15. useReducer: updateReducer,
  16. useRef: updateRef,
  17. useState: updateState
  18. };

一些代码

会在Component(props, xxx);执行前赋值给ReactCurrentDispatcher.current; 函数组件执行完毕后重新赋值为ContextOnlyDispatcher;保证在非函数式组件内以外的地方调用时报错。

  1. export function renderWithHooks(
  2. current,
  3. workInProgress,
  4. Component,
  5. props,
  6. secondArg,
  7. nextRenderExpirationTime,
  8. ) {
  9. renderExpirationTime = nextRenderExpirationTime;
  10. currentlyRenderingFiber = workInProgress;
  11. workInProgress.memoizedState = null;
  12. workInProgress.updateQueue = null;
  13. workInProgress.expirationTime = NoWork;
  14. ReactCurrentDispatcher.current =
  15. current === null || current.memoizedState === null
  16. ? HooksDispatcherOnMount
  17. : HooksDispatcherOnUpdate;
  18. let children = Component(props, secondArg);
  19. if (workInProgress.expirationTime === renderExpirationTime) {
  20. // ....这里的逻辑我们先放一放
  21. }
  22. ReactCurrentDispatcher.current = ContextOnlyDispatcher;
  23. renderExpirationTime = NoWork;
  24. currentlyRenderingFiber = null;
  25. currentHook = null
  26. workInProgressHook = null;
  27. didScheduleRenderPhaseUpdate = false;
  28. return children;
  29. }
  1. function mountWorkInProgressHook() {
  2. const hook: Hook = {
  3. memoizedState: null, // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
  4. baseState: null,
  5. baseQueue: null,
  6. queue: null,
  7. next: null,
  8. };
  9. if (workInProgressHook === null) { // 例子中的第一个`hooks`-> useState(0) 走的就是这样。
  10. currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  11. } else {
  12. workInProgressHook = workInProgressHook.next = hook;
  13. }
  14. return workInProgressHook;
  15. }
  1. function updateWorkInProgressHook() {
  2. let nextCurrentHook;
  3. if (currentHook === null) { /* 如果 currentHook = null 证明它是第一个hooks */
  4. const current = currentlyRenderingFiber.alternate;
  5. if (current !== null) {
  6. nextCurrentHook = current.memoizedState;
  7. } else {
  8. nextCurrentHook = null;
  9. }
  10. } else { /* 不是第一个hooks,那么指向下一个 hooks */
  11. nextCurrentHook = currentHook.next;
  12. }
  13. let nextWorkInProgressHook
  14. if (workInProgressHook === null) { //第一次执行hooks
  15. // 这里应该注意一下,当函数组件更新也是调用 renderWithHooks ,memoizedState属性是置空的
  16. nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  17. } else {
  18. nextWorkInProgressHook = workInProgressHook.next;
  19. }
  20. if (nextWorkInProgressHook !== null) {
  21. /* 这个情况说明 renderWithHooks 执行 过程发生多次函数组件的执行 ,我们暂时先不考虑 */
  22. workInProgressHook = nextWorkInProgressHook;
  23. nextWorkInProgressHook = workInProgressHook.next;
  24. currentHook = nextCurrentHook;
  25. } else {
  26. invariant(
  27. nextCurrentHook !== null,
  28. 'Rendered more hooks than during the previous render.',
  29. );
  30. currentHook = nextCurrentHook;
  31. const newHook = { //创建一个新的hook
  32. memoizedState: currentHook.memoizedState,
  33. baseState: currentHook.baseState,
  34. baseQueue: currentHook.baseQueue,
  35. queue: currentHook.queue,
  36. next: null,
  37. };
  38. if (workInProgressHook === null) { // 如果是第一个hooks
  39. currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
  40. } else { // 重新更新 hook
  41. workInProgressHook = workInProgressHook.next = newHook;
  42. }
  43. }
  44. return workInProgressHook;
  45. }

React 16.0 工作流程

注意,这节是16.0的源码分析!此时还没有Hooks
image.png

schedule阶段

image.png
flushWork就是requestHostCallback的参数callback
所以,Sync模式下,更新是异步的?不,就是同步的。 在flushSyncCallbackQueueImpl中,同步模式下,executionContext===NoContext,此时会直接执行flushSyncCallbackQuue();然后后面的函数就是同步的了。
image.png

render阶段—reconcilation

workLoopSync会在performSyncWorkOnRoot当中执行
image.png
image.png
image.png
image.png

commit阶段

image.png

React 16.0 Concurrent模式

image.png
React更新任务被打断的时机有两个:

  1. workLoop里循环取出并执行完一个任务时,都会检查是否需要打断
  2. workLoopConcurrent里,会遍历每个Fiber,每个Fiber任务完成后,也会有机会终止此次流程

image.pngimage.png
image.pngimage.png
image.pngimage.png
image.png从第一个被跳过的update开始都会被保存到队列中
image.png