1、React架构

React15架构可以分为两层:

  • Reconciler(协调器)——负责找出有变化的组件
  • Renderer(渲染器)——负责将变化的组件渲染到页面上

Reconciler(协调器)

在React中可以通过this.setStatethis.forceUpdateReactDOM.render等API触发更新。

每当有更新发生时,Reconciler会做如下工作:

  • 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
  • 将虚拟DOM和上次更新时的虚拟DOM对比
  • 通过对比找出本次更新中变化的虚拟DOM
  • 通知Renderer将变化的虚拟DOM渲染到页面上

Renderer(渲染器)

由于React支持跨平台,所以不同平台有不同的RendererReactDOM则是负责在浏览器环境渲染的Renderer

在每次更新发生时,Renderer接到Reconciler通知,将变化的组件渲染在当前宿主环境。

在Reconciler中,mount的组件会调用mountComponentupdate的组件会调用updateComponent。这两个方法都会递归更新子组件

React16架构可以分为三层:

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
  • Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

Scheduler(调度器)

以浏览器是否有剩余时间作为任务中断的标准,当浏览器有剩余时间时通知我们。

React实现了功能更完备的requestIdleCallbackpolyfill,也就是Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。

Reconciler(协调器)

在React15中Reconciler是递归处理虚拟DOM的。

React16的更新工作从递归变成了可以中断的循环过程。每次循环都会调用shouldYield判断当前是否有剩余时间

  1. /** @noinline */
  2. function workLoopConcurrent() {
  3. // Perform work until Scheduler asks us to yield
  4. while (workInProgress !== null && !shouldYield()) {
  5. workInProgress = performUnitOfWork(workInProgress);
  6. }
  7. }

那么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 元素上下文信息的数据节点,以及由 childsiblingreturn 等指针域组成的链表的结构。

ReactFiber.js

  1. function FiberNode(
  2. tag: WorkTag,
  3. pendingProps: mixed,
  4. key: null | string,
  5. mode: TypeOfMode,
  6. ) {
  7. // 作为React Element静态数据结构的属性
  8. //标识 Fiber 类型的标签 ,Fiber对应组件的类型(Function/Class/Host)
  9. this.tag = tag;
  10. //根据 key 字段判断该 fiber 对象是否可以复用
  11. this.key = key;
  12. // 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
  13. this.elementType = null;
  14. // 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
  15. this.type = null;
  16. // Fiber对应的真实DOM节点
  17. this.stateNode = null;
  18. // 用于连接其他Fiber节点形成Fiber树
  19. //父Fiber节点
  20. this.return = null;
  21. //子Fiber节点
  22. this.child = null;
  23. //右边第一个兄弟
  24. this.sibling = null;
  25. this.index = 0;
  26. this.ref = null;
  27. //作为动态的工作单元 保存了本次更新相关的信息
  28. //在开始执行时设置 props 值,和 memoizedProps 一起使用, 若 pendingProps 与 memoizedProps 相等, 则可以复用上一个 fiber 相关的props
  29. this.pendingProps = pendingProps;
  30. //在结束时设置的 props 值
  31. this.memoizedProps = null;
  32. this.updateQueue = null;
  33. //current tree的state 也就是当前的 state
  34. this.memoizedState = null;
  35. this.dependencies = null;
  36. this.mode = mode;
  37. // 保存本次更新会造成的DOM操作 Effects
  38. this.flags = NoFlags;
  39. this.nextEffect = null;
  40. this.firstEffect = null;
  41. this.lastEffect = null;
  42. this.subtreeFlags = NoFlags;//17.02未使用
  43. this.deletions = null;//17.02未使用
  44. // 调度优先级相关
  45. // 用于标识一个 work 优先级顺序(React16.8版本为expirationTime,值越大优先级越高,17更改为lane)
  46. this.lanes = NoLanes;
  47. this.childLanes = NoLanes;
  48. //指向其对应的 workInProgress fiber
  49. this.alternate = null;
  50. ...
  51. }

从 React 元素创建一个 fiber 对象

  1. const createFiber = function(
  2. tag: WorkTag,
  3. pendingProps: mixed,
  4. key: null | string,
  5. mode: TypeOfMode,
  6. ): Fiber {
  7. // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
  8. return new FiberNode(tag, pendingProps, key, mode);
  9. };

workTag

