0. 前言
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 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
更新。
4. 源码相关
在这部分内容中,我们关注一下React源码的目录结构。出去配置文件和隐藏文件夹,根目录的文件包括三个:
根目录
|-- fixtures // 包含一些贡献者准备的小型 React 测试项目
|-- packages // 包含元数据(比如 package.json)和 React 仓库中所有 package 的源码(子目录 src)
|-- scripts // 各种工具链的脚本,比如git、jest、eslint
也就是说,我们想看React的源码,只需要关注package文件夹下面的代码即可。
从上图我们也能看到,package文件夹下面的文件相当多。因此,我们需要给他做一个分类:
- 三个主文件夹:
react
文件夹:React的核心,包含所有全局React API。这些API是全平台通用的,它不包含ReactDOM
、ReactNative
等平台特定的代码。在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对象。