《React技术揭秘》学习笔记(一):理念篇(下)

802 阅读6分钟

0. 前言

《React技术揭秘》学习笔记(一):理念篇(上)

3. Fiber相关

从上一节我们知道, 在React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。当递归的层级很深时,递归会占用线程很多时间,造成卡顿。为了解决这个问题,React16将递归的无法中断的更新重构为异步的可更新中断。由于之前递归的虚拟DOM无法满足可中断的需要,React16用Fiber架构来代替之前的虚拟DOM。在这一节,我们从三个方面来介绍Fiber架构的相关内容:

  • 代数效应
  • Fiber架构的实现原理
  • Fiber架构的工作原理

3.1 代数效应

代数效应是函数式编程的一个概念,用于将副作用从函数调用中分离,使函数关注点保持纯粹。

本文的关注点不是代数效应本身,而是代数效应在React中的应用。因此这里就不赘述了。想要详细了解代数效应的小伙伴,请戳这里

3.2 Fiber架构的实现原理

Fiber包含三层含义:

  • 作为架构来说,之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler. React16的Reconciler是基于Fiber节点来实现的,被称为Fiber Reconciler
  • 作为静态的数据结构来说,每个Fiber节点对应一个React Element,保存了该组件的类型、对应的节点信息
  • 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件变化的状态、要执行的工作

我们可以从源码中获取Fiber的属性定义如下:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  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.nextEffect = null;

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

  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  this.alternate = null;
}

然后我们来拆解这些属性:

  • 作为架构:每个Fiber节点都有对应React Element,并且通过指针指向其他对象构成Fiber树。对应的属性如下:
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
  • 作为静态数据结构:Fiber保存了组件相关的信息。对应的属性如下:
// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
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保存了本次更新相关的信息。对应的数据结构如下:
// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;

this.mode = mode;

// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;

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

3.3 Fiber架构的工作原理

使用Fiber更新DOM节点使用了双缓存的技术。那什么是双缓存呢?在内存中构建并直接替换的技术叫做双缓存。React使用双缓存来完成Fiber树的构建和替换对应着DOM树的创建和更新。

在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树. current Fiber树中的Fiber节点被称为current fiberworkInProgress 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树,通过currentworkInProgress的替换,完成DOM更新。

4. 源码相关

在这部分内容中,我们关注一下React源码的目录结构。出去配置文件和隐藏文件夹,根目录的文件包括三个:

根目录
|-- fixtures                 // 包含一些贡献者准备的小型 React 测试项目
|-- packages                 // 包含元数据(比如 package.json)和 React 仓库中所有 package 的源码(子目录 src)
|-- scripts                  // 各种工具链的脚本,比如git、jest、eslint

也就是说,我们想看React的源码,只需要关注package文件夹下面的代码即可。

图片.png

从上图我们也能看到,package文件夹下面的文件相当多。因此,我们需要给他做一个分类:

  • 三个主文件夹:
    • react文件夹:React的核心,包含所有全局React API。这些API是全平台通用的,它不包含ReactDOMReactNative等平台特定的代码。在NPM上作为单独的一个包发布
    • scheduler文件夹:scheduler(调度器)的实现
    • shared文件夹:源码中其他模块公用的方法和全局变量
  • Renderer相关的文件夹:对应不同平台的渲染
- react-art
- react-dom                 # 注意这同时是DOM和SSR(服务端渲染)的入口
- react-native-renderer
- react-noop-renderer       # 用于debug fiber(后面会介绍fiber)
- react-test-renderer
  • 试验性包的文件夹:
- react-server        # 创建自定义SSR流
- react-client        # 创建自定义的流
- react-fetch         # 用于数据请求
- react-interactions  # 用于测试交互相关的内部特性,比如React的事件模型
- react-reconciler    # Reconciler的实现,你可以用他构建自己的Renderer
  • 辅助包的文件而
- react-is       # 用于测试组件是否是某类型
- react-client   # 创建自定义的流
- react-fetch    # 用于数据请求
- react-refresh  # “热重载”的React官方实现
  • react-reconciler文件夹:我们需要重点关注的一个实验性的包,他一边对节Scheduler,一边对接不同平台的Renderer,构成了整个React16的架构体系

5. 深入理解JSX

相信作为React的使用者,很多看官已经接触过JSX。请看下面的代码:

const element = <h1>Hello, world!</h1>;

这段代码是不是很熟悉,它在React项目中几乎随处可见。这段代码等于号右边的部分就是JSX语法,是JavaScript的语法扩展,用来描述组建内容的数据结构。在这部分,我们就来跟随作者的脚步,深入了解一下JSX语法是如何工作的。

在 React 中,JSX代码会在编译时被Babel转义成React.createElement()方法。这也是在每个使用JSX的JS文件中显式声明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;
};

我们可以看到,这段代码将输入的三个参数打包成ReactElement返回,而ReactElement这个函数是一个React元素对象。也就是说,JSX的执行结果是一个ReactElement对象。