这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战
最近在学习react的Fiber架构相关内容,源码学习实在是一件很费劲的事情,正好看到 React技术揭秘,大神讲的还是挺清楚的,听了一遍下来,总算有点门路了,赶紧记录下来,好记性不如烂笔头~
一遍当然远远不够的,之后有时间还需要多刷几遍,再跟着demo自己多试试。相信书读百遍,其义自现~
React理念:快速响应
两大制约:
- CPU的瓶颈:当遇到大计算量的操作或者设备性能不足使页面掉帧,导致卡顿。
- IO的瓶颈:发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应。
解决方案:
- 时间切片,长任务分拆到每一帧。在浏览器每一帧的时间中,预留一些时间(5ms)给JS线程,
React利用这部分时间更新组件,当预留的时间不够用时,React将线程控制权交还给浏览器使其有时间渲染UI,React则等待下一帧时间到来继续被中断的工作。 - Suspense特性,同步加载代码和数据,在接收到数据的过程中,React迭代地渲染需要数据的组件,直到渲染完所有内容为止。
新老React架构
- React15老架构是同步更新,可以分为两层:
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
Reconciler和Renderer是交替工作的,整个过程是同步的,且Reconciler通过递归子组件判断更新,层级很深时递归会占用线程很多时间,引起页面卡顿。
- React16新架构是异步可中断的,可以分为三层:
- Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
- Reconciler(协调器)—— 负责找出变化的组件,内部采用了
Fiber的架构 - Renderer(渲染器)—— 负责将变化的组件渲染到页面上
Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。
Scheduler和Reconciler工作都在内存中进行,不会更新页面上的DOM,可能被以下情况反复中断: 1. 有其他更高优任务需要先更新; 2. 当前帧没有剩余时间。
Fiber结构
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// ------作为静态数据结构的属性------
// 每个`Fiber节点`对应一个`React element`,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息
this.tag = tag; // Fiber对应组件的类型 Function/Class/Host...
this.key = key; // key属性
this.elementType = null; // 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.type = null; // 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.stateNode = null; // Fiber对应的真实DOM节点
// ------作为架构属性------
// 用于连接其他Fiber节点形成Fiber树
this.return = null; // 指向父级Fiber节点
this.child = null; // 指向子Fiber节点
this.sibling = null; // 指向右边第一个兄弟Fiber节点
this.index = 0;
this.ref = null;
// ------作为动态的工作单元的属性------
// 保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null; // 保存update阶段处理的props
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null; // effectList中第一个Fiber节点
this.lastEffect = null; // effectList中最后一个Fiber节点
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
Fiber工作原理
React使用“双缓存”来完成Fiber树的构建与替换——对应着DOM树的创建与更新。
在内存中构建并直接替换的技术叫做双缓存
当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。
current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。
整个应用的根节点只有一个,那就是fiberRootNode。
我们可以多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。
fiberRootNode的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber树。
Diff发生在workInProgress fiber的创建,可以复用current Fiber树对应的节点数据,也可以新建。
深入理解JSX
JSX在编译时会被Babel编译为React.createElement方法;React.createElement最终会调用ReactElement方法返回一个包含组件数据的对象,该对象有个参数$$typeof: REACT_ELEMENT_TYPE标记了该对象是个React Element;- 我们常使用
ClassComponent与FunctionComponent构建组件,并作为第一个参数传给React.createElement; - 在组件
mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点,在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记; Fiber节点中包含了更多信息:- 组件在更新中的
优先级 - 组件的
state - 组件被打上的用于Renderer的
标记
- 组件在更新中的
Fiber树的创建过程
render阶段
从rootFiber开始向下深度优先遍历。“递”阶段为遍历到的每个Fiber节点调用beginWork方法。该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。
“归”阶段会调用completeWork处理Fiber节点。
“递”和“归”阶段会交错执行直到“归”到rootFiber。
render阶段的工作是在内存中进行,当工作结束后会通知Renderer需要执行的DOM操作。要执行DOM操作的具体类型就保存在fiber.effectTag中。
递调用beginWork
主要工作:创建子Fiber节点,并标记effectTag
function beginWork(
current: Fiber | null, // 当前组件对应的`Fiber节点`在上一次更新时的`Fiber节点`,即`workInProgress.alternate`, 也就是当前渲染页面的节点
workInProgress: Fiber, // 当前组件对应的`Fiber节点`,内存中的节点
renderLanes: Lanes, // 优先级相关
): Fiber | null {
if (current !== null) { // 通过`current === null ?`来区分组件是处于`mount`还是`update`
// updatea阶段,满足如下情况时`didReceiveUpdate === false`(即可以直接复用前一次更新的`子Fiber`,不需要新建`子Fiber`)
// 1. `oldProps === newProps && workInProgress.type === current.type`,即`props`与`fiber.type`不变
// 2. `!includesSomeLane(renderLanes, updateLanes)`,即当前`Fiber节点`优先级不够
} else {
didReceiveUpdate = false;
}
// 根据 fiber.tag 不同,创建不同的子Fiber节点,最终会进入reconcileChildren方法。
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
}
function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 对于`mount`的组件,他会创建新的`子Fiber节点`
} else {
// 对于`update`的组件,他会将当前组件与该组件在上次更新时对应的`Fiber节点`比较(也就是俗称的`Diff`算法),将比较的结果生成新`Fiber节点`
}
}
归调用completeWork
update情况下,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:
onClick、onChange等回调函数的注册- 处理
style prop - 处理
DANGEROUSLY_SET_INNER_HTML prop - 处理
children prop
mount情况下,主要逻辑包括三个:
- 为
Fiber节点生成对应的DOM节点 - 将子孙
DOM节点插入刚生成的DOM节点中 - 与
update逻辑中类似的处理props的过程
注意:
update时,updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue。mount时只会在rootFiber存在Placement effectTag,通过appendAllChildren方法插入整颗DOM树。- 每个执行完
completeWork且存在effectTag的Fiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。commit阶段只需要遍历effectList就能执行所有effect了。 effect副作用包括:插入DOM节点(Placement)、更新DOM节点(Update)、删除DOM节点(Deletion)、以及useEffect或useLayoutEffect的相关操作。
commit阶段
commitRoot方法是commit阶段工作的起点。fiberRootNode会作为传参。
commit阶段可以分为三个子阶段,三个阶段都会遍历一遍`effectList:
- before mutation阶段(执行
DOM操作前)- 处理
DOM节点渲染/删除后的autoFocus、blur逻辑 - 调用
getSnapshotBeforeUpdate生命周期钩子 - 调度
useEffect
- 处理
- mutation阶段(执行
DOM操作) 执行的是commitMutationEffects- 根据
ContentReset effectTag重置文字节点 - 更新
ref - 根据
effectTag分别处理,其中effectTag包括(Placement|Update|Deletion|Hydrating) - 对于FunctionComponent,
mutation阶段会执行useLayoutEffect的销毁函数。 - 对于HostComponent,
mutation阶段会将updateQueue(render阶段生成)对应的内容渲染在页面上。
- 根据
- layout阶段(执行
DOM操作后)- 调用
生命周期钩子和hook相关操作 - 赋值 ref
- 切换
fiberRootNode指向的current Fiber树在mutation阶段结束后,layout阶段开始前 componentWillUnmount会在mutation阶段执行。此时current Fiber树还指向前一次更新的Fiber树,在生命周期钩子内获取的DOM还是更新前的。componentDidMount和componentDidUpdate会在layout阶段执行。此时current Fiber树已经指向更新后的Fiber树,在生命周期钩子内获取的DOM就是更新后的。
- 调用