React源码之新老架构对比、Fiber架构、深入理解JSX

373 阅读7分钟

1. 新老React架构对比

React15:

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

缺点:

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

递归的缺点:

  1. 当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿
  2. React 15不支持可中断的异步更新代替同步的更新

React16:

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

Scheduler(调度器):

需要一种机制,当浏览器有剩余时间时通知我们,从而完成任务调度。

部分浏览器已经实现了这个API,这就是requestIdleCallback。但是由于以下因素,React放弃使用:

  • 浏览器兼容性
  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的requestIdleCallback触发的频率会变得很低

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

2. Fiber架构

2.1 fiber的核心思路

在react中遵循代数效应(algebraic effects)

代数效应时函数式编程中的一个概念,用于将副作用从函数中调用中分离。

function getTotalPicNum(user1, user2) {
  const picNum1 = getPicNum(user1);
  const picNum2 = getPicNum(user2);

  return picNum1 + picNum2;
}

假设getPickNum需要异步请求:async await?

  • 破坏了上下文的一致性,需要调用它的函数也是async

假如有一个类似的try...catch语法——try...handle、perform、resume

function getPicNum(name) {
  const picNum = perform name;
  return picNum;
}

try {
  getTotalPicNum('xianzao', 'houwan');
} handle (who) {
  switch (who) {
    case 'xianzao':
      resume with 230;
    case 'houwan':
      resume with 122;
    default:
      resume with 0;
  }
}

代数效应:将副作用(例子中为请求图片数量)从函数逻辑中分离,使函数关注点保持纯粹,也就是不用关心是同步还是异步

Example:Hooks,不用关心useState中state时如何保存变化的,我们只需要使用即可。

2.2 React Fiber

  1. 定义:React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。
  2. 功能:
    • 作为架构来说,之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler;
    • 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息;
    • 作为动态的工作单元来说,每个Fiber节点保存了本次更新该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新);

2.3 React Fiber Node定义

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance,静态节点的数据结构属性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber,用来链接其他fiber节点形成的fiber树
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  // 作为动态的工作单元的属性
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  this.effectTag = NoEffect;
  this.subtreeTag = NoSubtreeEffect;
  this.deletions = null;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  // 作为调度优先级的属性
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该fiber在另一次更新时对应的fiber
  this.alternate = null;

  if (enableProfilerTimer) {
    // Note: The following is done to avoid a v8 performance cliff.
    //
    // Initializing the fields below to smis and later updating them with
    // double values will cause Fibers to end up having separate shapes.
    // This behavior/bug has something to do with Object.preventExtension().
    // Fortunately this only impacts DEV builds.
    // Unfortunately it makes React unusably slow for some applications.
    // To work around this, initialize the fields below with doubles.
    //
    // Learn more about this here:
    // https://github.com/facebook/react/issues/14365
    // https://bugs.chromium.org/p/v8/issues/detail?id=8538
    this.actualDuration = Number.NaN;
    this.actualStartTime = Number.NaN;
    this.selfBaseDuration = Number.NaN;
    this.treeBaseDuration = Number.NaN;

    // It's okay to replace the initial doubles with smis after initialization.
    // This won't trigger the performance cliff mentioned above,
    // and it simplifies other profiler code (including DevTools).
    this.actualDuration = 0;
    this.actualStartTime = -1;
    this.selfBaseDuration = 0;
    this.treeBaseDuration = 0;
  }

  if (__DEV__) {
    // This isn't directly used but is handy for debugging internals:
    this._debugID = debugCounter++;
    this._debugSource = null;
    this._debugOwner = null;
    this._debugNeedsRemount = false;
    this._debugHookTypes = null;
    if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
      Object.preventExtensions(this);
    }
  }
}
  • 架构层面
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

表示的组件结构
function App() {
  return (
    <div>
      i am
      <span>xianzao</span>
    </div>
  )
}

Q:为什么指向的父节点是return而不是parent?

因为作为一个工作单元,return指节点执行完completeWork后会返回的下一个节点。子Fiber节点及其兄弟节点完成工作后返回其父级节点,所以用return指代父级节点。

  • 作为静态数据结构
// Fiber对应组件的类型 Function/Class...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class
this.type = null;
// Fiber对应的真实DOM节点
this.stateNode = null;
  • 作为动态工作单元

记录更新相关的信息,主要是updateQueue

Q:React Fiber 如何更新DOM?

使用“双缓存”

在内存中绘制当前的fiber dom,绘制完毕后直接替换上一帧的fiber dom,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。

在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber,正在内存中构建的Fiber树称为workInProgress Fiber,两者通过alternate连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点通过current指针指向不同的fiber dom切换,当update时,workInProgressFiber rende完成后会跟currentFiber 替换,下一次更新会将当前currentFiber(上一次的workInProgressFiber)替换

function App() {
  const [num, add] = useState(0);
  return (
    <p onClick={() => add(num + 1)}>{num}</p>
  )
}

ReactDOM.render(<App/>, document.getElementById('root'));
  • mount
  1. 首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是所在组件树的根节点;
    1. 区分fiberRootNode与rootFiber:因为在应用中我们可以多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。但是整个应用的根节点只有一个,那就是fiberRootNode;
    2. fiberRootNode的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber;
    3. 因为是首次渲染,此时页面还没有挂在所有的DOM,所以rootFiber还没有子fiber dom;

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

    1. 在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性(后续的diff),在首屏渲染时只有rootFiber存在对应的current fiber(即rootFiber.alternate)

  1. alternate阶段:此时workInProgress fiber已经构建完成,fiberRootNode的current指向了workInProgress fiber
  • update
  1. 假设p元素更新,这会开启一次新的render阶段并构建一棵新的workInProgress Fiber 树,且会尽可能复用显有的current Fiber

  1. alternate阶段

workInProgress fiber在更换完后,fiberRootNode的current指针更换。

3. 深入理解JSX

Q:

  • JSX和Fiber节点是同一个东西吗?
  • React Component、React Element是同一个东西吗,他们和JSX有什么关系?

JSX在编译时会被Babel编译为React.createElement方法,这也是为什么要引入 import React from 'react'; 的原因。

  • 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;
};

在全局API isValidElement里,通过$$typeof判断为REACT_ELEMENT_TYPE即为react元素,所以JSX返回的结构也是react element

export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}
  • React Component

Q:如何判断组件为class或者function组件

Example:codesandbox.io/s/jsx-type-…

ClassComponent对应的Element的type字段为AppClass自身。

FunctionComponent对应的Element的type字段为AppFunc自身。

且无法根据引用类型区分

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

实际上,React根据classComponent原型上的isReactComponent判读是否为ClassComponent

ClassComponent.prototype.isReactComponent; // {}
FunctionComponent.prototype.isReactComponent; // undefined

JSX与Fiber节点的关系

  • JSX是一种描述当前组件内容的 数据结构,他不包含组件schedule、reconcile、render所需的相关信息。
    • 比如如下信息就不包括在JSX中:组件在更新中的优先级、组件的state、组件被打上的用于Renderer的标记。
  • Fiber更多地是一种更新机制。
    • 在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。
    • 在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。