ReactWorkTags.js

  1. export const FunctionComponent = 0;
  2. export const ClassComponent = 1;
  3. export const IndeterminateComponent = 2; // Before we know whether it is function or class
  4. export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
  5. export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
  6. export const HostComponent = 5;
  7. export const HostText = 6;
  8. export const Fragment = 7;
  9. export const Mode = 8;
  10. export const ContextConsumer = 9;
  11. export const ContextProvider = 10;
  12. export const ForwardRef = 11;
  13. export const Profiler = 12;
  14. export const SuspenseComponent = 13;
  15. export const MemoComponent = 14;
  16. export const SimpleMemoComponent = 15;
  17. export const LazyComponent = 16;
  18. export const IncompleteClassComponent = 17;
  19. export const DehydratedFragment = 18;
  20. export const SuspenseListComponent = 19;
  21. export const FundamentalComponent = 20;
  22. export const ScopeComponent = 21;
  23. export const Block = 22;
  24. export const OffscreenComponent = 23;
  25. export const LegacyHiddenComponent = 24;

Fiber 对象的 tag 属性值,称作 workTag,用于标识一个 React 元素的类型。

EffectTag

ReactWorkTags.js

不能在 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来区分何时执行 componentDidMountcomponentDidUpdate

当 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 FiberworkInProgress Fiber树中的Fiber节点被称为workInProgress Fiber,他们通过alternate属性连接。

  1. currentFiber.alternate === workInProgressFiber;
  2. workInProgressFiber.alternate === currentFiber;

React应用的根节点通过使current指针在不同Fiber树rootFiber间切换来完成current Fiber树指向的切换。

即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树

每次状态更新都会产生新的workInProgress Fiber树,通过currentworkInProgress的替换,完成DOM更新。

mount时

  1. function App() {
  2. const [num, add] = useState(0);
  3. return (
  4. <p onClick={() => add(num + 1)}>{num}</p>
  5. )
  6. }
  7. ReactDOM.render(<App/>, document.getElementById('root'));

1.首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是所在组件树的根节点。

之所以要区分fiberRootNoderootFiber,是因为在应用中我们可以多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。但是整个应用的根节点只有一个,那就是fiberRootNode

fiberRootNode的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber树。

  1. fiberRootNode.current = rootFiber;

由于是首屏渲染,页面中还没有挂载任何DOM,所以fiberRootNode.current指向的rootFiber没有任何Fiber节点(即current Fiber树为空)。

2.接下来进入render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树

3.已构建完的workInProgress Fiber树commit阶段渲染到页面。

fiberRootNodecurrent指针指向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文件中,必须显式的声明

  1. import React from 'react';

JSX并不是只能被编译为React.createElement方法,可以通过@babel/plugin-transform-react-jsx 插件显式告诉Babel编译时需要将JSX编译为什么函数的调用(默认为React.createElement)。

  1. export function createElement(type, config, children) {
  2. let propName;
  3. const props = {};
  4. let key = null;
  5. let ref = null;
  6. let self = null;
  7. let source = null;
  8. if (config != null) {
  9. // 将 config 处理后赋值给 props
  10. // ...省略
  11. }
  12. const childrenLength = arguments.length - 2;
  13. // 处理 children,会被赋值给props.children
  14. // ...省略
  15. // 处理 defaultProps
  16. // ...省略
  17. return ReactElement(
  18. type,
  19. key,
  20. ref,
  21. self,
  22. source,
  23. ReactCurrentOwner.current,
  24. props,
  25. );
  26. }
  27. const ReactElement = function(type, key, ref, self, source, owner, props) {
  28. const element = {
  29. // 标记这是个 React Element
  30. $$typeof: REACT_ELEMENT_TYPE,
  31. type: type,
  32. key: key,
  33. ref: ref,
  34. props: props,
  35. _owner: owner,
  36. };
  37. return element;
  38. };

React.createElement最终会调用ReactElement方法返回一个包含组件数据的对象,该对象有个参数$$typeof: REACT_ELEMENT_TYPE标记了该对象是个React Element

React Fiber原理探析 - 图1

AppClass instanceof Function === true;
AppFunc instanceof Function === true;

所以无法通过引用类型区分ClassComponentFunctionComponent。React通过ClassComponent实例原型上的isReactComponent变量判断是否是ClassComponent

ClassComponent.prototype.isReactComponent = {};

总结

JSX是一种描述当前组件内容的数据结构,他不包含组件schedule、reconcile、render所需的相关信息。

比如如下信息就不包括在JSX中:

  • 组件在更新中的优先级
  • 组件的state
  • 组件被打上的用于Renderer的标记

这些内容都包含在Fiber节点中。

所以,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点

update时,ReconcilerJSXFiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。

参考:
React17.02
React 技术揭秘
React Fiber源码解析
图解React
React Note