React17 源码分析

2,300 阅读22分钟

前言

react 17.0版本并没有添加新的功能,而是专注于瘦身、提升性能,官方声明正在开发新功能,未来将在此版本基础上进行更新迭代,作为"垫脚石"的17版本,有些东西还是依旧值得细品,你品,你细品~

文章部分内容由 @文轩 授权提供

忆往昔

v15

react15的架构可以分为两层:

  • Reconciler(协调器)— 收集需要更新的组件、patch Vnode 更新标识
  • Renderer(渲染器)— 将变化后的组件进行 dom-diff => 渲染到页面上

V15的reconciler是stack-reconciler。是采用递归形式工作的,是同步的,在生成虚拟dom树并diff过程中是无法中断的。这种方式在组件层级过深时,会造成线程一直被占用,浏览器无法布局和绘制,造成丢帧、卡顿

v16

react16的架构可以分为三层:

  • Scheduler(调度器)— 调度任务的优先级,高级优先级的优先进入Reconciler阶段
  • Reconciler(协调器)— 收集需要更新的组件:fiber root 构建 - patch - Vnode更新标识
  • Renderer(渲染器)— 将变化后的组件进行 dom-diff => 渲染到页面上

Fiber

顾名思义,fiber(纤维),它是比线程更小的粒度

  • V16 增加了调度器,引入 fiber 协程管理,通过异步可中断更新,替代 V15的同步更新,Scheduler判定任务的优先级,通知Reconciler何时进行更新;V15的虚拟dom树已无法满足这种更新方式,因此用fiber节点树来代替原来的虚拟dom树
    • 将任务切片化(拆分成一个个独立的小task),高优任务排在前面,通过 expirationTime(过期时间),来控制任务的执行,先执行高优先级任务,当多个任务同时需要处理时,可以中断低优先级任务,等空闲时继续执行未完成的任务,这样就不会影响页面的正常渲染

优先级:reactDOM.render() > onClick/onInput > 动画

    • fiber tree VS virtural tree: fiber tree 多了 expirationTime 过期时间,这样就可以把每一个节点当作一个独立的小task,如果当一个节点发生update,可以通过这个节点的过期时间进行任务调度,比如一个节点发生update,则会一致向上找到 Fiber Root,进行所有的调度更新,这也是react不好的地方,这是为什么要用到shouldComponentUpdate进行拦截优化,防止过度更新

virtual-dom(Vue Vs React)

  • vue:把每个单独更新的节点单独放到watcher中(每个watcher对应一个节点)
  • React: 节点更新可能是通过Fiber Root => props传递的值,它对应的是整个树,需要对整个树进行遍历

Fiber如何调度任务

  • 任务队列:是一个循环双向链表(每个节点有previous和next两个属性来分别指向前后两个节点,同时,最后一个节点的next指向第一个节点)

  • fiber在什么时候处理重要的任务?首先咱们先了解一下浏览器渲染的原理,浏览器是一帧一帧渲染出来的,理想的动画是60 FPS(重绘、重排会造成很难达到60FPS),1000ms/60FPS=16.7ms/帧

如果说理想状态下,每一帧为16.7ms, 渲染耗时10ms,那么到下一帧之间剩余6.7ms,岂不是浪费了?要不我说Fiber NB 呢,Fiber利用浏览器渲染机制来执行任务,如下图所示

requestAnimationFrame 模拟帧

// requestAnimationFrame 只有激活的时候才能使用,可以大大节省CPU开销
// 系统在调用回调前立马执行了一下performance.now()传给了回调当参数。
// performance.now()是浏览器内置的时钟,从页面加载开始计时,返回到当前的总时间,单位ms
// 这样我们就可以在执行回调的时候知道当前的执行时间了。
let count = 0;
requestAnimationFrame(function F(t) {
  //会不断打印执行回调的时间,如果刷新频率为60Hz,则相邻的t间隔时间 大约为1000/60 = 16.7ms
  console.log(t, "====");
  if (++count > 10) return false;
  requestAnimationFrame(F);
});

相邻时间相减 约等于 16.7ms,

MessageChannel

  • 虽然requestAnimationFrame可以模拟实现requestIdleCallback,但它占用了主线程的渲染,因此不能在这里面执行宏任务,而是通过它计算是剩余时间,同时Fiber采用了MessageChannel机制来执行任务,它是一个macTask不会占用主线程的渲染
  • 简单来说,MessageChannel创建了一个通信的管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,而一个端口只要绑定了onmessage回调方法,就可以接收从另一个端口传过来的数据
var channel = new MessageChannel();
var port1 = channel.port1;
var port2 = channel.port2;
port1.onmessage = function(event) {
  console.log("port1收到来自port2的数据:" + event.data);
  // port1收到来自port2的数据:My name is Hmm
}
port2.onmessage = function(event) {
  console.log("port2收到来自port1的数据:" + event.data);
  // port2收到来自port1的数据:What's your name?
}
port1.postMessage("What's your name?");
port2.postMessage("My name is Hmm");

React 17

使用 lanes 模型替代 expirationTime 模型

在 V6 版本中,以expirationTime的大小来衡量优先级,expirationTime越大,则优先级越高,但如果有一个高优先级异步IO任务(比如 Suspense,等待接口返回再执行后续操作)和低优先级的任务(比如 cup 任务),那么按照目前的模型,高优先级任务会始终阻塞低优先级任务,低优先级任务需要等待,直至高优先级IO任务执行完毕才会被执行,这样显然是不合理的,如何更好的处理高优先级和低优先级任务?

  • lanes:解决了从之前的每次只能执行一个任务,到现在可以同时执行多个任务的能力
    • lanes 指定一个连续的优先级区间,如果update的优先级在这个区间内,则将位于该区间内的任务生成对应的页面快照,
    • lanes 算法使用31位的二进制(代表31种可能性),其中每个bit被称为一个lane,代表优先级;某几个lane组成的二进制数被称为一个lanes,代表一批优先级,这样 react 可以分别给IO任务、低优先级的任务分配不同的lane,最后可以并发执行这几种类型的优先级

这样描述有点抽象,引用“知乎”文章中的内容,

其本质是【叠加算法】,多个任务可以相互叠加表示,用 js 来表示就是一个状态队列 { lanes: [1, 2, 3] }
表示 fiber 有三个不同的优先级,他们应该被批处理

react 作者 acdlite 觉得操作状态队列不够方便,进而采用了一种“位运算代替状态队列”的方式:{ lanes: 0b10010 }
新的 lane 算法中,lanes 是一个二进制数字,比如 10010 是由 1000000010 两个任务叠加而成的 🤔❓

what's this?这什么跟什么啊?,看看源码是怎么搞的~

源码:ReactFiberLane

  • lane and lanes:位数越低,优先级越高
// 值越大,优先级越高
export const SyncLanePriority: LanePriority = 15;
export const SyncBatchedLanePriority: LanePriority = 14;
const InputDiscreteHydrationLanePriority: LanePriority = 13;
export const InputDiscreteLanePriority: LanePriority = 12;
const InputContinuousHydrationLanePriority: LanePriority = 11;
export const InputContinuousLanePriority: LanePriority = 10;
const DefaultHydrationLanePriority: LanePriority = 9;
export const DefaultLanePriority: LanePriority = 8;
const TransitionHydrationPriority: LanePriority = 7;
export const TransitionPriority: LanePriority = 6;
const RetryLanePriority: LanePriority = 5
const SelectiveHydrationLanePriority: LanePriority = 4;
const IdleHydrationLanePriority: LanePriority = 3;
const IdleLanePriority: LanePriority = 2;
const OffscreenLanePriority: LanePriority = 1;
export const NoLanePriority: LanePriority = 0;
export const NoTimestamp = -1;
比如:
1.onClick => setState => update => 2.InputDiscreteLanePriority(12)
同步的 update => SyncLanePriority(15)

update会以priority为线索寻找没被占用的lane

如果当前fiber树已经存在更新且更新的lanes包含了该lane,则update需要寻找其他lane

比如,InputDiscreteLanePriority对应的lanes为InputDiscreteLanes
// 第4、5位为1
const InputDiscreteLanes: Lanes = 0b0000000000000000000000000011000;
第五位为1,0b0000000000000000000000000010000 表示五位的lane已经被占用,该update则尝试占有后一位
第四位为1,0b0000000000000000000000000001000
如果InputDiscreteLanes的两个lane都被占用,则该update的优先级会下降到InputContinuousLanePriority(10)并继续寻找空余的lane

由于lanes可以包含多个lane,可以很方便的区分IO操作(Suspense)与CPU操作。

当构建fiber树进入构建Suspense子树时,会将Suspense的lane插入本次更新选定的lanes中。

