-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
React16源码之React Fiber架构 #7
Comments
清晰 透彻 |
非常清晰,感謝你的分享~ |
写的真好 |
感謝分享! |
写的很棒!请教一个问题,fiber结构中的updatequeue的effectlist是不是闭环?
但是在 |
Open
Good stuff! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 ⭐️
欢迎关注 我的掘金、我的知乎
The text was updated successfully, but these errors were encountered: