这是我参与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
就是更新后的。
- 调用