当构建离开Suspense子树时,会将Suspense lane从本次更新的lanes中移除

这个过程就好比如,商场每一层(不同优先级)都有一个WC(lanes),每个WC中有几个蹲坑(lane)。在商场顶层吃完饭想"嗯嗯"时,则先在该层找蹲位,如果没位置,就下到一层继续找,直到找到为止,如果找不到就在最低层等待~

代码分析

每次 update 时

  • React 会指定一个优先级
  • 根据优先级,去占一个位置
  • 如果有新的更新,占用剩余的位置
  • 最后把这个 lanes 区间的所有更新一起处理

1.指定优先级

// react-dom/src/events/ReactDOMEventListener.js
function dispatchUserBlockingUpdate(
  domEventName,
  eventSystemFlags,
  container,
  nativeEvent
) {
  // 获取上一次 lane 优先级,默认为 0
  const previousPriority = getCurrentUpdateLanePriority();
  try {
    // 设置 当前 lane 优先级
    setCurrentUpdateLanePriority(InputContinuousLanePriority);
    // Scheduler 调度任务
    runWithPriority(
      UserBlockingPriority,
      dispatchEvent.bind(
        null,
        domEventName,
        eventSystemFlags,
        container,
        nativeEvent,
      ),
    );
  } finally {
    setCurrentUpdateLanePriority(previousPriority);
  }
}

2.通过优先级寻找lane,并占据一个位置

// react-reconciler/src/ReactFiberLane.js
export function findUpdateLane(
  lanePriority: LanePriority,
  wipLanes: Lanes,
): Lane {
  switch (lanePriority) {
    case NoLanePriority:
      break;
    case SyncLanePriority:
      return SyncLane;
    case SyncBatchedLanePriority:
      return SyncBatchedLane;
    case InputDiscreteLanePriority: {
      // InputDiscreteLanes = 0b0000000000000000000000000011000
      // update wipLanes = 0b0000000000000000000000000001000
      // update wipLanes = 0b0000000000000000000000000011000
      // 两次更新发起一次调度 
      // wipLanes 已被占用的 lane
      // 如果有未被占用的 lane,则会取到优先级高的一个
      const lane = pickArbitraryLane(InputDiscreteLanes & ~wipLanes);
      // InputDiscreteLanesLanes的两个lane都被占据了,NoLane = 0
      if (lane === NoLane) {
        return findUpdateLane(InputContinuousLanePriority, wipLanes);
      }
      return lane;
    }
    case InputContinuousLanePriority: {
      const lane = pickArbitraryLane(InputContinuousLanes & ~wipLanes);
      // wipLanes 为此刻已被占用的 lane
      // InputContinuousLanes 的两个lane都被占据了
      if (lane === NoLane) {
        return findUpdateLane(DefaultLanePriority, wipLanes);
      }
      return lane;
    }
    case DefaultLanePriority: {
      let lane = pickArbitraryLane(DefaultLanes & ~wipLanes);
      // 如果默认 lane 已被占用
      if (lane === NoLane) {
        // 从 过度 TransitionLanes 区间,寻找一个lane
        lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
        if (lane === NoLane) {
          所有的过渡车道也被占用了(非常罕见)则选择一个默认车道,这将会中断当前正在进行的渲染
          lane = pickArbitraryLane(DefaultLanes);
        }
      }
      return lane;
    }
    ...
  );
}

Scheduler 调度器

调度任务的优先级:高优先级任务进入 Reconciler

react的优先级:

  • 生命周期方法:同步执行
  • 受控输入:同步执行,如 input
  • 事件:高优先级执行
  • 其他:低优先级执行,比如数据请求

调度逻辑描述

1.根据优先级区分同步任务和异步任务,同步任务应立即同步执行,最先渲染出来,异步任务走scheduler

2.计算得到 expirationTime expirationTime = currentTime(当前时间) + timeout(不同优先级的时间间隔不同,间隔越小,优先级越高)

3.对比startTime和currentTime,将任务氛围即时任务和延时任务

4.即时任务立即执行,但为了不阻塞页面交互,要放到 mac-task中执行

5.延时任务需要等待 currentTime >= expirationTime 时,才执行

6.即时任务执行完毕后,会判断当前时间点是否存在待执行的延时任务,如果存在,则执行

7.每一批任务的执行在不同的 mac-task 中,这样做的好处是不会阻塞页面的交互

