Open HuJiaoHJ opened 6 years ago
清晰 透彻
非常清晰,感謝你的分享~
写的真好
感謝分享!
写的很棒!请教一个问题,fiber结构中的updatequeue的effectlist是不是闭环?
下面的appendUpdateToQueue
函数没有形成闭环
function appendUpdateToQueue<State>(
queue: UpdateQueue<State>,
update: Update<State>,
) {
// Append the update to the end of the list.
if (queue.lastUpdate === null) {
// Queue is empty
queue.firstUpdate = queue.lastUpdate = update;
} else {
queue.lastUpdate.next = update;
queue.lastUpdate = update;
}
}
但是在commitHookEffectList
方法中又有 const firstEffect = lastEffect.next;
Good stuff!
React16源码之React Fiber架构
React源码分析内容非常多,本文专注在以下两个问题:
在开始源码分析之前,首先先简单介绍一下React的一些基础概念
基础概念
React定位是一个构建用户界面的JavaScript类库,使用JavaScript开发UI组件,支持多种方式渲染组件,输出用户界面。
React常见的三种应用类型:
这三种应用分别对应三种不同的渲染方式:
下面,以 React Web应用 为例,介绍下React三个主要组成部分:
在开始 Reconciliation 模块之前,先简单介绍各个模块:
React基础模块
从上面的源码可以看到,React基础模块只包括了基础的API和组件相关的定义。如:createRef、Component等。
其中可以重点关注的两点:
1、React.creatElement
在平时的开发中,我们使用的JSX语法,所以我们并没有直接接触到 React.creatElement 方法
大家都知道,JSX语法会被babel编译成调用 React.creatElement 方法,如下:
而 React.creatElement 最终返回的是 React Element,数据结构如下:
可以在页面中把
<App/>
打印出来,如下:2、React.component
组件是我们开发使用最多的,我们可以简单的看下源码:
从Component的定义上可以看到,我们常用的 setState 方法是调用了 updater.enqueueSetState,以 react-dom 为例,此 updater 对象会调用该组件构造函数时(这块会在后面的生命周期函数调用中讲到),赋值为classComponentUpdater,源码如下:
可以知道,组件中调用 setState 其实是调用的 classComponentUpdater.enqueueSetState 方法,这里就是开始 setState 的入口
至此,就简单的介绍了React基础模块,下面开始介绍渲染模块:react-dom
渲染模块:react-dom
这里我们可以关注下 render 方法,所有 react web应用入口都会调用 ReactDOM.render(),本文也会从 ReactDOM.render() 开始进行源码的分析
在进行源码分析之前,先介绍下本文的核心:Reconciliation模块
Reconciliation模块
Reconciliation模块又叫协调模块,而我们题目上说的
React Fiber
则是在这个模块中使用一种调度算法React Fiber调度算法又叫 Fiber Reconciler,是 React 16 启用的一种新的调度算法,是对核心调度算法(Stack Reconciler)的重构
Stack Reconciler
React 16版本之前使用的 Stack Reconciler 调度算法,它通过递归的形式遍历 Virtual DOM,存在难以中断和恢复的问题,如果react更新任务运行时间过长,就会阻塞布局、动画等的运行,可能导致掉帧。它的调用栈如下:
Fiber Reconciler
允许渲染过程分段完成,而不必须一次性完成,中间可以返回至主进程控制执行其他任务,它有如下新特性:
它的调用栈如下:
关于React新老调度算法的对比,大家可以看看:https://zhuanlan.zhihu.com/p/37095662
关于React Fiber概念的再详细的介绍,大家可以看看:http://www.ayqy.net/blog/dive-into-react-fiber/
以上,就对React的基本概念进行了介绍,接下来开始源码分析~
源码分析
React Fiber架构引入了新的数据结构:Fiber节点
Fiber
Fiber节点数据结构如下:
Fiber树结构图(链表结构)如下:
源码函数调用流程
我们看张图:
React组件渲染分为两个阶段:reconciler、render。从图上可以看到:
在上面的基础概念介绍中有提到,react-dom模块负责react web应用的渲染工作,那么Reconciliation模块(协调模块)具体做了什么工作呢?
Reconciliation模块的工作可以分为两部分:
1、reconciliation
简单来说就是找到需要更新的工作,通过 Diff Fiber Tree 找出要做的更新工作,这是一个js计算过程,计算结果可以被缓存,计算过程可以被打断,也可以恢复执行
所以,上面介绍 Fiber Reconciler 调度算法时,有提到新算法具有可拆分、可中断任务的新特性,就是因为这部分的工作是一个纯js计算过程,所以是可以被缓存、被打断和恢复的
2、commit
提交更新并调用对应渲染模块(react-dom)进行渲染,为了防止页面抖动,该过程是同步且不能被打断
下面我们来看看这两个阶段具体的函数调用流程
reconciliation阶段
我们以 ReactDOM.render() 方法为入口,来看看reconciliation阶段的函数调用流程:
从图中可以看到,我把此阶段分为三部分,分别以红线划分。简单的概括下三部分的工作:
1、第一部分从 ReactDOM.render() 方法开始,把接收的React Element转换为Fiber节点,并为其设置优先级,记录update等。这部分主要是一些数据方面的准备工作。
2、第二部分主要是三个函数:scheduleWork、requestWork、performWork,即安排工作、申请工作、正式工作三部曲。React 16 新增的异步调用的功能则在这部分实现。
3、第三部分是一个大循环,遍历所有的Fiber节点,通过Diff算法计算所有更新工作,产出 EffectList 给到commit阶段使用。这部分的核心是 beginWork 函数。
第一部分
第一部分较为简单,这里就不详细介绍了,小伙伴们可自行阅读源码~
第二部分:任务协调
三部曲:scheduleWork、requestWork、performWork(安排工作、申请工作、正式工作)
在三部曲中的 requestWork函数中,会判断当前任务是同步还是异步(暂时React的异步调用功能还在开发中,未开放使用,本文后续内容是以同步任务为例),然后通过不同的方式调用任务。同步任务直接调用performWork函数立即执行,而异步任务则会在后面的某一时刻被执行,那么异步任务是怎么被调度的呢?
异步任务调度有两种方式,主要是通过该任务的优先级进行判断,主要有两种:
1、animation(动画):则会调用 requestAnimationFrame API 告诉浏览器,在下一次重绘之前调用该任务来更新动画
2、其他异步任务:则会调用 requestIdleCallback API 告诉浏览器,在浏览器空闲时期依次调用任务,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互等关键的事件产生影响
以上两个API都是原生API,想深入了解的可以看看:requestAnimationFrame、requestIdleCallback
而原生requestIdleCallback存在兼容性问题,所以React本身开发了 ReactScheduler模块 来实现这个功能
后续会以同步任务为例,所以我们开始介绍第三部分的核心函数:beginWork
第三部分:beginWork
从上面的函数调用流程图可以看到,beginWork在大循环中被调用,返回当前节点的子节点。
首先,先介绍一下React Fiber架构的双缓冲技术:
从上图可以看到有两颗 Fiber Tree:current、workInProgress,它们之间是通过每个Fiber节点上的alternate属性联系在一起,可以查看源码ReactFiber.js中的 createWorkInProgress 方法,如下:
以上代码为简化之后的,可以发现,current与workInProgress互相持有引用。而从上图可以发现,所有更新都是在workInProgress上进行操作,等更新完毕之后,再把current指针指向workInProgress,从而丢弃旧的Fiber Tree
从beginWork源码来看,主要分为两部分,一部分是对Context的处理,一部分是根据fiber对象的tag类型,调用对应的update方法。在这里我们重点关注第二部分。而在第二部分中,我们以 ClassComponent类型 为例,讲讲 updateClassComponent函数 中做了什么呢?
主要有两部分:生命周期函数的调用及Diff算法
生命周期函数调用
流程图如下:
current为null,意味着当前的update是组件第一次渲染
1、调用 constructClassInstance 构造组件实例,主要是调用
constructor
构造函数,并注入classComponentUpdater(这块就是文章一开始介绍React Component时提到的updater注入)2、mountClassInstance 则是调用
getDerivedStateFromProps
生命周期函数(v16) 及UNSAFE_componentWillMount
生命周期函数current不为null,调用 updateClassInstance 方法
1、如果新老props不一致,则会调用
UNSAFE_componentWillReceiveProps
生命周期函数2、然后调用
shouldComponentUpdate
生命周期函数,获得shouldUpdate值,若未定义此生命周期函数,默认为true(是否重新渲染),如果shouldUpdate为true,则会调用UNSAFE_componentWillUpdate
生命周期函数最后调用 finishClassComponent 方法,那么 finishClassComponent函数 中做了什么呢?流程图如下:
如果 shouldUpdate 为false,表示不需要更新,直接返回
如果 shouldUpdate 为true,调用实例的
render
方法,返回新子节点如果是首次渲染,调用 mountChildFibers 创建子节点的Fiber实例
否则,调用 reconcileChildFibers 对新老子节点进行Diff
执行到了这,updateClassComponent函数主要是执行了组件的生命周期函数,下面讲讲需要对新老子节点进行Diff时使用的Diff算法
Diff算法
reconcileChildFibers函数 中,源码如下:
reconcileChildFibers函数中主要是根据newChild类型,调用不同的Diff算法:
1、单个元素,调用reconcileSingleElement
2、单个Portal元素,调用reconcileSinglePortal
3、string或者number,调用reconcileSingleTextNode
4、array(React 16 新特性),调用reconcileChildrenArray
前三种情况,在新子节点上添加 effectTag:Placement,标记为更新操作,而这些操作的标记,将用于commit阶段。下面以单个元素为例,讲讲具体的Diff算法
reconcileSingleElement函数源码如下:
具体过程在代码的注释中写的比较清楚,在这就不详细展开。不过我们可以看看 deleteChild(删除子节点)中,具体做了什么,源码如下:
可以看到,deleteChild 删除子节点并不是真的删除这个对象,而是通过 firstEffect、lastEffect、nextEffect 属性来维护一个 EffectList(链表结构),通过 effectTag 标记当前删除操作,这些信息都会在 commit 阶段使用到
以上,就是beginWork函数的整个过程,可以知道遍历完Fiber树之后,通过Diff算法,可以产出 EffectList,给commit阶段使用
commit阶段
函数调用流程图如下:
commit阶段做的事情是拿到reconciliation阶段产出的EffectList,即所有更新工作,提交这些更新工作并调用渲染模块(react-dom)渲染UI。
effectTag
在前面也提到,commit阶段会通过 effectTag标记 识别操作类型,所以我们先来看看 effectTag 有哪些类型:
可以看到:
1、effectTag类型是使用二进制位表示,可以多个叠加
2、通过位运算匹配effectTag类型
从上面的流程图,可以看到commit阶段有比较重要的三个函数:
1、commitBeforeMutationLifecycles
此函数主要是保存当前DOM的一个快照,执行
getSnapshotBeforeUpdate
生命周期函数2、commitAllHostEffects
提交所有更新并渲染,源码如下:
从源码可以看到,此函数主要是遍历EffectList,根据effectTag,调用对应commit方法,进而调用react-dom提供的操作DOM的方法,渲染UI,操作DOM的方法有:
注意,在调用删除操作的commit方法时,会执行
componentWillUnmount
生命周期函数在这个方法中,基本完成了将更新提交并渲染UI的工作
3、commitAllLifeCycles
此函数主要是根据fiber节点类型,执行相应的处理,以 ClassComponent 为例,完成UI渲染之后,会执行后续的生命周期函数:
1、判断是否首次渲染,是则执行
componentDidMount
生命周期函数2、否则,执行
componentDidUpdate
生命周期函数以上就是commit阶段的全过程
至此,我们源码等的全过程也完成了,我们再总结一下整个函数调用流程:
总结
最后,我们回到一开始的那两个问题:
现在,是不是觉得整个过程都很清晰了呢~~~
附上,生命周期函数汇总表:
写在最后
以上就是我对React16源码的分享,希望能对有需要的小伙伴有帮助~~~
喜欢我的文章的小伙伴可以点star ⭐️
欢迎关注 我的掘金、我的知乎