chst365's blog chst365's blog
首页
  • Git
  • 网络
  • 操作系统
  • 浏览器
  • webpack
  • JavaScript
  • TypeScript
  • 性能
  • 工程化
  • React
  • 编程题
  • React技术揭秘
  • 算法
  • Node
  • 编码解码
  • NodeJS系列
  • Linux系列
  • JavaScript系列
  • HTTP系列
  • GIT系列
  • ES6系列
  • 设计模式系列
  • CSS系列
  • 小程序系列
  • 数据结构与算法系列
  • React系列
  • Vue3系列
  • Vue系列
  • TypeScript系列
  • Webpack系列
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

chst365

DIV工程师
首页
  • Git
  • 网络
  • 操作系统
  • 浏览器
  • webpack
  • JavaScript
  • TypeScript
  • 性能
  • 工程化
  • React
  • 编程题
  • React技术揭秘
  • 算法
  • Node
  • 编码解码
  • NodeJS系列
  • Linux系列
  • JavaScript系列
  • HTTP系列
  • GIT系列
  • ES6系列
  • 设计模式系列
  • CSS系列
  • 小程序系列
  • 数据结构与算法系列
  • React系列
  • Vue3系列
  • Vue系列
  • TypeScript系列
  • Webpack系列
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 浏览器

  • webpack

  • TypeScript

  • 性能

  • 工程化

  • React

  • JavaScript

  • 编程题

  • React技术揭秘

    • React理念
    • React新老架构
    • Fiber
      • Fiber架构的心智模型
        • 代数效应
        • 代数效应在React中的应用
        • 代数效应与Generator
        • 代数效应与Fiber
      • Fiber架构的实现原理
        • Fiber的含义
        • Fiber的结构
      • Fiber架构的工作原理
        • 双缓存
        • 双缓存Fiber树
    • 前置知识
    • render阶段
    • commit阶段
    • Diff算法
    • 状态更新
    • Hooks
  • 算法

  • 前端
  • React技术揭秘
chst365
2022-09-14
目录

Fiber

# Fiber架构的心智模型

# 代数效应

代数效应是函数式编程中的一个概念,用于将副作用从函数调用中分离 接下来我们用虚构的语法来解释下代数效应。

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

  return picNum1 + picNum2;
}
1
2
3
4
5
6

函数getTotalPicNum,传入2个用户名称后,分别查找该用户在平台保存的图片数量,最后将图片数量相加后返回。 在getTotalPicNum中,我们不关注getPicNum的实现,只在乎“获取到两个数字后将他们相加的结果返回”这一过程。 接下来我们来实现getPicNum 用户在平台保存的图片数量是保存在服务器的,所以,我们需发起异步请求 为了尽量保持getTotalPicNum的调用方式不变,首先想到了async await

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

  return picNum1 + picNum2;
}
1
2
3
4
5
6

但是,async await是有传染性的——当一函数变为async后,调用他的函数也需async,破坏了getTotalPicNum的同步特性。 有什么办法能保持getTotalPicNum现有调用方式不变的情况下实现异步请求呢? 没有,不过可虚构一个。 虚构一个类似try...catch的语法 —— try...handle与两个操作符perform、resume

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