代码分析

1.根据优先级区分同步任务和异步任务,同步任务应立即同步执行,最先渲染出来,异步任务走scheduler

// react-reconciler/src/ReactFiberWorkLoop.new.js
export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  ...
  // 获得当前FiberRoot
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    return null;
  }
  // 标记该Fiber的优先级执行的时间
  markRootUpdated(root, lane, eventTime);
  ...
  // 同步任务,立即更新,SyncLane为最高优先级
  if (lane === SyncLane) {
    if (
      // 处在unbatchedUpdate 并且还未 Renderer 阶段,则立即执行
      // executionContext 执行上下文,CommitContext 渲染上下文
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      // 注册 pending 状态,防止丢失交互数据
      schedulePendingInteractions(root, lane);
      // 主线程执行,ReactDom.render()
      performSyncWorkOnRoot(root);
    } else {
      // 异步调度和中断,每次更新时以及更新之前都会调用此函数,用来退出前一个任务
      ensureRootIsScheduled(root, eventTime);
      schedulePendingInteractions(root, lane);
      ...
    }
  } else {
    ...
    // Schedule other updates after in case the callback is sync.
    ensureRootIsScheduled(root, eventTime);
    schedulePendingInteractions(root, lane);
  }
  mostRecentlyUpdatedRoot = root;
}

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  // root.callbackNode 的存活周期是从ensureRootIsScheduled 开始 到 commitRootImpl截止
  const existingCallbackNode = root.callbackNode;
  ...
  // 检查是否存在现有任务
  if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority;
    /**
     * 如果优先级没有改变,可以复用现有任务,现有任务的优先级和下一个任务的优先级相同,比如input连续输入,优先级相同,可以复用之前的任务
     
     由于获取更新任务是从root开始,往下找到在这个优先级内所有updates,比如连续的setState,不会新建update,不需要重新重新发起一个调度,复用之前的任务
     */
    if (existingCallbackPriority === newCallbackPriority) {
      return;
    }
    // 优先级变化了,取消掉已存在任务,之后会重新发起一个调度
    cancelCallback(existingCallbackNode);
  }

  // 发起一个新的callback
  ...
  newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root),
  );
  root.callbackPriority = newCallbackPriority;
  // root.callbackNode的存活周期是ensureRootIsScheduled开始,到commitRootImpl截止
  root.callbackNode = newCallbackNode;
}

2.计算得到 expirationTime expirationTime = currentTime(当前时间) + timeout(不同优先级的时间间隔不同,间隔越小,优先级越高)

// scheduler/src/forks/SchedulerDOM.js
function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 获取当前时间
  var currentTime = getCurrentTime();// performance.now()
  
  // 计算 startTime
  var startTime;
  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

  var timeout;
  // 根据优先级增加不同的时间间隔
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }
  
  // expirationTime 越接近 currentTime,优先级越高
  var expirationTime = startTime + timeout;
  ...
}

3.对比startTime和currentTime,将任务氛围即时任务和延时任务

// scheduler/src/forks/SchedulerDOM.js
function unstable_scheduleCallback(priorityLevel, callback, options) {
  ...
  // 创建一个任务
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  
  if (startTime > currentTime) {
    // 延时任务
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
	// 当没有即时任务时,在间隔之后执行handleTimeout,把timerQueue的任务添加到taskQueue队列中,然后调用requestHostTimeout
    ...
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 即时任务
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    ...
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      // 调用即时任务
      requestHostCallback(flushWork);
    }
  }
  return newTask;
}

4.即时任务立即执行,但为了不阻塞页面交互,要放到 mac-task中执行

话说为啥要用宏任务而不用微任务?

要搞懂这个,咱们先从 js 线程说起

js是单线程的,所有任务需要排队,前一个任务结束,才会执行下一个任务,为了解决等待问题,js任务分为同步任务和异步任务

  • 所有的同步任务都是在主线程上执行(执行栈),异步任务不进入执行栈,而是进入任务队列
  • 同步顺序执行,只有执行栈里的同步任务都执行完了,浏览器才会读取任务队列中可执行的异步任务
  • macrotask(宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行),宏任务中可以创建微任务,但是在宏任务中创建的微任务不会影响当前宏任务的执行,当一个宏任务队列中的任务全部执行完后,会查看是否有微任务队列,如果有就会优先执行微任务队列中的所有任务,如果没有就查看是否有宏任务队列(常见的宏任务I/O,setTimeout,setInterval)
  • microtask(微任务),可以理解是在当前 task 执行结束后立即执行的任务,在当前task任务后,下一个task之前执行,所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染(常见微任务 promise、MessageChannel,process.nextTick)

