本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
大家好,我是小杜杜,在React v16
以上的版本引入了一个非常重要的概念,那就是fiber
,实际上fiber
是react
团队花费两年的时间重构的架构,在之前的文章中也提及到了fiber
,那么fiber
架构究竟是什么,为什么要使用fiber
在正式开始前,我们可以先看看以下几个问题:
- 什么是
fiber
,fiber
解决了什么问题? fiber
中,保存了哪些信息,这些信息的作用是什么?- 如何将
jsx
转化为fiber
链表,又是如何将链表链接起来的 fiber
是如何更新的?fiber
与stack
相比,主要解决了哪些方面的问题?- ...
先附上今天的学习图谱,方便我们更好的理解:
本文基于React v17.0.1源码
走进 Fiber
什么是Fiber
在一个庞大的项目中,如果有某个节点发生变化,就会给diff
带来巨大的压力,此时想要要找到真正变化的部分就会耗费大量的时间,也就是说此时,js
会占据主线程去做对比,导致无法正常的页面渲染,此时就会发生页面卡顿、页面响应变差、动画、手势等应用效果差
为了解决这一问题,react
团队花费两年时间,重写了react
的核心算法reconciliation
,在v16
中发布,为了区分reconciler
(调和器),将之前的reconciler
称为stack reconciler
,之后称作fiber reconciler
(简称:fiber
)
简而言之,fiber
就是v16
之后的虚拟DOM
(React
在遍历的节点的时候,并不是真正的DOM
,而是采用虚拟的DOM
)
v16之前,React是如何遍历节点的?
我们先看看下面这张图:
遍历的顺序为: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
实际上是一种核心算法,为了解决中断和树庞大的问题,那么接下来我们先来了解下fiber
虚拟DOM是如何转化成fiber的
先看看最常见的一段jsx
代码:
const Index = (props)=> {
return (
<div>
大家好,我是小杜杜
</div>
);
}
这段代码就是最普通的jsx
,经过babel
会编译成React.createElement
的形式
再来看看绑定的结构:
ReactDOM.render(
<App />,
document.getElementById('root')
);
之后会走一个beginWork
的方法,这个方法会通过tag
去判断这段代码的element
对象,再之后会调用reconcileChildFibers
函数,这个函数就是转化后的fiber
结构
element、fiber和DOM元素 的关系
element
对象就是我们的jsx
代码,上面保存了props
、key
、children
等信息DOM元素
就是最终呈现给用户展示的效果- 而
fiber
就是充当element
和DOM元素
的桥梁,简单的说,只要elemnet
发生改变,就会通过fiber
做一次调和,使对应的DOM
元素发生改变
其中有一个
tag
,这个tag
的类型就是判断element
对应那种的fiber
,如:
beginWork 的入参
在这里我将这三个入参单独说一下,因为这三个参数比较重要,我们要有一个基础的概念
- current:在视图层渲染的树
- workInProgress:这个参数尤为重要,它就是在整个内存中所构建的
Fiber
树,所有的更新都发生在workInProgress
中,所以这个树是最新状态的,之后它将替换给current
- renderLanes:跟优先级有关,优先级也是一块非常大的点,这里先不介绍
element 和 fiber 的对应表
在这里总结了一些比较常用的对照表,供大家参考:
fiber | element |
---|---|
FunctionComponent = 0 | 函数组件 |
ClassComponent = 1 | 类组件 |
IndeterminateComponent = 2 | 初始化的时候不知道是函数组件还是类组件 |
HostRoot = 3 | 根元素,通过reactDom.render()产生的根元素 |
HostPortal = 4 | ReactDOM.createPortal 产生的 Portal |
HostComponent = 5 | dom 元素(如<div> ) |
HostText = 6 | 文本节点 |
Fragment = 7 | <React.Fragment> ) |
Mode = 8 | <React.StrictMode> |
ContextConsumer = 9 | <Context.Consumer> |
ContextProvider = 10 | <Context.Provider> |
ForwardRef = 11 | React.ForwardRef |
Profiler = 12 | <Profiler> |
SuspenseComponent = 13 | <Suspense> |
MemoComponent = 14 | React.memo 返回的组件 |
SimpleMemoComponent = 15 | React.memo 没有制定比较的方法,所返回的组件 |
LazyComponent = 16 | <lazy /> |
fiber 保存了什么?
接下来我们看看fiber
中保存了什么,如:
源码部分在packages/react-reconciler/src/ReactFiber.old.js
中的FiberNode
在这里我们直接来看看对应的type
(位置在同目录下的ReactInternalTypes.js
)
然后简单的分为四个部分,分别是Instance
、Fiber
、Effect
、Priority
Instance
Instance:这个部分是用来存储一些对应element
元素的属性
export type Fiber = {
tag: WorkTag, // 组件的类型,判断函数式组件、类组件等(上述的tag)
key: null | string, // key
elementType: any, // 元素的类型
type: any, // 与fiber关联的功能或类,如<div>,指向对应的类或函数
stateNode: any, // 真实的DOM节点
...
}
Fiber
Fiber:这部分内容存储的是关于fiber
链表相关的内容和相关的props
、state
export type Fiber = {
...
return: Fiber | null, // 指向父节点的fiber
child: Fiber | null, // 指向第一个子节点的fiber
sibling: Fiber | null, // 指向下一个兄弟节点的fiber
index: number, // 索引,是父节点fiber下的子节点fiber中的下表
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject, // ref的指向,可能为null、函数或对象
pendingProps: any, // 本次渲染所需的props
memoizedProps: any, // 上次渲染所需的props
updateQueue: 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<Fiber> | 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<any> = (props)=> {
return (
<div>
大家好,我是小杜杜
<div>走进fiber的世界</div>
<p>收藏 === 学会</p>
</div>
);
}
那么按照之前讲的就会转化为:
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 constructors
return 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; // 指向rootFiber
uninitializedFiber.stateNode = root; // 指向fiberRoot
initializeUpdateQueue(uninitializedFiber);
return root;
}
顺便看下FiberRootNode
函数
beginWork
beginWork:这个函数正真走我们的jsx
代码,也就是上面讲解的链表之间如何连接的部分
那么我们再把上面的图,拿出来,看看遍历的流程:
首先,我们要知道
react
对fiber
结构的创建和更新都是深度优先遍历
- 首先会判断当前组件是类组件还是函数式组件,类组件
tag
为1,函数式为0 - 然后发现
div
标签,标记tag
为 5 - 发现
div
下包含三个部分,分别是,文本:大家好,我是小杜杜
、div标签
、p标签
- 首先遍历文本:
大家好,我是小杜杜
,下面无节点,标记tag
为 6 - 在遍历
div标签
,标记tag为 5
,此时下面有节点,所以对节点进行遍历,也就是文本走进fiber的世界
,标记tag
为 6 - 同理最后遍历
p标签
整个的流程就是这样,通过tag
标记属于哪种类型,然后通过return
、child
、sibling
这三个参数来判断节点的位置
更新(Update)阶段
接下来看看更新阶段,举个例子:
const Index:React.FC<any> = (props)=> {
const [count, setCount] = useState(0)
return (
<div>
<div>数字:{count}</div>
<Button onClick={() => setCount(v => v + 1)} >点击</Button>
</div>
);
}
当我们点击按钮后,会走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图,发生了怎样的变化:
可以看出来,fiber
明显比stack
要流畅很多,代表响应速度变快,宽度的变化也不会引发卡顿
对比Vue
我们经过上面的了解,已经知道React Fiber
实际上是无差别刷新,他是将整个变化的树作为更改,而Vue
是精确的将变化的节点进行替换,那是不是说Vue
要强于React
?
其实这个话题非常有争议,就我个人而言,Vue
的精确的替换也是具有代价的,至于两者孰强孰弱,作为一个小白也不好去多做评论,我们主要是学习思想,毕竟思想才是最重要的
React
自身也会提供一些优化的方法,如useMemo
、useCallback
等,我们一定要善用于这些API,帮助我们更好的去玩React
End
参考
其他好文
结语
React Fiber
可以说是React
的基石,很多方面都离不开它,学习fiber
是不可缺少的一部分
实际上,这篇文章笔者已经推翻过两三次,主要原因是fiber
实际上非常大,里面涉及的概念也十分琐碎,加上关于fiber
的文章也非常多,我不知道该如何更好的去呈现出来
其次,这篇文章算是个入门级的fiber
,fiber
比较难的概念都没有涉及到,阅读起来相对轻松一点,比较难的是优先级、调度、调和等模块
相比于更高级的模块,应该把架子搭起来,由浅入深,一点一点的慢慢啃,(如有不对的地方请在评论区留言指出~)
最后,感兴趣的可以关注下这个专栏,这个专栏会以进阶为目的,详细讲解React
相关的原理、源码、实战,有感兴趣的可以关注下,一起学习,一起进步~(别忘记点赞+收藏哦~)
玩转 React Hooks 小册
小册链接:《玩转 React Hooks》
知其然,知其所以然。React Hooks 带来的全新机制让人耳目一新,因为它拓展了 React 的开发思路,为 React 开发者提供了一种更方便、更简洁的选择。
在引入 Hooks 的概念后,函数组件既保留了原本的简洁,也具备了状态管理、生命周期管理等能力,在原来 Class 组件所具备的能力基础上,还解决了 Class 组件存在的一些代码冗余、逻辑难以复用等问题。因此,在如今的 React 中,Hooks 已经逐渐取代了 Class 的地位,成了主导。
而且,Hooks 相对于 Class 而言,更容易上手,其简洁性、逻辑复用性
等特性深受开发者喜爱,可谓是前端界的"流量明星"
,不止 React,Vue 3.0 、Preact、Solid.js 等框架也都选择加入 Hooks 的大家庭,前端的日常工作也在趋向于 Hooks 开发。
因此,掌握好 React Hooks 是非常有必要的一件事。本小册会通过基础篇、原码篇、实践篇 三大方向
探讨 Hooks,从原码的角度探寻 React 的奥秘。
除此之外,小册会以 React Hooks 为核心,同时穿插其他知识,如 TS、Jest、Fiber 等核心知识,并包含 React v18 的并发、数据撕裂等概念,最后结合 Hooks 写一个简易版 react-redux 和 Form 表单,通过其设计思想,助你在面试中脱颖而出。
小册整体设计如下思维导图
所示:
你会学到什么?
- 全面知悉 React 提供的 15 Hooks API 的使用和场景;
- 手写 30+ 自定义 Hooks 的实现,全面掌握设计思想;
- 了解 Hooks 源码,从根源上彻底解决现有的难点;
- 掌握函数式编程思想,用于工作,享受便利。
最后
感谢各位小伙伴的支持,如果在阅读过程中有什么问题欢迎大家加我微信,交个朋友,微信:domesyPro, 也可以关注笔者的公众号:杜杜的全栈之旅,一起来玩转 React Hooks 吧~