一 引沿
Fiber 架构是React16中引入的新概念,目的就是解决大型 React 应用卡顿,React在遍历更新每一个节点的时候都不是用的真实DOM,都是采用虚拟DOM,所以可以理解成fiber就是React的虚拟DOM,更新Fiber的过程叫做调和,每一个fiber都可以作为一个执行单元来处理,所以每一个 fiber 可以根据自身的过期时间expirationTime,来判断是否还有空间时间执行更新,如果没有时间更新,就要把主动权交给浏览器去渲染,做一些动画,重排( reflow ),重绘 repaints 之类的事情,这样就能给用户感觉不是很卡。
二 什么是调和
调和是一种算法,就是React对比新老虚拟DOM的过程,以决定需要更新哪一部分。
三 什么是Filber
Fiber的目的是为了让React充分利用调度,以便做到如下几点:
- 暂停工作,稍后再回来
- 优先考虑不同类型的工作
- 重用以前完成的工作
- 如果不再需要,则中止工作
为了实现上面的要求,我们需要把任务拆分成一个个可执行的单元,这些可执行的单元就叫做一个Fiber,一个Fiber就代表一个可执行的单元。
一个Fiber就是一个普通的JS对象,包含一些组件的相关信息。
1`function FiberNode(){
2 this.tag = tag; // fiber 标签 证明是什么类型fiber。
3 this.key = key; // key调和子节点时候用到。
4 this.type = null; // dom元素是对应的元素类型,比如div,组件指向组件对应的类或者函数。
5 this.stateNode = null; // 指向对应的真实dom元素,类组件指向组件实例,可以被ref获取。
6
7 this.return = null; // 指向父级fiber
8 this.child = null; // 指向子级fiber
9 this.sibling = null; // 指向兄弟fiber
10 this.index = 0; // 索引
11
12 this.ref = null; // ref指向,ref函数,或者ref对象。
13
14 this.pendingProps = pendingProps;// 在一次更新中,代表element创建
15 this.memoizedProps = null; // 记录上一次更新完毕后的props
16 this.updateQueue = null; // 类组件存放setState更新队列,函数组件存放
17 this.memoizedState = null; // 类组件保存state信息,函数组件保存hooks信息,dom元素为null
18 this.dependencies = null; // context或是时间的依赖项
19
20 this.mode = mode; //描述fiber树的模式,比如 ConcurrentMode 模式
21
22 this.effectTag = NoEffect; // effect标签,用于收集effectList
23 this.nextEffect = null; // 指向下一个effect
24
25 this.firstEffect = null; // 第一个effect
26 this.lastEffect = null; // 最后一个effect
27
28 this.expirationTime = NoWork; // 通过不同过期时间,判断任务是否过期, 在v17版本用lane表示。
29
30 this.alternate = null; //双缓存树,指向缓存的fiber。更新阶段,两颗树互相交替。
31}`
32
33
type 就是react的元素类型
1`export const FunctionComponent = 0; // 对应函数组件
2export const ClassComponent = 1; // 对应的类组件
3export const IndeterminateComponent = 2; // 初始化的时候不知道是函数组件还是类组件
4export const HostRoot = 3; // Root Fiber 可以理解为跟元素 , 通过reactDom.render()产生的根元素
5export const HostPortal = 4; // 对应 ReactDOM.createPortal 产生的 Portal
6export const HostComponent = 5; // dom 元素 比如 <div>
7export const HostText = 6; // 文本节点
8export const Fragment = 7; // 对应 <React.Fragment>
9export const Mode = 8; // 对应 <React.StrictMode>
10export const ContextConsumer = 9; // 对应 <Context.Consumer>
11export const ContextProvider = 10; // 对应 <Context.Provider>
12export const ForwardRef = 11; // 对应 React.ForwardRef
13export const Profiler = 12; // 对应 <Profiler/ >
14export const SuspenseComponent = 13; // 对应 <Suspense>
15export const MemoComponent = 14; // 对应 React.memo 返回的组件`
比如元素结构如下:
1`export default class Parent extends React.Component{
2 render(){
3 return <div>
4 <h1>hello,world</h1>
5 <Child />
6 </div>
7 }
8}
9
10function Child() {
11 return <p>child</p>
12}`
对应的Filber结构如下:
有了上面的概念后我们就自己实现一个Fiber的更新机制
四 实现调和的过程
我们通过渲染一段jsx来说明React的调和过程,也就是我们要手写实现ReactDOM.render()
1`const jsx = (
2 <div className="border">
3 <h1>hello</h1>
4 <a href="https://www.reactjs.org/">React</a>
5 </div>
6)
7
8ReactDOM.render(
9 jsx,
10 document.getElementById('root')
11);`
1. 创建FiberRoot
react-dom.js
1`function createFiberRoot(element, container){
2 return {
3 type: container.nodeName.toLocaleLowerCase(),
4 props: { children: element },
5 stateNode: container
6 }
7}
8
9function render(element, container) {
10 const FibreRoot = createFiberRoot(element, container)
11 scheduleUpdateOnFiber(FibreRoot)
12}
13export default { render }`
参考React实战视频讲解:进入学习
2. render阶段
调和的核心是render和commit,本文不讲调度过程,我们会简单的用requestIdleCallback代替React的调度过程。
ReactFiberWorkloop.js
1`let wipRoot = null // work in progress
2let nextUnitOfwork = null // 下一个fiber节点
3
4export function scheduleUpdateOnFiber(fiber) {
5 wipRoot = fiber
6 nextUnitOfwork = fiber
7}
8
9function workLoop(IdleDeadline) {
10 while(nextUnitOfwork && IdleDeadline.timeRemaining() > 0) {
11 nextUnitOfwork = performUnitOfWork(nextUnitOfwork)
12 }
13}
14
15function performUnitOfWork() {}
16
17requestIdleCallback(workLoop)`
18
19
每一个 fiber 可以看作一个执行的单元,在调和过程中,每一个发生更新的 fiber 都会作为一次 workInProgress 。那么 workLoop 就是执行每一个单元的调度器,如果渲染没有被中断,那么 workLoop 会遍历一遍 fiber 树
performUnitOfWork 包括两个阶段:
- 是向下调和的过程,就是由 fiberRoot 按照 child 指针逐层向下调和,期间会执行函数组件,实例类组件,diff 调和子节点
- 是向上归并的过程,如果有兄弟节点,会返回 sibling兄弟,没有返回 return 父级,一直返回到 fiebrRoot
这么一上一下,构成了整个 fiber 树的调和。
1`import { updateHostComponent } from './ReactFiberReconciler'
2function performUnitOfWork(wip) {
3 // 1. 更新wip
4 const { type } = wip
5 if (isStr(type)) {
6 // type是string,更新普通元素节点
7 updateHostComponent(wip)
8 } else if (isFn(type)) {
9 // ...
10 }
11
12 // 2. 返回下一个要更新的任务 深度优先遍历
13 if (wip.child) {
14 return wip.child
15 }
16 let next = wip
17 while(next) {
18 if (next.sibling) {
19 return next.sibling
20 }
21 next = next.return
22 }
23 return null
24}`
25
26
根据type类型区分是FunctionComponent/ClassComponent/HostComponent/… 本文中只处理HostComponent类型,其他类型的处理可以看文末的完整代码链接。
ReactFiberReconciler.js
1`import { createFiber } from './createFiber'
2
3export function updateHostComponent(wip) {
4 if (!wip.stateNode) {
5 wip.stateNode = document.createElement(wip.type);
6 updateNode(wip.stateNode, wip.props);
7 }
8 // 调和子节点
9 reconcileChildren(wip, wip.props.children);
10}
11
12function reconcileChildren(returnFiber, children) {
13 if (isStr(children)) {
14 return
15 }
16
17 const newChildren = isArray(children) ? children : [children];
18 let previousNewFiber = null
19 for(let i = 0; i < newChildren.length; i++) {
20 const newChild = newChildren[i];
21 const newFiber = createFiber(newChild, returnFiber)
22
23 if (previousNewFiber === null) {
24 returnFiber.child = newFiber
25 } else {
26 previousNewFiber.sibling = newFiber
27 }
28 previousNewFiber = newFiber
29 }
30}
31
32function updateNode(node, nextVal) {
33 Object.keys(nextVal).forEach((k) => {
34 if (k === "children") {
35 if (isStringOrNumber(nextVal[k])) {
36 node.textContent = nextVal[k];
37 }
38 } else {
39 node[k] = nextVal[k];
40 }
41 });
42}`
43
44
createFiber.js
1`export function createFiber(vnode, returnFiber) {
2 const newFiber = {
3 type: vnode.type, // 标记节点类型
4 key: vnode.key, // 标记节点在当前层级下的唯一性
5 props: vnode.props, // 属性
6 stateNode: null, // 如果组件是原生标签则是dom节点,如果是类组件则是类实例
7 child: null, // 第一个子节点
8 return: returnFiber,// 父节点
9 sibling: null, // 下一个兄弟节点
10 };
11
12 return newFiber;
13}`
至此已经完成了render阶段,下面是commit阶段,commit阶段就是依据Fiber结构操作DOM
1`function workLoop(IdleDeadline) {
2 while(nextUnitOfwork && IdleDeadline.timeRemaining() > 0) {
3 nextUnitOfwork = performUnitOfWork(nextUnitOfwork)
4 }
5
6 // commit
7 if (!nextUnitOfwork && wipRoot) {
8 commitRoot();
9 }
10}
11
12function commitRoot() {
13 commitWorker(wipRoot.child)
14 wipRoot = null;
15}
16
17function commitWorker(wip) {
18 if (!wip) {
19 return
20 }
21 // 1. 提交自己
22 const { stateNode } = wip
23 let parentNode = wip.return.stateNode
24 if (stateNode) {
25 parentNode.appendChild(stateNode);
26 }
27
28 // 2. 提交子节点
29 commitWorker(wip.child);
30
31 // 3. 提交兄弟节点
32 commitWorker(wip.sibling);
33}`
34
35
五 总结
- Fiber结构,Fiber的生成过程。
- 调和过程,以及 render 和 commit 两大阶段。