总结:

  • 所有同步代码执行完毕后 -> -> 执行当前的微任务 -> 执行宏任务 -> DOM渲染 -> 下一个事件循环
  • 之所以React选择用MessageChannel来处理及时任务
    • 1.利用MessageChannel信道通信能力,更加方便触发执行,同时它是macTask,创建宏任务和执行宏任务不会占用主线程的渲染
    • 2.虽然微任务的执行不会占用主线程的渲染,但是在创建微任务时是要占用主线的,假如使用 Promise
    • 3.如果微任务对列不清空,会阻塞宏任务,这样会影响其他涉及到宏任务的工作,
new promise((resolve)=>{ 占用主线程执行 }).then( 微任务队列中执行 )
new promise((resolve)=>{ 占用主线程执行 }).then( 微任务队列中执行 )
new promise((resolve)=>{ 占用主线程执行 }).then( 微任务队列中执行 )
....

看明白了吧,这回影响后面主线程的渲染,下面咱们来分析代码

/**
 * 即时任务:
 *  第一次次调用 scheduleCallback
 *     将任务放在 taskQueue 中
 *     执行 port.postMessage,如果主线程还有任务,那就不会走到 performWorkUntilDeadline 逻辑
 * 第二次调用 scheduleCallback
 *     将任务放在 taskQueue 中
 *     执行 port.postMessage,如果主线程没有任务,执行
 */

// 创建宏任务:消息通道
const channel = new MessageChannel();
const port = channel.port2;

// 在MessageChannel执行真正的调度,可以保证任务与任务之间不是连续执行的,这样可以保证一次执行多个任务时不会阻塞
channel.port1.onmessage = performWorkUntilDeadline;

//  从 TaskQueue获取任务,判断TimerQueue中是否有到期任务,如果有就push 到 TaskQueue
function performWorkUntilDeadline = () => {
  ...
  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.
        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;
    }
}

// callback: flushWork
function requestHostCallback(callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    port.postMessage(null);
  }
}

function flushWork(hasTimeRemaining, initialTime) {
  ...
   return workLoop(hasTimeRemaining, initialTime);
}

function workLoop(hasTimeRemaining, initialTime) {
  // 检查 TimerQueue中是否有到期任务,如果有就push 到 TaskQueue
  advanceTimers(currentTime);
  // 获取到期任务
  currentTask = peek(taskQueue);
  const callback = currentTask.callback;
  // 执行任务
  const continuationCallback = callback(didUserCallbackTimeout);
}

5.延时任务需要等待 currentTime >= expirationTime 时,才执行。每次调度即时任务时,都会判断延时任务的执行时间是否到了,如果到了,则添加到及时任务中

// 延时任务 callback: handleTimeout
function requestHostTimeout(callback, ms) {
  taskTimeoutID = setTimeout(() => {
    callback(getCurrentTime());
  }, ms);
}

function handleTimeout(currentTime) {
  advanceTimers(currentTime);
}

function advanceTimers(currentTime) {
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      return;
    }
    timer = peek(timerQueue);
  }
}

Reconciler

  • 负责构建 fiber Tree -> dom diff -> patch -> renderer(commit阶段,不可中断,同步)
  • V16以上版本,为了方便控制(打断),采用链表的数据结构,每一个节点为一个Fiber,节点连接 -> Fiber Tree

获取更新的组件

  • ReactDOM.render 和 setState 引起的更新,都会从 Fiber Root 开始,从上至下遍历,找到变化的节点,构建完成会形成一个 Fiber Tree,与 原来的 Fiber Tree 进行 dom diff

双缓存结构

  • 如果之前(第一次渲染)没有Fiber Tree就逐级创建 Current Fiber Tree,如果存在就创建workInProgress Fiber Tree(待更新节点),workInProgress Tree 上的节点可以复用 Current Tree 上没有变化的节点数据
  • 当更新变化时,React内部存在两个 Fiber Tree:Current Fiber 与 workInProgress Fiber 通过 alternate 属性链接
  • 代码:react-reconciler/src/ReactFiber.new.js
  • find current Fiber Tree(之前的Fiber) -> workInProgress Fiber Tree(复用 currrent 未变化的节点数据)-> current 指向 workInProgress Fiber Root -> Fiber update

构建 FiberTree

render() {
  return (
    <div>
      I am <span>{name}</span>
    </div>
  )
}

Fiber Tree 构建从 上面的 scheduler调度执行主线程渲染(ReactDom.render)performSyncWorkOnRoot(root) -> renderRootSync(root, lanes) -> workLoopSync()(从优先级最高的 FiberRoot 开始递归)

function workLoopSync() {
  // workInProgress:当前正在处理的节点
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
  ...
  // 创建一个 Fiber,赋值给 workInProgress.child并返回workInProgress.child,
  // next = workInProgress
  next = beginWork(current, unitOfWork, subtreeRenderLanes);
  ...
  if (next === null) {
    // 如果没有next,则完成当前工作
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
  ReactCurrentOwner.current = null;
}

在beginWork函数中,只创建了 App、div、I am 3个Fiber,当没有子节点时,执行 completeUnitOfWork -> 执行后,如果存在兄弟Fiber节点,就从兄弟Fiber节点接着执行beginWork -> completeUnitOfWork,循环往复,这就是Fiber Tree 的构建过程

beginWork

1.判断 Fiber 节点是否可以复用 2.根据不同的Tag(标记不同的组件类型:纯组件、函数组件、类组件),生成不同的Fiber节点(调用reconcileChildren)

  • mount阶段:创建Fiber节点
  • update阶段:与 current Fiber对比,生成新的Fiber节点
    • 单节点 diff
    • 多节点 diff 3.在变更的Fiber节点上标记上lane,newFiber.flags = Placement | Update | Deletion | ... 4.创建Fiber节点并赋给workInprogress.child,返回workInProgress.child 5.进行下一次循环
// react-reconciler/src/ReactFiberBeginWork.new.js
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const updateLanes = workInProgress.lanes;
  // current tree 存在,说明不是初次构建
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged()
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      // 更新的优先级 和 current tree优先级是否一致,如果不一致,则触发
      didReceiveUpdate = false;
      ...
      // didReceiveUpdate 表示可以复用 current Fiber Tree
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    } else {
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        didReceiveUpdate = true;
      } else {
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  }

  workInProgress.lanes = NoLanes;

  switch (workInProgress.tag) {
    case IndeterminateComponent: 
    ...
    case LazyComponent: {
     ...
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
          
      // 1.调用renderWithHooks -> 注入 hooks -> 执行function
      // 2.判断节点是否可以复用,能复用则调用bailoutHooks
      // 3.设置flags
      // 4.调用 reconcileChildren,获取子Fibler节点
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      // 执行 render()、生命周期逻辑 -> reconcileChildren
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case HostRoot:
       // ReactDOM.render(<App/>) -> 调用reconcileChildren
      return updateHostRoot(current, workInProgress, renderLanes);
    ...
}

// 处理 子 Fiber 节点
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
   // 更新的节点,diff后更新Fiber节点
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}
-----------------------------------------------
// react-reconciler/src/ReactChildFiber.new.js
// reconcileChildFibers 会判断变更的类型,比如增删改类型,每一种类型变更,调用不同的方法,赋予flags一个值,在commit阶段,根据flags做dom渲染
function placeSingleChild(newFiber: Fiber): Fiber {
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.flags = Placement;
    }
    return newFiber;
  }

diff

  • React Diff 原则:
    • 同级比较
    • 节点变化直接删除,然后重建
    • 如果有key值,对比相同key相同的节点
// react-reconciler/src/ReactChildFiber.new.js
 function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    const isUnkeyedTopLevelFragment = ...;
    // Handle object types
    const isObject = typeof newChild === 'object' && newChild !== null;
    if (isObject) {
      // 根据不同的类型,处理不同的节点对比 
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        ...
    }

    if (typeof newChild === 'string' || typeof newChild === 'number') {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild,
          lanes,
        ),
      );
    }
    // 多节点数组
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }
    ...
  }

