前言
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 是由 10000 和 00010 两个任务叠加而成的 🤔❓
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;
...
}
有点乱儿~
整体流程
上面整了一大堆,自己看都嫌恶心,感觉很杂并没有串起来,接下来会花一点时间画一下整体流程图,敬请期待
❤️ 加入我们
字节跳动 · 幸福里团队
Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者
期待您的加入,一起用技术改变生活!!!