1、React架构
React15架构可以分为两层:
- Reconciler(协调器)——负责找出有变化的组件
- Renderer(渲染器)——负责将变化的组件渲染到页面上
Reconciler(协调器)
在React中可以通过
this.setState
、this.forceUpdate
、ReactDOM.render
等API触发更新。每当有更新发生时,
Reconciler
会做如下工作: - 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM - 将虚拟DOM和上次更新时的虚拟DOM对比 - 通过对比找出本次更新中变化的虚拟DOM - 通知Renderer
将变化的虚拟DOM渲染到页面上Renderer(渲染器)
由于React支持跨平台,所以不同平台有不同的
Renderer
。ReactDOM
则是负责在浏览器环境渲染的Renderer
。在每次更新发生时,Renderer接到Reconciler通知,将变化的组件渲染在当前宿主环境。
在Reconciler中,
mount
的组件会调用mountComponent
,update
的组件会调用updateComponent
。这两个方法都会递归更新子组件。React16架构可以分为三层:
- Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
Scheduler(调度器)
以浏览器是否有剩余时间作为任务中断的标准,当浏览器有剩余时间时通知我们。
React实现了功能更完备的
requestIdleCallback
polyfill,也就是Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。Reconciler(协调器)
在React15中Reconciler是递归处理虚拟DOM的。
React16的更新工作从递归变成了可以中断的循环过程。每次循环都会调用
shouldYield
判断当前是否有剩余时间。/** @noinline */function workLoopConcurrent() { // Perform work until Scheduler asks us to yield while (workInProgress !== null && !shouldYield()) { workInProgress = performUnitOfWork(workInProgress); }}
那么React16是如何解决中断更新时DOM渲染不完全的问题呢?
在React16中,Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记,
整个Scheduler与Reconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。
Renderer(渲染器)
Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。
2、基础概念
Work
在
Reconciler
过程中出现的各种必须执行计算的活动,比如说更新state
、更新props
、更新refs
。Fiber
Fiber
包含三层含义: 1. 作为架构来说,之前React15的Reconciler
采用递归的方式执行,数据保存在递归调用栈中,所以被称为Stack Reconciler
。React16的Reconciler
基于Fiber节点
实现,被称为Fiber Reconciler
。 2. 作为静态的数据结构来说,每个Fiber节点
对应一个React element
,保存了该组件的类型(函数组件/类组件/原生组件…)、对应的DOM节点等信息。 3. 作为动态的工作单元来说,每个Fiber节点
保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新…)。因此
Fibers
可以理解为是一个包含 React 元素上下文信息的数据节点,以及由 child
、sibling
、return
等指针域组成的链表的结构。function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode,) { // 作为React Element静态数据结构的属性 //标识 Fiber 类型的标签 ,Fiber对应组件的类型(Function/Class/Host) this.tag = tag; //根据 key 字段判断该 fiber 对象是否可以复用 this.key = key; // 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹 this.elementType = null; // 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName this.type = null; // Fiber对应的真实DOM节点 this.stateNode = null; // 用于连接其他Fiber节点形成Fiber树 //父Fiber节点 this.return = null; //子Fiber节点 this.child = null; //右边第一个兄弟 this.sibling = null; this.index = 0; this.ref = null;//作为动态的工作单元 保存了本次更新相关的信息 //在开始执行时设置 props 值,和 memoizedProps 一起使用, 若 pendingProps 与 memoizedProps 相等, 则可以复用上一个 fiber 相关的props this.pendingProps = pendingProps; //在结束时设置的 props 值 this.memoizedProps = null; this.updateQueue = null; //current tree的state 也就是当前的 state this.memoizedState = null; this.dependencies = null; this.mode = mode; // 保存本次更新会造成的DOM操作 Effects this.flags = NoFlags; this.nextEffect = null; this.firstEffect = null; this.lastEffect = null; this.subtreeFlags = NoFlags;//17.02未使用 this.deletions = null;//17.02未使用// 调度优先级相关 // 用于标识一个 work 优先级顺序(React16.8版本为expirationTime,值越大优先级越高,17更改为lane) this.lanes = NoLanes; this.childLanes = NoLanes;//指向其对应的 workInProgress fiber this.alternate = null;...}
从 React 元素创建一个 fiber 对象
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);};
workTag
export const FunctionComponent = 0;export const ClassComponent = 1;export const IndeterminateComponent = 2; // Before we know whether it is function or classexport const HostRoot = 3; // Root of a host tree. Could be nested inside another node.export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.export const HostComponent = 5;export const HostText = 6;export const Fragment = 7;export const Mode = 8;export const ContextConsumer = 9;export const ContextProvider = 10;export const ForwardRef = 11;export const Profiler = 12;export const SuspenseComponent = 13;export const MemoComponent = 14;export const SimpleMemoComponent = 15;export const LazyComponent = 16;export const IncompleteClassComponent = 17;export const DehydratedFragment = 18;export const SuspenseListComponent = 19;export const FundamentalComponent = 20;export const ScopeComponent = 21;export const Block = 22;export const OffscreenComponent = 23;export const LegacyHiddenComponent = 24;
Fiber 对象的 tag 属性值,称作 workTag,用于标识一个 React 元素的类型。
EffectTag
不能在
render 阶段
完成的一些work 称为副作用,比如说对于节点的增、删、改操作Render 阶段和 Commit 阶段
这是 React 团队作者 Dan Abramov 画的一张生命周期阶段图。他把 React 的生命周期主要分为两个阶段:
render 阶段
和 commit 阶段
。其中 commit 阶
段又可以细分为 pre-commit 阶段
和commit 阶段
,如下图所示:!(React lifecycle methods)[./flow.jpg]
在
render阶段
,React可以根据当前可用的时间片处理一个或多个Fiber节点
。得益于
Fiber对象
中存储的元素上下文信息和指针域构成的链表结构,能够将执行到一半的工作保存在内存的链表中。当React停止并完成保存的工作后,让出时间片去处理一些优先级更高的事情。
之后在重新获取到可用的时间片后,它能够根据之前保存在内存的上下文信息通过快速遍历的方式找到停止的
Fiber 节点
并继续工作。因为在这个阶段没有被提交到
真实的 DOM 上
,用户不会感知到任何可见的更改。这就是
Fiber
通过调度能够实现暂停、中止以及重新开始等增量渲染的能力。相反,在
commit 阶段
,work 执行总是同步的,这是因为在此阶段执行的工作将导致用户可见的更改。这就是为什么在 commit 阶段, React 需要一次性提交并完成这些工作的原因。Current 树和 WorkInProgress 树
首次渲染之后,React 会生成一个对应于 UI 渲染的
Fiber 树
,称之为 current 树
React 在调用
生命周期函数
时就是通过判断是否存在current
来区分何时执行 componentDidMount
和 componentDidUpdate
。当 React 遍历
current Fiber树
时,它会为每一个存在的 Fiber 节点
创建了一个替代节点
,这些节点构成一个workInProgress Fiber树
。后续所有发生 work 的地方都是在 workInProgress Fiber树
中执行,如果该树还未创建,则会创建一个 current Fiber树
的副本,作为workInProgress Fiber树
。当 workInProgress Fiber树
被提交后将会在commit 阶段
的某一子阶段被替换成为 current Fiber树
。Fiber树的构建与替换过程
Fiber节点
可以保存对应的DOM节点
,也就是说 Fiber节点
构成的Fiber树
就对应DOM树
React使用“双缓存”来完成
Fiber树
的构建与替换——对应着DOM树
的创建与更新。(在内存中构建并直接替换的技术叫做双缓存)双缓存Fiber树
在React中最多会同时存在两棵Fiber树。 -
current Fiber树
: 当前屏幕上显示内容对应的Fiber树被称为current Fiber树 - workInProgress Fiber树
:正在内存中构建的Fiber树被称为workInProgress Fiber树current Fiber树
中的Fiber节点
被称为current Fiber
,workInProgress Fiber树
中的Fiber节点
被称为workInProgress Fiber
,他们通过alternate
属性连接。currentFiber.alternate === workInProgressFiber;workInProgressFiber.alternate === currentFiber;
React应用的根节点通过使
current指针
在不同Fiber树
的rootFiber
间切换来完成current Fiber树
指向的切换。即当
workInProgress Fiber树
构建完成交给Renderer
渲染在页面上后,应用根节点的current指针
指向workInProgress Fiber树
,此时workInProgress Fiber树
就变为current Fiber树
每次状态更新都会产生新的
workInProgress Fiber树
,通过current
与workInProgress
的替换,完成DOM更新。mount时
function App() { const [num, add] = useState(0); return ( <p onClick={() => add(num + 1)}>{num}</p> )}ReactDOM.render(<App/>, document.getElementById('root'));
1.首次执行
ReactDOM.render
会创建fiberRootNode
(源码中叫fiberRoot)和rootFiber
。其中fiberRootNode
是整个应用的根节点,rootFiber
是所在组件树的根节点。之所以要区分
fiberRootNode
与rootFiber
,是因为在应用中我们可以多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber
。但是整个应用的根节点只有一个,那就是fiberRootNode
。fiberRootNode
的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber
树。fiberRootNode.current = rootFiber;
由于是首屏渲染,页面中还没有挂载任何DOM,所以
fiberRootNode.current
指向的rootFiber
没有任何 子Fiber节点
(即current Fiber树
为空)。2.接下来进入
render阶段
,根据组件返回的JSX在内存中依次创建Fiber节点
并连接在一起构建Fiber树
,被称为workInProgress Fiber树
。3.已构建完的
workInProgress Fiber树
在commit阶段
渲染到页面。fiberRootNode
的current指针
指向workInProgress Fiber树
使其变为current Fiber 树
。update时
1.接下来点击p节点触发状态改变,这会开启一次新的
render阶段
并构建一棵新的workInProgress Fiber 树
。和mount时一样,
workInProgress fiber
的创建可以复用current Fiber树
对应的节点数据。(根据diff算法判断是否复用)2.
workInProgress Fiber 树
在render阶段
完成构建后进入commit阶段
渲染到页面上。渲染完毕后,workInProgress Fiber 树
变为current Fiber 树
。3、JSX、Fiber节点、React Component、React Element
JSX是一种描述当前组件内容的数据结构,Babel 会把JSX转译成一个名为React.createElement()函数调用。
这也是为什么在每个使用JSX的JS文件中,必须显式的声明
import React from 'react';
JSX
并不是只能被编译为React.createElement
方法,可以通过[@babel/plugin-transform-react-jsx](https://babeljs.io/docs/en/babel-plugin-transform-react-jsx) 插件显式告诉Babel
编译时需要将JSX
编译为什么函数的调用(默认为React.createElement
)。export function createElement(type, config, children) { let propName; const props = {}; let key = null; let ref = null; let self = null; let source = null; if (config != null) { // 将 config 处理后赋值给 props // ...省略 } const childrenLength = arguments.length - 2; // 处理 children,会被赋值给props.children // ...省略 // 处理 defaultProps // ...省略 return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, );}const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { // 标记这是个 React Element $$typeof: REACT_ELEMENT_TYPE, type: type, key: key, ref: ref, props: props, _owner: owner, }; return element;};
React.createElement
最终会调用ReactElement方法
返回一个包含组件数据的对象,该对象有个参数$$typeof: REACT_ELEMENT_TYPE
标记了该对象是个React Element
Class和Function组件
AppClass instanceof Function === true; AppFunc instanceof Function === true;所以无法通过引用类型区分ClassComponent
和FunctionComponent
。React通过ClassComponent
实例原型上的isReactComponent
变量判断是否是ClassComponent
。 ClassComponent.prototype.isReactComponent = {};
总结
JSX是一种描述当前组件内容的数据结构,他不包含组件schedule、reconcile、render所需的相关信息。
比如如下信息就不包括在JSX中:
- 组件在更新中的优先级
- 组件的state
- 组件被打上的用于Renderer的标记
这些内容都包含在Fiber节点中。
所以,在组件
mount
时,Reconciler
根据JSX
描述的组件内容生成组件对应的Fiber节点
。在
update
时,Reconciler
将JSX
与Fiber节点
保存的数据对比,生成组件对应的Fiber节点
,并根据对比结果为Fiber节点
打上标记。参考: React17.02 React 技术揭秘 React Fiber源码解析 图解React React Note