单节点diff

  • 判断存在对应节点,key相同、节点类型一致,可以复用
    • 存在,节点类型不一致,标记删除
    • 存在,key不同,标记删除
    • 不存在,创建节点
  // react-reconciler/src/ReactChildFiber.new.js
  function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    // 是否存在对应节点
    while (child !== null) {
	  // 比较key值
      if (child.key === key) {
        switch (child.tag) {
          ...
          default: {
            // 节点类型一致,可以复用
            if (child.elementType === element.type)) {
              deleteRemainingChildren(returnFiber, child.sibling);
              const existing = useFiber(child, element.props);
              existing.ref = coerceRef(returnFiber, child, element);
              existing.return = returnFiber;
              return existing;
            }
            break;
          }
        }
        // 节点类型不一致,比如 div -> span,标记为删除
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        // key 不同,标记删除
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }
	
    if (element.type === REACT_FRAGMENT_TYPE) {
      ...
    } else {
      // 不存在节点,创建
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
  }

多节点diff

  • 对比新旧children相同index对象的key是否相同
    • 相同:返回该对象,可以复用
    • 不相同:节点不能复用
  • 判断节点是否存在移动,存在则返回新位置
    • 如果新数组长度 < 老数组长度,老数组多出的部分删除
  • 新数组存在新增节点,则创建
  • 创建一个existingChildren代表所有剩余没匹配的节点,新数组根据key从existingChildren 中查找,如果有,则复用,没有则创建
  // react-reconciler/src/ReactChildFiber.new.js
  function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ): Fiber | null {
    let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      // 对比新旧children相同index对象的key是否相同,相同返回该对象,不同返回null
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      // key不相同,
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      
      // 判断节点是否移动
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }
    // 数组的节点都可以复用
    if (oldFiber === null) {
      // 创建
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
          continue;
        }
        // 添加sibling
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      }
      return resultingFirstChild;
    }

    // 创建一个existingChildren代表所有剩余没匹配的节点,新数组根据key从existingChildren 中查找,如果有,则复用,没有则创建
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    for (; newIdx < newChildren.length; newIdx++) {
      // 对比是否有可以复用的
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        // 返回新位置
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        previousNewFiber = newFiber;
      }
    }

    return resultingFirstChild;
  }

completeUnitOfWork

  • 向上递归completeWork
  • 创建DOM节点、更新DOM节点;DOM节点赋值给stateNode属性
  • 把子节点的 side Effect 加到 父节点的 sideEffect 上,在commit节点使用
  • 存在兄弟节点,将workInProgress指向兄弟节点,执行兄弟节点的beginWork -> Fiber Node
  • 不存在兄弟节点,返回父节点,继续执行父节点的completeWork

整个Fiber Tree的构建是2重循环

// react-reconciler/src/ReactFiberWorkLoop.new.js
function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    if ((completedWork.flags & Incomplete) === NoFlags) {
      setCurrentDebugFiberInDEV(completedWork);
      let next;
  	  ...
      // mount时,创建DOM节点,将后代DOM节点插入刚生成的DOM节点,DOM节点赋值给stateNode缓存
      // update时,由于DOM树已经存在,只需更新一些属性
      next = completeWork(current, completedWork, subtreeRenderLanes);
      resetCurrentDebugFiberInDEV();
      ...

      // 把子节点side Effect 加到父节点的 side Effect 上
      if (returnFiber !== null && (returnFiber.flags & Incomplete) === NoFlags) {
       
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = completedWork.firstEffect;
        }
        if (completedWork.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
          }
          returnFiber.lastEffect = completedWork.lastEffect;
        }

        const flags = completedWork.flags;
        if (flags > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            returnFiber.firstEffect = completedWork;
          }
          returnFiber.lastEffect = completedWork;
        }
      }
    } else {
      ...
    }
	
    // 存在兄弟节点,将workInProgress指向兄弟节点,并return,执行兄弟节点的beginWork -> Fiber Node
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    // 没有兄弟节点,返回父节点
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

-----------------------------------------
// react-reconciler/src/ReactFiberCompleteWork.new.js
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  ...
  // 创建DOM节点
  const instance = createInstance(
     type,
     newProps,
     rootContainerInstance,
     currentHostContext,
     workInProgress,
  );
  // 将后代DOM节点插入刚生成的DOM节点
  appendAllChildren(instance, workInProgress, false, false);
  workInProgress.stateNode = instance;
  ...
}

有点乱儿~

整体流程

上面整了一大堆,自己看都嫌恶心,感觉很杂并没有串起来,接下来会花一点时间画一下整体流程图,敬请期待

React17整体流程图

❤️ 加入我们

字节跳动 · 幸福里团队

Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者

期待您的加入,一起用技术改变生活!!!

招聘链接: job.toutiao.com/s/JHjRX8B