React15的渲染和diff会递归对比vDom树,找出有改动(增删改)的节点,然后同步的更新他们,如果页面的节点数量非常庞大,React会一直占用浏览器资源,导致用户操作得不到响应,二则会掉帧,用户能感知到明显的卡顿。这个时候的React就像一个反应迟钝的直男,忙起来就冷落了女朋友😅,这篇我们来看下时间管理大师fiber是如何帮助React平衡事业(浏览器任务)和爱情(触发响应)的。
Fiber是什么
fiber并不是计算机术语中的新名词,他的中文翻译叫做纤程
,与进程(Progress)、线程(Thread)、协程(Coroutine)同为执行过程。React Fiber
可以理解为:React
内部的一套更新机制。支持任务不同优先级,可中断与恢复,并且在恢复之后复用之前的保存状态。
其中每个任务
(work)更新单元为React Element
对应的Fiber节点。
React Fiber 与浏览器的核心交互流程
页面在渲染时是以帧为单位的,一般情况下设备的刷新频率时1s 60次,也就是每秒内绘制的帧数(fps)如果低于60,页面就会出现明显的卡顿,所以一帧绘制的时间不能超过16.7ms(这个时间很重要)。
相较于React15,React16中新增了Scheduler(调度器)。
首先React向浏览器申请调度,如果浏览器在一帧
内(16.7ms)还有空余时间,就会去判断是否有待执行的任务,如果不存在就继续渲染页面,如果存在需要执行的任务,执行完成后就会判断是否还有时间,一直到浏览器任务完成,我们可以说 Fiber 是一种数据结构(堆栈帧)
,也可以说是一种解决可中断的调用任务的一种解决方案,它的特性就是时间分片(time slicing)和暂停(supense)
。
更新工作从递归变成了可以中断的循环过程。每次循环都会调用shouldYield判断当前是否有剩余时间。
// react源码 packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
// 听调度器的,他说执行再执行
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
Scheduler(调度器)
我们需要一种机制,来告诉我们浏览器是否有时间。requestIdleCallback
是实现这一机制的关键api,他能让用户的操作快速的响应,不阻塞用户的交互,而神奇的是,它并没有减少计算,只是将碎片化的时间运用到了极致,其实部分浏览器已经实现了这个API,但是因为浏览器兼容
问题,React放弃使用,自己实现了功能更完备的requestIdleCallback polyfill
,这就是Scheduler。
Scheduler
是独立于React的库
Fiber的结构
// react源码 packages/react-reconciler/src/ReactInternalTypes
{
type: any, // 对于类组件,它指向构造函数;对于DOM元素,它指定HTML tag
key: null | string, // 唯一标识符
stateNode: any, // 保存对组件的类实例,DOM节点或与fiber节点关联的其他React元素类型的引用
child: Fiber | null, // 大儿子
sibling: Fiber | null, // 下一个兄弟
return: Fiber | null, // 父节点
tag: WorkTag, // 定义fiber操作的类型, 详见https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
nextEffect: Fiber | null, // 指向下一个节点的指针
updateQueue: mixed, // 用于状态更新,回调函数,DOM更新的队列
memoizedState: any, // 用于创建输出的fiber状态
pendingProps: any, // 已从React元素中的新数据更新,并且需要应用于子组件或DOM元素的props
memoizedProps: any, // 在前一次渲染期间用于创建输出的props
// ……
}
-
type & key
fiber 的 type 和 key 与 React 元素的作用相同。fiber 的 type 描述了它对应的组件,对于复合组件,type 是函数或类组件本身。对于原生标签(div,span等),type 是一个字符串。随着 type 的不同,在 reconciliation 期间使用 key 来确定 fiber 是否可以重新使用。
-
stateNode
stateNode 保存对组件的类实例,DOM节点或与 fiber 节点关联的其他 React 元素类型的引用。一般来说,可以认为这个属性用于保存与 fiber 相关的本地状态。
-
child & sibling & return
child 属性指向此节点的第一个子节点(大儿子)。 sibling 属性指向此节点的下一个兄弟节点(大儿子指向二儿子、二儿子指向三儿子)。 return 属性指向此节点的父节点,即当前节点处理完毕后,应该向谁提交自己的成果。如果fiber 具有多个子 fiber,则每个子 fiber 的 return fiber 是 parent 。
这里我非常好奇,为什么父级指针叫做return而不是parent,参照
卡颂
大佬的 《React技术揭秘》 中的解释 子Fiber节点及其兄弟节点完成工作后会返回其父级节点,所以用return指代父级节点 。
Fiber的遍历流程
可以类比王朝的嫡长子继承制,制度是这样的,如果皇帝驾崩,会传位给大儿子,如果皇帝无后,那传位给兄弟,如果没有直系血统,那就要传位皇叔了,直到龙脉枯竭,王朝结束。
执行传序为 A1 B1 C1 C2 B2 C3 C4
我们根据这个规则手写下这套深度优先遍历的算发
// 看源码还能学算法 🐶
// 遍历函数
const performUnitOfWork = (Fiber) => {
// 太子第一顺位
if (Fiber.child) {
return Fiber.child
}
while (Fiber) {
// 二皇子第二顺位
if (Fiber.sibling) {
return Fiber.sibling
}
// 本支断绝,回去挑选皇叔
Fiber = Fiber.return
}
}
const workloop = (nextUnitOfWork) => {
// 如果有待执行的执行单元则执行,返回下一个执行单元
while (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
if (!nextUnitOfWork) {
console.log('reconciliation阶段结束')
}
}
workloop(rootFiber)
// rootFiber在后面手写的时候构造
本地调试源码
下面我们在实际项目中打印一个Fiber节点观察一下
我们还是使用create-react-app来创建一个项目,调试源码需要在项目里做一些配置
- npm run eject(暴露出webpack配置)
- 在src文件中添加react源码文件夹
- 修改webpack中react的引用路径
这里就不详细展开配置的细节 如何在本地调试react源码,或者使用我配置好的项目,直接clone调试.
这样我们就可以在本地调试源码了
// packages/react-dom/src/ReactDOMLegacy.js
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: Container,
forceHydrate: boolean,
callback: ?Function,
) {
if (__DEV__) {
topLevelUpdateWarnings(container);
warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
}
// TODO: Without `any` type, Flow says "Property cannot be accessed on any
// member of intersection type." Whyyyyyy.
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// Initial mount 初次渲染
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Initial mount should not be batched.
// 初次渲染是非批量更新,可以保证更新效率与用户体验
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Update
updateContainer(children, fiberRoot, parentComponent, callback);
}
console.log('fiber-----current', fiberRoot.current); // 在这里我们打印一下fiber树
return getPublicRootInstance(fiberRoot);
}
// 我们jsx是这样写的,div下有一个h1标签和一个a标签
const jsx = (
<div className="content">
我是
<a href="www.baidu.com">明非</a>
</div>
)
看下打印结果
我们画一个 fiber的链表图图谱
我们来手写一个fiber
可以继续使用我们react源码解析(一) 手写render函数的代码
这里我们只关注fiber的type,props,stateNode,child,sibling,return属性。// fiber js对象。
// 我们在render函数中,构造一个fiber节点
let wipRoot = null;
function render(vnode, container) {
wipRoot = {
type: "div",
props: {
children: {...vnode},
},
stateNode: container,
};
nextUnitOfWOrk = wipRoot;
}
// 原生标签节点,接收work正在执行的fiber节点
function updateHostComponent(workInProgress) {
const {type, props} = workInProgress;
// 插入节点前要考虑是否已经存在节点
if (!workInProgress.stateNode) {
workInProgress.stateNode = createNode(workInProgress);
}
reconcileChildren(workInProgress, workInProgress.props.children);
console.log("workInProgress", workInProgress);
}
// 协调子节点
function reconcileChildren(workInProgress, children) {
if (typeof children === "string" || typeof children === "number") {
return;
}
// React16之后为 一个元素包裹,或者数组
const newChildren = Array.isArray(children) ? children : [children];
// 上一个fiber节点
let previousNewFiber = null;
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
// 构造新的fiber节点
let newFiber = {
type: child.type,
props: {...child.props},
stateNode: null,
child: null,
sibling: null,
return: workInProgress,
};
if (i === 0) {
// 第一个子fiber,首先大儿子
workInProgress.child = newFiber;
} else {
// 没有给兄弟
previousNewFiber.sibling = newFiber;
}
// 记录上一个fiber
previousNewFiber = newFiber;
}
}
function performUnitOfWork(workInProgress) {
// step1 执行任务
// todo
const {type} = workInProgress;
if (typeof type === "string") {
// 原生标签节点
updateHostComponent(workInProgress);
}
// step2 并且返回下一个执行任务
if (workInProgress.child) {
return workInProgress.child;
}
let nextFiber = workInProgress;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.return;
}
}
function workLoop(IdleDeadline) {
while (nextUnitOfWOrk && IdleDeadline.timeRemaining() > 1) {
// 执行任务, 并且返回下一个执行任务
nextUnitOfWOrk = performUnitOfWork(nextUnitOfWOrk);
}
// 提交
if (!nextUnitOfWOrk && wipRoot) {
commitRoot();
}
}
// 实现时间切片
requestIdleCallback(workLoop);
// 调度结束,渲染
function commitRoot() {
commitWorker(wipRoot.child);
wipRoot = null;
}
// 提交work执行结果
function commitWorker(workInProgress) {
// 提交自己
if (!workInProgress) {
return;
}
let parentNodeFiber = workInProgress.return;
let parentNode = parentNodeFiber.stateNode;
if (workInProgress.stateNode) {
parentNode.appendChild(workInProgress.stateNode);
}
// 提交子节点
commitWorker(workInProgress.child);
// 提交兄弟节点
commitWorker(workInProgress.sibling);
}
上效果
代码已上传到git代码地址
思路,创建根节点,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点,使用深度优先遍历构建出一棵dom树,
总结
fiber架构
是React的基石,通过时间切片实现了将同步的更新
变成了可中断的异步更新
,这次我们了解了fiber和浏览器交互流程,实现了一个简单的fiber树,遍历流程根据深度优先遍历
。但是这里没有覆盖到优先级机制
,如何断点续传
,和如何收集任务结果
。之后的文章想分析下这些机制,看react是如何比对(diff)更新的。
参考链接
珠玉在前,只是想表达出自己的理解,推荐卡颂 《react技术揭秘》