try {
  getTotalPicNum('kaSong', 'xiaoMing');
} handle (who) {
  switch (who) {
    case 'kaSong':
      resume with 230;
    case 'xiaoMing':
      resume with 122;
    default:
      resume with 0;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

当执行getPicNum方法时,会执行perform name,之后name会作为handle的参数。 与try...catch最大的不同在于:当Error被catch捕获后,之前的调用栈就销毁了。而handle执行resume后会回到之前的perform的调用栈。 从例子中可以看出,perform resume不需要区分同步异步。 总结一下:代数效应能够将副作用(例子中为请求图片数量)从函数逻辑中分离,使函数关注点保持纯粹

# 代数效应在React中的应用

代数效应在React中最明显的例子就是Hooks。 对于类似useState、useReducer、useRef这样的Hook,不需关注FunctionComponent的state在Hook中是如何保存的,React会处理。 我们只需假设useState返回的是我们想要的state,并编写业务逻辑就行。

function App() {
  const [num, updateNum] = useState(0);
  
  return (
    <button onClick={() => updateNum(num => num + 1)}>{num}</button>  
  )
}
1
2
3
4
5
6
7

这个例子不够明显,可看官方的Suspense Demo (opens new window) 在Demo中ProfileDetails用于展示用户名称。而用户名称是异步请求的。

但是Demo中完全是同步的写法。

function ProfileDetails() {
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}
1
2
3
4

# 代数效应与Generator

React从15到16,协调器(Reconciler)重构的一大目的是:将老的同步更新架构变为异步可中断更新。 异步可中断更新可理解为:更新在执行过程中可能会被打断,当可以继续执行时恢复之前执行的中间状态。 这就是代数效应中try...handle的作用。 其实,浏览器原生支持类似的实现Generator 但它的一些缺陷是React放弃了它:

  • Generator也是有传染性的,使用它则上下文的其他函数也需作出改变
  • Generator执行的中间状态是上下文关联的

举个例子

function* doWork(A, B, C) {
  var x = doExpensiveWorkA(A);
  yield;
  var y = x + doExpensiveWorkB(B);
  yield;
  var z = y + doExpensiveWorkC(C);
  return z;
}
1
2
3
4
5
6
7
8

当浏览器有空时都会依次执行其中一个doExpensiveWork,当时间用尽则中断,当再次恢复时则会从中断位置继续执行。 只考虑单一优先级任务的中断与继续时,Generator可很好的实现异步可中断更新。 但当有高优先级任务插队时,若此时已完成A与B计算出x与y。 此时B组件收到一高优更新,由于Generator执行的中间状态是上下文关联的,计算y时无法复用之前已得的x,需重新计算。 若通过全局变量保存之前执行的中间状态,又会引入新的复杂度。 更详细的解释可参考这个issue (opens new window)。 基于这些原因,React没有采用Generator实现协调器。

# 代数效应与Fiber

Fiber中文叫纤程,与进程(Process)、线程(Thread)、协程(Coroutine)同为程序执行过程。 在很多文章中将纤程理解为协程的一种实现。在js中,协程的实现便是Generator。 所以可将纤程、协程理解为代数效应思想在JS中的体现。 React Fiber可理解为: React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,且恢复后可复用之前的中间状态。 其中每个任务更新单元为React Element对应的Fiber节点。

# Fiber架构的实现原理

在React15及以前,Reconciler采用递归的方式创建虚拟DOM,此过程不可中断。若组件树层级很深,递归会占用线程很多时间,造成卡顿。 Fiber的诞生解决了这个问题。

# Fiber的含义

Fiber有三层含义:

  • 作为架构来说,之前的React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler
  • 作为静态数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件、类组件、原生组件...)、对应DOM节点等信息。
  • 作为动态的工作单元来说,每个Fiber节点保存了本次更新中组件改变的状态、要执行的工作(需被删除、被插入页面中、被更新...)

# Fiber的结构

从这里 (opens new window)可看到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;

  // 用于连接其他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.nextEffect = null;

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

  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该fiber在另一次更新时对应的fiber
  this.alternate = null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

# 作为架构来说

每个Fiber节点有个对应的React element,多个Fiber节点是如何连接形成树呢?靠如下三个属性:

// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
1
2
3
4
5
6

举个例子,如下的组件结构

function App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>
  )
}
1
2
3
4
5
6
7
8

对应的Fiber树结构: 这里注意:为什么父级指针叫return而不是parent或father? 作为一个工作单元,return指节点执行完completeWork后会返回下一个节点。子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;
1
2
3
4
5
6
7
8
9
10

# 作为动态的工作单元

作为动态的工作单元,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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

如下两个字段保存调度优先级相关的信息,会在讲解Scheduler时介绍。

// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
1
2
3

# Fiber架构的工作原理

# 双缓存

当用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。 若当前帧画面计算量较大,导致清除上一帧画面到绘制当前帧画面间有较长间隙,会出现白屏。 为解决此问题,可在内存中绘制当前帧动画,绘制完后直接替换上一帧画面,省去了两帧替换间的计算时间,不会出现白屏。 这种在内存中构建并直接替换的技术叫双缓存 (opens new window)。 React使用双缓存来完成Fiber树的构建与替换——对应DOM树的创建与更新。

# 双缓存Fiber树

在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树为current Fiber树,正在内存中构建的Fiber树为workInProgress Fiber树。 current Fiber树中的Fiber节点叫currentFiber,workInProgress Fiber树中的Fiber节点叫workInProgressFiber。它们通过alternate属性连接。

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

React应用的根节点通过是current指针在不同Fiber树的rootFiber间切换来完成current Fiber树指向的切换。 每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。 以具体例子讲解mount时、update时的构建/替换流程。

# mount时

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

ReactDOM.render(<App/>, document.getElementById('root'));
1
2
3
4
5
6
7
8
  1. 首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot,是整个应用的根节点)和rootFiber(<App/>所在组件树的根节点)。 为什么要区分两者呢?因为在应用中可多次调用ReactDOM.render渲染不同的组件树,它们会拥有不同的rootFiber。但整个应用的根节点只有一个。 fiberRootNode的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber树
fiberRootNode.current = rootFiber;
1

由于是首屏渲染,页面没挂载任何DOM,所以rootFiber无子Fiber节点。 2. 进入render阶段,组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,为workInProgress Fiber树 下图中右侧为内存中构建的树,左侧为页面显示的树。 在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性。 在首屏渲染时只有rootFiber存在对应的current fiber(即rootFiber.alternate)。 3. 图中右侧已构建完的workInProgress Fiber树在commit阶段渲染到页面。 此时DOM更新为右侧对应的样子。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 树。

#前端#React技术揭秘
上次更新: 2022/09/16, 17:10:42
React新老架构
前置知识

← React新老架构 前置知识→

最近更新
01
面试官
03-27
02
this&指针&作用域&闭包
03-27
03
前端
03-27
更多文章>
Theme by Vdoing | Copyright © 2019-2025 chst365 | 豫ICP备17031889号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式