在React v16
以上的版本引入了一个非常重要的概念,那就是fiber
,实际上fiber
是react
团队花费两年的时间重构的架构,在之前的文章中也提及到了fiber
,那么fiber
架构究竟是什么,为什么要使用fiber
在正式开始前,我们可以先看看以下几个问题:
fiber
,fiber
解决了什么问题?fiber
中,保存了哪些信息,这些信息的作用是什么?jsx
转化为fiber
链表,又是如何将链表链接起来的fiber
是如何更新的?fiber
与stack
相比,主要解决了哪些方面的问题?先附上今天的学习图谱,方便我们更好的理解:
在一个庞大的项目中,如果有某个节点发生变化,就会给diff
带来巨大的压力,此时想要要找到真正变化的部分就会耗费大量的时间,也就是说此时,js
会占据主线程去做对比,导致无法正常的页面渲染,此时就会发生页面卡顿、页面响应变差、动画、手势等应用效果差
为了解决这一问题,react
团队花费两年时间,重写了react
的核心算法reconciliation
,在v16
中发布,为了区分reconciler
(调和器),将之前的reconciler
称为stack reconciler
,之后称作fiber reconciler
(简称:fiber
)
简而言之,fiber
就是v16
之后的虚拟DOM
(React
在遍历的节点的时候,并不是真正的DOM
,而是采用虚拟的DOM
)
我们先看看下面这张图:
遍历的顺序为:A => B => D => E => C => F => G
在v16
之前,react
采用的是深度优先遍历去遍历节点,转化为代码为:
const root = {key: 'A',children: [{key: 'B',children: [{key: 'D',},{key: 'E',},],},{key: 'C',children: [{key: 'F',},{key: 'G',},],},],};const walk = dom => dom.children.forEach(child => walk(dom));walk(root);
可以看出这种遍历采取的递归
遍历,如果这颗树非常的庞大,那么对应的栈也会越来越深,如果其中发生中断,那么整颗树都不能恢复。
也就是说,在传统的方法中,在寻找节点的过程中,花费了1s,那么这1s就是浏览器无法响应的,同时树越庞大,卡顿的效果也就越明显
所以在v16
之前的版本,无法解决中断和树庞大的问题
在上面的介绍中,我们知道fiber
实际上是一种核心算法,为了解决中断和树庞大的问题,那么接下来我们先来了解下fiber
先看看最常见的一段jsx
代码:
const Index = (props)=> {return (大家好,我是小杜杜);
}
这段代码就是最普通的jsx
,经过babel
会编译成React.createElement
的形式
再来看看绑定的结构:
ReactDOM.render( ,document.getElementById('root')
);
之后会走一个beginWork
的方法,这个方法会通过tag
去判断这段代码的element
对象,再之后会调用reconcileChildFibers
函数,这个函数就是转化后的fiber
结构
1.element
对象就是我们的jsx
代码,上面保存了props
、key
、children
等信息
2.DOM元素
就是最终呈现给用户展示的效果
3.而fiber
就是充当element
和DOM元素
的桥梁,简单的说,只要elemnet
发生改变,就会通过fiber
做一次调和,使对应的DOM
元素发生改变
其中有一个
tag
,这个tag
的类型就是判断element
对应那种的fiber
,如:
在这里我将这三个入参单独说一下,因为这三个参数比较重要,我们要有一个基础的概念
Fiber
树,所有的更新都发生在workInProgress
中,所以这个树是最新状态的,之后它将替换给current
在这里总结了一些比较常用的对照表,供大家参考:
fiber | element |
---|---|
FunctionComponent = 0 |
函数组件 |
ClassComponent = 1 |
类组件 |
IndeterminateComponent = 2 | 初始化的时候不知道是函数组件还是类组件 |
HostRoot = 3 | 根元素,通过reactDom.render()产生的根元素 |
HostPortal = 4 | ReactDOM.createPortal 产生的 Portal |
HostComponent = 5 | dom 元素(如
|
HostText = 6 | 文本节点 |
Fragment = 7 | ) |
Mode = 8 |
|
ContextConsumer = 9 |
|
ContextProvider = 10 |
|
ForwardRef = 11 | React.ForwardRef |
Profiler = 12 |
|
SuspenseComponent = 13 |
|
MemoComponent = 14 | React.memo 返回的组件 |
SimpleMemoComponent = 15 | React.memo 没有制定比较的方法,所返回的组件 |
LazyComponent = 16 |
|
接下来我们看看fiber
中保存了什么,如:
源码部分在packages/react-reconciler/src/ReactFiber.old.js
中的FiberNode
在这里我们直接来看看对应的type
(位置在同目录下的ReactInternalTypes.js
)
然后简单的分为四个部分,分别是Instance
、Fiber
、Effect
、Priority
Instance:这个部分是用来存储一些对应element
元素的属性
export type Fiber = {tag: WorkTag,// 组件的类型,判断函数式组件、类组件等(上述的tag)key: null | string, // keyelementType: any, // 元素的类型type: any, // 与fiber关联的功能或类,如,指向对应的类或函数stateNode: any, // 真实的DOM节点...
}
Fiber
Fiber:这部分内容存储的是关于fiber
链表相关的内容和相关的props
、state
export type Fiber = {...return: Fiber | null, // 指向父节点的fiberchild: Fiber | null, // 指向第一个子节点的fibersibling: Fiber | null, // 指向下一个兄弟节点的fiberindex: number, // 索引,是父节点fiber下的子节点fiber中的下表ref:| null| (((handle: mixed) => void) & {_stringRef: ?string, ...})| RefObject,// ref的指向,可能为null、函数或对象pendingProps: any,// 本次渲染所需的propsmemoizedProps: any,// 上次渲染所需的propsupdateQueue: mixed,// 类组件的更新队列(setState),用于状态更新、DOM更新memoizedState: any, // 类组件保存上次渲染后的state,函数组件保存的hooks信息dependencies: Dependencies | null,// contexts、events(事件源) 等依赖mode: TypeOfMode, // 类型为number,用于描述fiber的模式 ...
}
Effect
Effect:副作用相关的内容
export type Fiber = {... flags: Flags, // 用于记录fiber的状态(删除、新增、替换等) subtreeFlags: Flags, // 当前子节点的副作用状态 deletions: Array | null, // 删除的子节点的fiber nextEffect: Fiber | null, // 指向下一个副作用的fiber firstEffect: Fiber | null, // 指向第一个副作用的fiber lastEffect: Fiber | null, // 指向最后一个副作用的fiber...
}
Priority
Priority: 优先级相关的内容
export type Fiber = {...lanes: Lanes, // 优先级,用于调度childLanes: Lanes,alternate: Fiber | null,actualDuration?: number,actualStartTime?: number,selfBaseDuration?: number,treeBaseDuration?: number,...
}
链表之间如何连接的?
在 Fiber
中我们看到有return
、child
、sibling
这三个参数,分别指向父级、子级、兄弟,也就是说每个element
通过这三个属性进行连接
举个栗子:
const Index:React.FC = (props)=> {return (大家好,我是小杜杜走进fiber的世界收藏 === 学会
);
}
那么按照之前讲的就会转化为:
Fiber 执行阶段
初始化(mount)阶段
在上文已经说过,react
首次执行(初始化阶段)会以ReactDOM.render
为入口,然后开始执行,由于调用的函数实在过多,这里我就简化一些,方便我们更好理解
createFiber
createFiber:这个函数会创建rootFiber
,也就是react
应用的根,会调用FiberNode
函数来进行对应的构建工作
位置:packages/react-reconciler/src/ReactFiberRoot.old.js
const createFiber = function( tag: WorkTag,pendingProps: mixed,key: null | string,mode: TypeOfMode, ): Fiber {// $FlowFixMe: the shapes are exact here but Flow doesn't like constructorsreturn new FiberNode(tag, pendingProps, key, mode);
};
而FiberNode就是上述讲的构造函数
createFiberRoot
createFiberRoot:它会调用FiberRootNode
构造函数,创建fiberRoot
,并且指向真正的根节点(root
)
位置:packages/react-reconciler/src/ReactFiberRoot.old.js
export function createFiberRoot(containerInfo: any,tag: RootTag,hydrate: boolean,hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);if (enableSuspenseCallback) {root.hydrationCallbacks = hydrationCallbacks;}const uninitializedFiber = createHostRootFiber(tag);root.current = uninitializedFiber; // 指向rootFiberuninitializedFiber.stateNode = root; // 指向fiberRootinitializeUpdateQueue(uninitializedFiber);return root;
}
顺便看下FiberRootNode
函数
beginWork
beginWork:这个函数正真走我们的jsx
代码,也就是上面讲解的链表之间如何连接的部分
首先,我们要知道react
对fiber
结构的创建和更新都是深度优先遍历
1.首先会判断当前组件是类组件还是函数式组件,类组件tag
为1,函数式为0
2.然后发现div
标签,标记tag
为 5
3.发现div
下包含三个部分,分别是,文本:大家好,我是小杜杜
、div标签
、p标签
4.首先遍历文本:大家好,我是小杜杜
,下面无节点,标记tag
为 6
5.在遍历div标签
,标记tag 为 5
,此时下面有节点,所以对节点进行遍历,也就是文本走进fiber的世界
,标记tag
为 6
6.同理最后遍历p标签
整个的流程就是这样,通过tag
标记属于哪种类型,然后通过return
、child
、sibling
这三个参数来判断节点的位置
更新(Update)阶段
接下来看看更新阶段,举个例子:
const Index:React.FC = (props)=> {const [count, setCount] = useState(0)return (数字:{count});
}
当我们点击按钮后,会走createWorkInProgress
方法,这个方法会将创建一个新的workInProgress fiber
,然后还是会深度优先遍历,对发生改变的fiber
打上不同的flags
副作用标签,然后通过副作用(Effect)
中的nextEffect
、firstEffect
、lastEffect
等字短行程一个Effect List
的链表
再来看对应的源码(位置在packages/react-reconciler/src/ReactFiber.old.js
):
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {let workInProgress = current.alternate; //以alternate作为基础if (workInProgress === null) { //这里判断是初始化还是更新阶段workInProgress = createFiber(current.tag,pendingProps,current.key,current.mode,);workInProgress.elementType = current.elementType;workInProgress.type = current.type;workInProgress.stateNode = current.stateNode;...// 这部分是重置所有的副作用workInProgress.flags = current.flags & StaticMask;workInProgress.childLanes = current.childLanes;workInProgress.lanes = current.lanes;workInProgress.child = current.child;workInProgress.memoizedProps = current.memoizedProps;workInProgress.memoizedState = current.memoizedState;workInProgress.updateQueue = current.updateQueue; // 对依赖的克隆 const currentDependencies = current.dependencies; workInProgress.dependencies =currentDependencies === null? null: {lanes: currentDependencies.lanes,firstContext: currentDependencies.firstContext,}; workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref;
... return workInProgress;
}
总的来说在更新阶段,更新阶段会将current
的alternate
作为基础,然后复制一部分,进行节点的更新,返回一个新的workInProgress
这里需要注意一点,current fiber
和workInProgress fiber
中的alternate
是相互指向的,当新的 workInProgress fiber
创建完成后,fiberRoot
的current
字段会从current fiber
中的rootFiber
变为workInProgress fiber
中的rootFiber
fiber 带来后的变化
最后,我们再来看看更改前后的fiber图,发生了怎样的变化:
Stack Example
Fiber Example
可以看出来,fiber
明显比stack
要流畅很多,代表响应速度变快,宽度的变化也不会引发卡顿
对比Vue
我们经过上面的了解,已经知道React Fiber
实际上是无差别刷新,他是将整个变化的树作为更改,而Vue
是精确的将变化的节点进行替换,那是不是说Vue
要强于React
?
其实这个话题非常有争议,就我个人而言,Vue
的精确的替换也是具有代价的,至于两者孰强孰弱,作为一个小白也不好去多做评论,我们主要是学习思想,毕竟思想才是最重要的
React
自身也会提供一些优化的方法,如useMemo
、useCallback
等,我们一定要善用于这些API,帮助我们更好的去玩React
结语
React Fiber
可以说是React
的基石,很多方面都离不开它,学习fiber
是不可缺少的一部分
实际上,这篇文章笔者已经推翻过两三次,主要原因是fiber
实际上非常大,里面涉及的概念也十分琐碎,加上关于fiber
的文章也非常多,我不知道该如何更好的去呈现出来
其次,这篇文章算是个入门级的fiber
,fiber
比较难的概念都没有涉及到,阅读起来相对轻松一点,比较难的是优先级、调度、调和等模块
相比于更高级的模块,应该把架子搭起来,由浅入深,一点一点的慢慢啃,(如有不对的地方请在评论区留言指出~)
最后
最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。
有需要的小伙伴,可以点击下方卡片领取,无偿分享