React源码理解之Fiber

react16就是用fiber写的
fiber解决了执行栈不能中断的问题

1.屏幕刷新率

  • 目前大多数设备的屏幕刷新率为 60 次/秒
  • 浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致
  • 页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿
  • 每个帧的预算时间是16.66 毫秒 (1秒/60)
  • 1s 60帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms。所以我们书写代码时力求不让一帧的工作量超过 16ms

2.帧

  • 每个帧的开头包括样式计算、布局和绘制
  • JavaScript执行 Javascript引擎和页面渲染引擎在同一个渲染线程,GUI渲染和Javascript执行两者是互斥的
  • 如果某个任务执行时间过长,浏览器会推迟渲染

2.1 rAF

requestAnimationFrame 回调函数会在绘制之前执行

window.requestAnimationFrame() 告诉浏览器你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
即在上图布局、绘制之前的浅蓝色区域执行。
requestIdleCallback就是上图最右侧的绿色区域,如果在这个区域操作DOM就会重新渲染。

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
<body>
<div style="background: lightblue;width: 0;height: 20px;"></div>
<button>开始</button>
<script>
/**
* requestAnimationFrame(callback) 由浏览器专门为动画提供的API
* cancelAnimationFrame(返回值) 清除动画
* <16.7 丢帧
* >16.7 跳跃 卡顿
*/
const div = document.querySelector('div');
const button = document.querySelector('button');
let start;
function progress() {
// div的宽 = div之前的宽 + 1px
div.style.width = div.offsetWidth + 1 + 'px';
div.innerHTML = (div.offsetWidth) + '%';
if (div.offsetWidth < 100) {
let current = Date.now();
// 打印的是line28开始准备执行的时间到真正执行的时间差
console.log(current - start);
start = current;
timer = requestAnimationFrame(progress);
}
}
button.addEventListener('click',function(){
div.style.width = 0;
start = Date.now();// 先获取到当前时间 current是毫秒数
requestAnimationFrame(progress);
})
</script>
</body>

点击button之后,可以看到打印出的时间差都是16、17秒,基本上在16.7左右

2.2 requestIdleCallback

  • 我们希望快速响应用户,让用户觉得够快,不能阻塞用户的交互
  • requestIdleCallback使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
  • 正常帧任务完成后没超过16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务


上图,绿色的框中就是一帧。
首先,用户代码向浏览器申请时间片,当浏览器执行完之后如果有剩余时间,就分配时间片给用户代码执行,在约定时间内再归还控制器。

1
2
3
4
// 这是一个全局属性
// 告诉浏览器,我现在执行callback函数,但是它优先级比较低,告诉浏览器可以空闲的时候执行
// 但是如果到了超时时间,就必须马上执行
requestIdleCallback(callback, {timeout: 1000});
1
2
3
4
5
6
7
8
9
window.requestIdleCallback(
callback: (deaLine: IdleDeadline) => void,
option?: {timeout: number}
)

interface IdleDeadline {
didTimeout: boolean // 表示任务执行是否超过约定时间
timeRemaining(): DOMHighResTimeStamp // 任务可供执行的剩余时间
}
  • callback:回调即空闲时需要执行的任务,该回调函数接收一个IdleDeadline对象作为入参。其中IdleDeadline对象包含:
    • didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用
    • timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少
  • options:目前 options 只有一个参数
    • timeout。表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲
      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
      <body>
      <script>
      function sleep(delay) {
      for (var time = Date.now(); Date.now() - time <= delay;);
      }
      const works = [
      () => {
      console.log("第1个任务开始");
      //sleep(20); // 一帧16.6 因为此任务的执行时间超过了16.6毫秒,所以把控制权交给浏览器
      //while(true){}
      console.log("第1个任务结束");
      },
      () => {
      console.log("第2个任务开始");
      //sleep(20);
      console.log("第2个任务结束");
      },
      () => {
      console.log("第3个任务开始");
      //sleep(20);
      console.log("第3个任务结束");
      },
      ];

      requestIdleCallback(workLoop, { timeout: 1000 });
      function workLoop(deadline) {
      console.log('本帧剩余时间', parseInt(deadline.timeRemaining()));
      // 如果此帧的剩余时间超过0 或者 已经超时了(timeout)
      while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && works.length > 0) {
      performUnitOfWork();
      } // 如果说没有剩余时间了,就需要放弃执行任务控制权,执行权交还给浏览器

      if (works.length > 0) { // 说明还有未完成的任务
      console.log(`只剩下${parseInt(deadline.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`);
      requestIdleCallback(workLoop, { timeout: 1000 });
      }
      }
      function performUnitOfWork() {
      //console.log("performUnitOfWork:",works.length);
      works.shift()();
      }
      </script>
      </body>

这里的line29 while,每完成一个任务(works),都会判断是否还有剩余时间,有的话再去执行下一个任务,即line30

如果把上面的sleep(20)注释都打开,打印如下:

1
2
3
4
5
6
7
8
9
10
11
本帧剩余时间 14
第1个任务开始
第1个任务结束
只剩下0ms,时间片到了等待下次空闲时间的调度
本帧剩余时间 9
第2个任务开始
第2个任务结束
只剩下0ms,时间片到了等待下次空闲时间的调度
本帧剩余时间 15
第3个任务开始
第3个任务结束

如果line10的while(true){}那执行任务就会卡在这里
如果把sleep都打开,上述任务总共执行时间为60ms+,具体时间值不固定

这里的一帧16.6毫秒只是大多数情况,显示器越强,刷新频率越高,一帧的时间越短。一秒多少帧跟显示器的刷新频率有关。只有当显示器的刷新频率是60HZ的时候,一帧才是16.6毫秒。

fiber是把整个任务分成很多小任务,每次执行一个任务,执行完成后会看看有没有剩余时间,如果有继续下一个任务,如果没有放弃执行,交给浏览器调度。

2.3 MessageChannel

3. 单链表

在fiber中很多地方都用到链表

  • 单链表是一种链式存取的数据结构
  • 链表中的数据是以节点来表示的,每个节点的构成:元素 + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个节点的地址

如上图,firstUpdate 头指针、lastUpdate 尾指针、nextUpdate 指向下一个

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
44
45
46
47
48
49
class Update {
constructor(payload) {
this.payload = payload; // payload指数据或者元素
this.nextUpdate = null; // 指向下一个节点的指针
}
}
class UpdateQueue {
constructor() {
this.baseState = null; // 原状态
this.firstUpdate = null; // 第一个 更新
this.lastUpdate = null; // 最后一个更新
}
clear() {
this.firstUpdate = null;
this.lastUpdate = null;
}
enqueueUpdate(update) {
if (this.firstUpdate === null) {
this.firstUpdate = this.lastUpdate = update;
} else {
this.lastUpdate.nextUpdate = update; // 最后一个元素的next指向新插入的这个元素
this.lastUpdate = update; // 尾指针指向新插入的这个元素
}
}
// 获取老状态,然后遍历这个链表,进行更新 更新状态
forceUpdate() {
let currentState = this.baseState || {}; // 初始状态
let currentUpdate = this.firstUpdate;
while (currentUpdate) {
let nexState = typeof currentUpdate.payload == 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload;
currentState = { ...currentState, ...nexState }; // 解构老状态 合并新状态
currentUpdate = currentUpdate.nextUpdate;
}
this.firstUpdate = this.lastUpdate = null; // 更新完成之后清空列表
this.baseState = currentState;
return currentState;// 返回新状态
}
}


let queue = new UpdateQueue();
queue.enqueueUpdate(new Update({ name: 'test1' }));
queue.enqueueUpdate(new Update({ number: 0 }));
queue.enqueueUpdate(new Update(state => ({ number: state.number + 1 })));
queue.enqueueUpdate(new Update(state => ({ number: state.number + 1 })));
queue.enqueueUpdate(new Update({ name: 'test2' }));
queue.enqueueUpdate(new Update({ age: '18' }));
queue.forceUpdate();
console.log(queue.baseState);

打印: { name: ‘test2’, number: 2, age: ‘18’ }

一个虚拟DOM是一个链表节点,也是一个工作单元。一个虚拟DOM就是一个最小单元。

4. fiber历史

  1. fiber之前是什么样的?为什么需要fiber
  2. 看一下fiber之后的代码是怎么样的?

4.1 fiber之前的协调

  • React 会递归比对VirtualDOM树,找出需要变动的节点,然后同步更新它们。这个过程 React 称为Reconcilation(协调)
  • 在Reconcilation期间,React 会一直占用着浏览器资源,一则会导致用户触发的事件得不到响应, 二则会导致掉帧,用户可能会感觉到卡顿
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
let root = {
key: 'A1',
children: [
{
key: 'B1',
children: [
{
key: 'C1',
children: []
},
{
key: 'C2',
children: []
}
]
},
{
key: 'B2',
children: []
}
]
}
function walk(element) {
doWork(element);
element.children.forEach(child=>{
walk(child)
});
}

function doWork(element) {
console.log(element.key);
}
walk(root);

如上数据结构,A1下有B1、B2,B1下有C1、C2
打印:A1 B1 C1 C2 B2 深度优先遍历

这种遍历是递归调用,执行栈会越来越深,不能中断,因为中断后想恢复就非常难了。
1.不能中断 2.执行栈太深

4.2 fiber是什么

  • 我们可以通过某些调度策略合理分配CPU资源,从而提高用户的响应速度
  • 通过Fiber架构,让自己的Reconcilation过程变成可被中断。 适时地让出CPU执行权,除了可以让浏览器及时地响应用户的交互

4.2.1 fiber是一个执行单位

  • Fiber是一个执行单元,每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去

4.2.2 fiber是一种数据结构

  • React目前的做法是使用链表, 每个 VirtualDOM 节点内部表示为一个Fiber

1
2
3
4
5
6
7
8
9
10
type Fiber = {
//类型
type: any,
//父节点
return: Fiber,
// 指向第一个子节点
child: Fiber,
// 指向下一个弟弟
sibling: Fiber
}

5. fiber执行阶段

  • 每次渲染有两个阶段:Reconciliation(协调\render阶段)和Commit(提交阶段)
  • 协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更React 称之为副作用(Effect)
  • 提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行了。这个阶段必须同步执行,不能被打断

5.1 render阶段

render阶段会构建fiber树

element.js

1
2
3
4
5
6
7
8
9
10
let A1 = { type: 'div', key: 'A1' };
let B1 = { type: 'div', key: 'B1', return: A1 };
let B2 = { type: 'div', key: 'B2', return: A1 };
let C1 = { type: 'div', key: 'C1', return: B1 };
let C2 = { type: 'div', key: 'C2', return: B1 };
A1.child = B1;
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2;
module.exports = A1;

render.js 遍历fiber树

  • 从顶点开始遍历
  • 如果有第一个儿子,先遍历第一个儿子
  • 如果没有第一个儿子,标志着此节点遍历完成
  • 如果有弟弟遍历弟弟
  • 如果有没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔遍历叔叔
  • 没有父节点遍历结束

如上图:
遍历顺序:A1,有儿子,B1,有儿子,C1,没有儿子,有弟弟,C2,没有儿子,没有弟弟,有叔叔,即B1的弟弟,B2,没儿子,没弟弟,没叔叔,回到A1

完成顺序:c1C2B1B2A1。 这里C1没有儿子,里面完成了,接着是C2没有儿子,完成,C1、C2都完成了,B1完成一次类推。

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
let rootFiber = require('./element');
//下一个执行工作单元
let nextUnitOfWork = null;
//render工作循环
function workLoop() {
while (nextUnitOfWork) {// 如果有待执行的执行单元就执行
//执行一个任务并返回下一个任务
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
//render阶段结束
}
// 执行单个工作单元
function performUnitOfWork(fiber) { // A1 B1
beginWork(fiber); // 处理次fiber
if (fiber.child) {//如果有子节点就返回第一个子节点
// 这里A1有child返回B1;B1有儿子返回C1
return fiber.child;
}
while (fiber) {//如果没有子节点说明当前节点已经完成了渲染工作 C1
completeUnitOfWork(fiber);//可以结束此fiber的渲染了
if (fiber.sibling) {//如果它有弟弟就返回弟弟
return fiber.sibling;
}
fiber = fiber.return;//如果没有弟弟让爸爸完成,然后找叔叔
}
}
function beginWork(fiber) {
console.log('beginWork', fiber.key); // A1
//fiber.stateNode = document.createElement(fiber.type);
}
function completeUnitOfWork(fiber) {
console.log('completeUnitOfWork', fiber.key);
}
nextUnitOfWork = rootFiber;
workLoop();

打印:

1
2
3
4
5
6
7
8
9
10
beginWork A1
beginWork B1
beginWork C1
completeUnitOfWork C1
beginWork C2
completeUnitOfWork C2
completeUnitOfWork B1
beginWork B2
completeUnitOfWork B2
completeUnitOfWork A1

现在把它和之前的requestIdleCallback连起来

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
function workLoop(deadline) {
// while (nextUnitOfWork) {
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork) { // 说明还有未完成的任务
console.log(`只剩下${parseInt(deadline.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`);
requestIdleCallback(workLoop, { timeout: 1000 });
}
}
function performUnitOfWork(fiber) {
beginWork(fiber);
if (fiber.child) {
return fiber.child;
}
while (fiber) {
completeUnitOfWork(fiber);
if (fiber.sibling) {
return fiber.sibling;
}
fiber = fiber.return;
}
}
function beginWork(fiber) {
console.log('beginWork', fiber.key);
}
function completeUnitOfWork(fiber) {
console.log('completeUnitOfWork', fiber.key);
}
nextUnitOfWork = rootFiber;
requestIdleCallback(workLoop, {timeout: 1000});

相当于用链实现了一个类似于可中断的递归的功能

vue中不是fiber,在vue中是把每个更新任务分割的足够小,它是组件级别的更新,更新范围小。vue3采用了模板标记更新,能很快速的定位到更新的地方进行更新。
react是不管在哪里调setState,都是从根节点(指整个项目的根节点)比较更新的。在react中是任务还是很大,但是分割成多个小任务,可以中断和恢复,不阻塞主进程执行高级优先任务。

5.2 commit阶段

6. 手动实现React

创建一个空项目 create-react-app myApp

6.1 实现虚拟DOM

1】

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import ReactDOM from 'react-dom';
// JSX其实是一种特殊语法,在babel编译的时候会编译成js
let element = (
<div id="A1">
<div id="B1">
<div id="C1"></div>
<div id="C2"></div>
</div>
<div id="B2"></div>
</div>
);

ReactDOM.render(
element,
document.getElementById('root')
);

将上述line4-ling12放到https://www.babeljs.cn/中编译(勾选左边的react),结果:

1
2
3
4
5
6
// React.createElement(type,props,...children);
var element = React.createElement("div", {id: "A1"},
React.createElement("div", {id: "B1"},
React.createElement("div", {id: "C1"}),
React.createElement("div", {id: "C2"})),
React.createElement("div", {id: "B2"}));
1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import ReactDOM from 'react-dom';

var element = React.createElement("div", {id: "A1"},
React.createElement("div", {id: "B1"},
React.createElement("div", {id: "C1"}),
React.createElement("div", {id: "C2"})),
React.createElement("div", {id: "B2"}));
console.log(element);
ReactDOM.render(
element,
document.getElementById('root')
);

React.createElement 返回一个元素,也就是虚拟DOM,虚拟DOM就是一个JS对象,以JS对象的方式描述界面上DOM的样子

React.createElement替换jsx格式在页面上显示是一样的,还是以A1为开头的div 的数据格式,console.log()打印出来如下:

新建 src/react.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 创建元素(虚拟DOM)的方法
* @param {*} type 元素的类型
* @param {*} config 配置对象 属性、key、ref
* @param {...any} children 放置所有的儿子 这里做成数组,正在在react中可能是数组也可能是对象
*/
function createElement(type, config, ...children){
delete config.__self;
delete config.__source; // 表示这个文件是在哪行哪列哪个文件生成的
return {
type,
props:{
...config,
children
}
}
}

2】

当C1中的内容是文本时,children里面不是虚拟DOM,是文本值
src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import ReactDOM from 'react-dom';
let element = (
<div id="A1">
<div id="B1">
<div id="C1">C1内容</div>
<div id="C2">C2内容</div>
</div>
<div id="B2">B2内容</div>
</div>
);

console.log(element);
ReactDOM.render(
element,
document.getElementById('root')
);

新建src/constants.js

1
2
// 表示这是一个文本元素
export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT');
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
import {ELEMENT_TEXT} from './constants';
/**
* 创建元素(虚拟DOM)的方法
* @param {*} type 元素的类型
* @param {*} config 配置对象 属性、key、ref
* @param {...any} children 放置所有的儿子 这里做成数组,正在在react中可能是数组也可能是对象
*/
function createElement(type, config, ...children){
delete config.__self;
delete config.__source; // 表示这个文件是在哪行哪列哪个文件生成的
return {
type,
props:{
...config,
children: children.map(child=>{
// 做了一个兼容处理,如果是react元素就返回自己,如果是一个字符串的话就返回元素对象
return typeof child === 'object' ? child : { // 源码里面没有转,就是字符串
type: ELEMENT_TEXT,
props: {text:child, children: []}
}
})
}
}
}
const React = {
createElement
};
export default React;

调用自己写的react看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from './react';
import ReactDOM from 'react-dom';
let element = (
<div id="A1">
<div id="B1">
<div id="C1">C1内容</div>
<div id="C2">C2内容</div>
</div>
<div id="B2">B2内容</div>
</div>
);

console.log(element);
// ReactDOM.render(
// element,
// document.getElementById('root')
// );

打印出的虚拟DOM结构如下:

6.2 实现初次渲染

6.2.1 根据虚拟DOM生成fiber树

1】

原生的效果
src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import ReactDOM from 'react-dom';
let style = { border: '3px solid red', margin: '5px' };
let element = (
<div id="A1" style={style}>A1内容
<div id="B1" style={style}>B1内容
<div id="C1" style={style}>C1内容</div>
<div id="C2" style={style}>C2内容</div>
</div>
<div id="B2" style={style}>B2内容</div>
</div>
);

console.log(element);
ReactDOM.render(
element,
document.getElementById('root')
);

src/constants.js

1
2
3
4
5
6
7
8
// 表示这是一个文本元素
export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT');
// React应用需要一个跟fiber
export const TAG_ROOT = Symbol.for('TAG_ROOT');
// 原生的节点 span div 函数组件 类组件
export const TAG_HOST = Symbol.for('TAG_HOST');
// 这是文本节点
export const TAG_TEXT = Symbol.for('TAG_TEXT');

新建 src/react-dom.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {TAG_ROOT} from './constants';
/**
*render是要把一个元素渲染到一个容器内部
*每个虚拟DOM节点对应一个fiber
* @param {*} element 虚拟DOM
* @param {*} container 容器
*/
function render(element, container){ // ReactDOM.render(element,document.getElementById('root')); 这里 container 对应的是<div id="root"></div>这个真实的DOM元素
let rootFiber = {
tag: TAG_ROOT, // 每个fiber都会有一个tag标识,
stateNode: container, // 一般情况下如果这个元素是一个原生节点的话,stateNode指向真实DOM元素
// props.children 是一个数组,里面放的是React元素 即虚拟DOM ,后面会根据每个React元素创建对应的fiber
props:{ children: [element] } // 这个fiber的属性对象children属性,里面放的是要渲染的元素
}
scheduleRoot(rootFiber); // 开始调度,从根节点开始遍历
}
const ReactDOM = {
render
};
export default ReactDOM;

新建 src/schedule.js

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
/**
*从根节点开始渲染和调度
*两个阶段:
一、diff阶段,对比新旧的虚拟DOM,进行增量 更新或创建
render阶段:这个阶段比较花时间,可以对任务进行拆分,拆分的维度就是虚拟DOM节点,一个虚拟DOM节点对应一个任务,此阶段可以暂停,成果是effect list知道哪些节点删除了 增加了
二、commit阶段: 进行DOM更新创建阶段,此阶段不能暂停,要一气呵成
*/
let nextUnitOfWork = null//下一个工作单元
let workInProgressRoot = null;//正在渲染中的根Fiber
function scheduleRoot(rootFiber){ // {tag: TAG_ROOT, stateNode: container, props:{ children: [element] } }
// 大致原理:根据 element虚拟DOM 创建真实DOM插入到容器 container 中
//把当前树设置为nextUnitOfWork开始进行调度
workInProgressRoot = rootFiber;
nextUnitOfWork = rootFiber;
}
function workLoop(deadline){
let shouldYield = false; // 是否要让出时间片或控制权
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);//执行一个任务并返回下一个任务
shouldYield = deadline.timeRemaining() < 1;//如果剩余时间小于1毫秒就说明没有时间了,需要把控制权让给浏览器
}
//如果没有下一个执行单元了,并且当前渲染树存在,则进行提交阶段
if (nextUnitOfWork) { // 如果时间片到期后还有任务没完成,就需要请求浏览器再次调度
requestIdleCallback(workLoop, {timeout: 500});
}else{
console.log("渲染阶段结束");
}
}
// react告诉浏览器,我现在有任务,请你在闲的时候执行
// 这里有一个优先级的概念 expirationTime
requestIdleCallback(workLoop, {timeout: 500});
2】跟节点A1

src/schedule.js

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
function performUnitOfWork(currentFiber) {
beginWork(currentFiber);//开始渲染前的Fiber,就是把子元素变成子fiber

if (currentFiber.child) {//如果有子节点就返回第一个子节点 做下一个任务
return currentFiber.child;
}
while (currentFiber) {//如果没有子节点说明当前节点已经完成了渲染工作 开始找弟弟
completeUnitOfWork(currentFiber);//可以结束此fiber的渲染了
if (currentFiber.sibling) {//如果它有弟弟就返回弟弟
return currentFiber.sibling;
}
currentFiber = currentFiber.return;//如果没有弟弟让爸爸完成,然后找叔叔
}
}
/**
*beginwork 开始收下线的钱
*completeUnitOfWork 把下线的钱收完了
* @param {*} currentFiber
* 1. 创建真实DOM元素
* 2. 创建子fiber
*/
function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) {//如果是根节点
updateHostRoot(currentFiber);
}
}
function updateHostRoot(currentFiber) {//如果是根节点
const newChildren = currentFiber.props.children;//直接渲染子节点
reconcileChildren(currentFiber, newChildren);
}
function reconcileChildren(currentFiber, newChildren){// 根据虚拟DOM节点 创建子fiber
let newChildIndex = 0;// 新子节点的索引
let prevSibling; // 上一个新的子fiber
// 遍历子虚拟DOM数组 虚拟DOM转化为fiber
while (newChildIndex < newChildren.length) {
const newChild = newChildren[newChildIndex]; // 取出虚拟DOM节点
let tag;
if (newChild && newChild.type === ELEMENT_TEXT) {
tag = TAG_TEXT;//文本
} else if (newChild && typeof newChild.type === 'string') {
tag = TAG_HOST;//如果type是字符串,那他是原生DOM节点
}
let newFiber = {
tag,//原生DOM组件 TAG_HOST
type: newChild.type,//具体的元素类型
props: newChild.props,//新的属性对象
stateNode: null,//stateNode肯定是空的,因为这个时候div还没有创建DOM元素
return: currentFiber,//父Fiber
effectTag: PLACEMENT,//副作用标识 增加节点 副作用标识,在render阶段我们会收集副作用,有增加 删除 更新
nextEffect: null // effect list也是一个单链表 nextEffect指向下一个节点
// effect list的顺序和完成顺序是一样的 但是只放出钱的fiber即有副作用的fiber
}// 在beginWork创建fiber 在completeUnitOfWork的时候收集effect
if (newFiber) {
if (newChildIndex === 0) {
currentFiber.child = newFiber;//第一个子节点挂到父节点的child属性上
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;//然后newFiber变成了上一个哥哥了
}
newChildIndex++;
}
}

constants.js

1
2
3
4
5
6
7
// 副作用
// 增加节点
export const PLACEMENT = Symbol.for('PLACEMENT');
// 更新节点
export const UPDATE = Symbol.for('UPDATE');
// 删除节点
export const DELETION = Symbol.for('DELETION');
3】如果是原生文本节点

新建 src/utils.js

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
    if (/^on/.test(key)) {
dom[key.toLowerCase()] = value;
} else if (key === 'style') {
if (value) {
for (let styleName in value) {
if (value.hasOwnProperty(styleName)) {
dom.style[styleName] = value[styleName];
}
}
}
} else {
dom.setAttribute(key, value);
}
return dom;
}
export function setProps(elem, oldProps, newProps) {
for (let key in oldProps) {
if (key !== 'children') {
if (newProps.hasOwnProperty(key)) {
setProp(elem, key, newProps[key]);
} else {
elem.removeAttribute(key);
}
}
}
for (let key in newProps) {
if (key !== 'children') {
setProp(elem, key, newProps[key])
}
}
}

schedule.js

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
function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) {
updateHostRoot(currentFiber);
} else if (currentFiber.tag === TAG_TEXT) {//如果是原生文本节点
updateHostText(currentFiber);
}
}
//如果是原生文本节点
function updateHostText(currentFiber) {
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点
}
}
function createDOM(currentFiber) {
if (currentFiber.type === ELEMENT_TEXT) { // 如果是文本节点创建文本节点
return document.createTextNode(currentFiber.props.text);
}else if(currentFiber.type === TAG_HOST){
const stateNode = document.createElement(currentFiber.type);
updateDOM(stateNode, {}, currentFiber.props);
return stateNode;
}

}
function updateDOM(stateNode, oldProps, newProps) {
setProps(stateNode, oldProps, newProps);
}
4】如果是原生DOM节点

schedule.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) {
updateHostRoot(currentFiber);
} else if (currentFiber.tag === TAG_TEXT) {
updateHostText(currentFiber);
} else if (currentFiber.tag === TAG_HOST) {//如果是原生DOM节点
updateHostComponent(currentFiber);
}
}
//如果是原生DOM节点
function updateHostComponent(currentFiber) {//如果是原生DOM节点
if (!currentFiber.stateNode) {// 如果此fiber没有创建DOM节点
currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点
}
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}

6.2.2 收集effect list

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
// 在完成的时候要收集有副作用的fiber,然后组成effect list
// 每个fiber有两个属性,一个firstEffect指向第一个有副作用的子fiber lastEffect指向最后一个有副作用的子fiber
// 中间的用nextEffect做成一个单链表 firstEffect->大儿子,大儿子.nextEffect->二儿子,二儿子.nextEffect->三儿子,lastEffect也指向三儿子
function completeUnitOfWork(currentFiber) {// 第一个完成的A1(TEXT)
const returnFiber = currentFiber.return; //找它爹A1
if (returnFiber) {
////////// 这段是把自己儿子的链挂到父亲身上
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect;
}
if (!!currentFiber.lastEffect) {
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
}
returnFiber.lastEffect = currentFiber.lastEffect;
}
////////// 这段是把自己挂到儿子身上
const effectTag = currentFiber.effectTag;
if (effectTag) { // 如果有值,有副作用
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber;
} else {
returnFiber.firstEffect = currentFiber;
}
returnFiber.lastEffect = currentFiber;
}
}
}

A1:B1,B2
B1:C1,C2
B2:D1,D2

B1.firstEffect->C1
B1.lastEffect->C2

A1.firstEffect->C1
A1.lastEffect->C2

C1.nextEffect ->C2
C2.nextEffect ->B1

A1.lastEffect ->B1

完成顺序:C1,C2,B1,D1,D2,B2,A1

6.2.3 commit阶段

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
function workLoop(deadline){
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (nextUnitOfWork && workInProgressRoot) {
requestIdleCallback(workLoop, {timeout: 500});
}else{
console.log("渲染阶段结束");
// 提交阶段
commitRoot();
}
}
function commitRoot() {
let currentFiber = workInProgressRoot.firstEffect;
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
workInProgressRoot = null;
}
function commitWork(currentFiber) {
if (!currentFiber) {
return;
}
let returnFiber = currentFiber.return;//先获取父Fiber
const domReturn = returnFiber.stateNode;//获取父的DOM节点
if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode != null) {//如果是新增DOM节点
let nextFiber = currentFiber;
domReturn.appendChild(nextFiber.stateNode);
}
currentFiber.effectTag = null;
}

完整版schedule.js

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import {PLACEMENT, ELEMENT_TEXT, TAG_TEXT, TAG_HOST} from './constants';
/**
*从根节点开始渲染和调度
*两个阶段:
一、diff阶段,对比新旧的虚拟DOM,进行增量 更新或创建
render阶段:这个阶段比较花时间,可以对任务进行拆分,拆分的维度就是虚拟DOM节点,一个虚拟DOM节点对应一个任务,此阶段可以暂停,成果是effect list知道哪些节点删除了 增加了
render阶段有两个任务:1.根据虚拟DOM生成fiber树;2.收集effect list
二、commit阶段: 进行DOM更新创建阶段,此阶段不能暂停,要一气呵成
*/
let nextUnitOfWork = null//下一个工作单元
let workInProgressRoot = null;//正在渲染中的根Fiber
export function scheduleRoot(rootFiber){ // {tag: TAG_ROOT, stateNode: container, props:{ children: [element] } }
// 大致原理:根据 element虚拟DOM 创建真实DOM插入到容器 container 中
//把当前树设置为nextUnitOfWork开始进行调度
workInProgressRoot = rootFiber;
nextUnitOfWork = rootFiber;
}
function performUnitOfWork(currentFiber) {
beginWork(currentFiber);//开始渲染前的Fiber,就是把子元素变成子fiber

if (currentFiber.child) {//如果子节点就返回第一个子节点
return currentFiber.child;
}
while (currentFiber) {//如果没有子节点说明当前节点已经完成了渲染工作
completeUnitOfWork(currentFiber);//可以结束此fiber的渲染了
if (currentFiber.sibling) {//如果它有弟弟就返回弟弟
return currentFiber.sibling;
}
currentFiber = currentFiber.return;//如果没有弟弟让爸爸完成,然后找叔叔
}
}
// 在完成的时候要收集有副作用的fiber,然后组成effect list
// 每个fiber有两个属性,一个firstEffect指向第一个有副作用的子fiber lastEffect指向最后一个有副作用的子fiber
// 中间的用nextEffect做成一个单链表 firstEffect->大儿子,大儿子.nextEffect->二儿子,二儿子.nextEffect->三儿子,lastEffect也指向三儿子
function completeUnitOfWork(currentFiber) {// 第一个完成的A1(TEXT)
const returnFiber = currentFiber.return; //找它爹A1
if (returnFiber) {
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect;
}
if (!!currentFiber.lastEffect) {
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
}
returnFiber.lastEffect = currentFiber.lastEffect;
}

const effectTag = currentFiber.effectTag;
if (effectTag) { // 如果有值,有副作用
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber;
} else {
returnFiber.firstEffect = currentFiber;
}
returnFiber.lastEffect = currentFiber;
}
}
}

/**
*beginwork 开始收下线的钱
*completeUnitOfWork 把下线的钱收完了
* @param {*} currentFiber
* 1. 创建真实DOM元素
* 2. 创建子fiber
*/
function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) {//如果是根节点
updateHostRoot(currentFiber);
} else if (currentFiber.tag === TAG_TEXT) {//如果是原生文本节点
updateHostText(currentFiber);
} else if (currentFiber.tag === TAG_HOST) {//如果是原生DOM节点
updateHostComponent(currentFiber);
}
}
//如果是原生DOM节点
function updateHostComponent(currentFiber) {//如果是原生DOM节点
if (!currentFiber.stateNode) {// 如果此fiber没有创建DOM节点
currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点
}
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}
//如果是原生文本节点
function updateHostText(currentFiber) {
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点
}
}
function createDOM(currentFiber) {
if (currentFiber.type === ELEMENT_TEXT) { // 如果是文本节点创建文本节点
return document.createTextNode(currentFiber.props.text);
}else if(currentFiber.type === TAG_HOST){
const stateNode = document.createElement(currentFiber.type);
updateDOM(stateNode, {}, currentFiber.props);
return stateNode;
}

}
function updateDOM(stateNode, oldProps, newProps) {
setProps(stateNode, oldProps, newProps);
}

function updateHostRoot(currentFiber) {//如果是根节点
const newChildren = currentFiber.props.children;//直接渲染子节点
reconcileChildren(currentFiber, newChildren);
}
function reconcileChildren(currentFiber, newChildren){// 根据虚拟DOM节点 创建子fiber
let newChildIndex = 0;// 新子节点的索引
let prevSibling; // 上一个新的子fiber
// 遍历子虚拟DOM数组 虚拟DOM转化为fiber
while (newChildIndex < newChildren.length) {
const newChild = newChildren[newChildIndex]; // 取出虚拟DOM节点
let tag;
if (newChild && newChild.type === ELEMENT_TEXT) {
tag = TAG_TEXT;//文本
} else if (newChild && typeof newChild.type === 'string') {
tag = TAG_HOST;//如果type是字符串,那他是原生DOM节点
}
let newFiber = {
tag,//原生DOM组件 TAG_HOST
type: newChild.type,//具体的元素类型
props: newChild.props,//新的属性对象
stateNode: null,//stateNode肯定是空的,因为这个时候div还没有创建DOM元素
return: currentFiber,//父Fiber
effectTag: PLACEMENT,//副作用标识 增加节点 副作用标识,在render阶段我们会收集副作用,有增加 删除 更新
nextEffect: null // effect list也是一个单链表 nextEffect指向下一个节点
// effect list的顺序和完成顺序是一样的 但是只放出钱的fiber即有副作用的fiber
}// 在beginWork创建fiber 在completeUnitOfWork的时候收集effect
if (newFiber) {
if (newChildIndex === 0) {
currentFiber.child = newFiber;//第一个子节点挂到父节点的child属性上
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;//然后newFiber变成了上一个哥哥了
}
newChildIndex++;
}
}
function workLoop(deadline){
let shouldYield = false; // 是否要让出时间片或控制权
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);//执行一个任务并返回下一个任务
shouldYield = deadline.timeRemaining() < 1;//如果剩余时间小于1毫秒就说明没有时间了,需要把控制权让给浏览器
}
//如果没有下一个执行单元了,并且当前渲染树存在,则进行提交阶段
if (nextUnitOfWork && workInProgressRoot) { // 如果时间片到期后还有任务没完成,就需要请求浏览器再次调度
requestIdleCallback(workLoop, {timeout: 500});
}else{
console.log("渲染阶段结束");
// 提交阶段
commitRoot();
}
}
function commitRoot() {
let currentFiber = workInProgressRoot.firstEffect;
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
workInProgressRoot = null;
}
function commitWork(currentFiber) {
if (!currentFiber) {
return;
}
let returnFiber = currentFiber.return;//先获取父Fiber
const domReturn = returnFiber.stateNode;//获取父的DOM节点
if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode != null) {//如果是新增DOM节点
let nextFiber = currentFiber;
domReturn.appendChild(nextFiber.stateNode);
}
currentFiber.effectTag = null;
}

// react告诉浏览器,我现在有任务,请你在闲的时候执行
// 这里有一个优先级的概念 expirationTime
requestIdleCallback(workLoop, {timeout: 500});

6.3 实现元素的更新

currentRoot 当前根
workInProgressRoot 正在工作的根

public\index.html

1
2
3
4
5
<body>
<div id="root"></div>
<button id="reRender1">reRender1</button>
<button id="reRender2">reRender2</button>
</body>

src/index.js

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import React from './react';
import ReactDOM from './react-dom';
let style = { border: '3px solid red', margin: '5px' };
let element = (
<div id="A1" style={style}>
A1
<div id="B1" style={style}>
B1
<div id="C1" style={style}>C1</div>
<div id="C2" style={style}>C2</div>
</div>
<div id="B2" style={style}>B2</div>
</div>
)
console.log(element);
ReactDOM.render(
element,
document.getElementById('root')
);

+let reRender2 = document.getElementById('reRender1');
+reRender2.addEventListener('click', () => {
+ let element2 = (
+ <div id="A1-new" style={style}>
+ A1-new
+ <div id="B1-new" style={style}>
+ B1-new
+ <div id="C1-new" style={style}>C1-new</div>
+ <div id="C2-new" style={style}>C2-new</div>
+ </div>
+ <div id="B2" style={style}>B2</div>
+ <div id="B3" style={style}>B3</div>
+ </div>
+ )
+ ReactDOM.render(
+ element2,
+ document.getElementById('root')
+ );
+});
+
+let reRender3 = document.getElementById('reRender2');
+reRender3.addEventListener('click', () => {
+ let element3 = (
+ <div id="A1-new2" style={style}>
+ A1-new2
+ <div id="B1-new2" style={style}>
+ B1-new2
+ <div id="C1-new2" style={style}>C1-new2</div>
+ <div id="C2-new2" style={style}>C2-new2</div>
+ </div>
+ <div id="B2" style={style}>B2</div>
+ </div>
+ )
+ ReactDOM.render(
+ element3,
+ document.getElementById('root')
+ );
+});

原理解析

第一次渲染 workInProgressRoot,渲染完成

1
2
3
4
5
6
7
8
9
10
// schedule.js
function commitRoot() {
let currentFiber = workInProgressRoot.firstEffect;
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
+ currentRoot = workInProgressRoot; // 把当前渲染成功的根fiber给 currentRoot
workInProgressRoot = null;
}

将 workInProgressRoot 置为空

每次更新的时候都拿虚拟DOM节点和上次的fiber节点进行对比

奇数更新

1
2
3
4
5
6
7
8
9
export function scheduleRoot(rootFiber){
if (currentRoot) {//奇数次更新 说明有值,已经渲染过了
+ rootFiber.alternate = currentRoot;
+ workInProgressRoot = rootFiber;
+ } else {
+ workInProgressRoot = rootFiber;//第一次渲染
+ }
nextUnitOfWork = rootFiber;
}

完整代码schedule.js

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import { setProps } from './utils';
import {
ELEMENT_TEXT, TAG_ROOT, TAG_HOST, TAG_TEXT, PLACEMENT, DELETION, UPDATE
} from './constants';
+let currentRoot = null;//当前的根Fiber
let workInProgressRoot = null;//正在渲染中的根Fiber
let nextUnitOfWork = null//下一个工作单元
+let deletions = [];//要删除的fiber节点

export function scheduleRoot(rootFiber) {
//{tag:TAG_ROOT,stateNode:container,props: { children: [element] }}
+ if (currentRoot && currentRoot.alternate) {//偶数次更新
+ workInProgressRoot = currentRoot.alternate;
+ workInProgressRoot.firstEffect = workInProgressRoot.lastEffect = workInProgressRoot.nextEffect = null;
+ workInProgressRoot.props = rootFiber.props;
+ workInProgressRoot.alternate = currentRoot;
+ } else if (currentRoot) {//奇数次更新
+ rootFiber.alternate = currentRoot;
+ workInProgressRoot = rootFiber;
+ } else {
+ workInProgressRoot = rootFiber;//第一次渲染
+ }
nextUnitOfWork = workInProgressRoot;
}

function commitRoot() {
+ deletions.forEach(commitWork);
let currentFiber = workInProgressRoot.firstEffect;
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
+ deletions.length = 0;//先把要删除的节点清空掉
+ currentRoot = workInProgressRoot;
workInProgressRoot = null;
}
function commitWork(currentFiber) {
if (!currentFiber) {
return;
}
let returnFiber = currentFiber.return;//先获取父Fiber
const domReturn = returnFiber.stateNode;//获取父的DOM节点
if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode != null) {//如果是新增DOM节点
let nextFiber = currentFiber;
domReturn.appendChild(nextFiber.stateNode);
+ } else if (currentFiber.effectTag === DELETION) {//如果是删除则删除并返回
+ domReturn.removeChild(currentFiber.stateNode);
+ } else if (currentFiber.effectTag === UPDATE && currentFiber.stateNode != null) {//如果是更新
+ if (currentFiber.type === ELEMENT_TEXT) {
+ if (currentFiber.alternate.props.text != currentFiber.props.text) {
+ currentFiber.stateNode.textContent = currentFiber.props.text;
+ }
+ } else {
+ updateDOM(currentFiber.stateNode, currentFiber.alternate.props, currentFiber.props);
+ }
+ }
currentFiber.effectTag = null;
}

function performUnitOfWork(currentFiber) {
beginWork(currentFiber);//开始渲染前的Fiber,就是把子元素变成子fiber

if (currentFiber.child) {//如果子节点就返回第一个子节点
return currentFiber.child;
}

while (currentFiber) {//如果没有子节点说明当前节点已经完成了渲染工作
completeUnitOfWork(currentFiber);//可以结束此fiber的渲染了
if (currentFiber.sibling) {//如果它有弟弟就返回弟弟
return currentFiber.sibling;
}
currentFiber = currentFiber.return;//如果没有弟弟让爸爸完成,然后找叔叔
}
}

function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) {//如果是根节点
updateHostRoot(currentFiber);
} else if (currentFiber.tag === TAG_TEXT) {//如果是原生文本节点
updateHostText(currentFiber);
} else if (currentFiber.tag === TAG_HOST) {//如果是原生DOM节点
updateHostComponent(currentFiber);
}
}
function updateHostRoot(currentFiber) {//如果是根节点
const newChildren = currentFiber.props.children;//直接渲染子节点
reconcileChildren(currentFiber, newChildren);
}
function updateHostText(currentFiber) {
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点
}
}
function updateHostComponent(currentFiber) {//如果是原生DOM节点
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点
}
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}
function createDOM(currentFiber) {
if (currentFiber.type === ELEMENT_TEXT) {
return document.createTextNode(currentFiber.props.text);
}
const stateNode = document.createElement(currentFiber.type);
updateDOM(stateNode, {}, currentFiber.props);
return stateNode;
}

function reconcileChildren(currentFiber, newChildren) {
let newChildIndex = 0;//新虚拟DOM数组中的索引
+ let oldFiber = currentFiber.alternate && currentFiber.alternate.child;//父Fiber中的第一个子Fiber
+ let prevSibling;
+ while (newChildIndex < newChildren.length || oldFiber) {
+ const newChild = newChildren[newChildIndex];
+ let newFiber;
+ const sameType = oldFiber && newChild && newChild.type === oldFiber.type;//新旧都有,并且元素类型一样
+ let tag;
+ if (newChild && newChild.type === ELEMENT_TEXT) {
+ tag = TAG_TEXT;//文本
+ } else if (newChild && typeof newChild.type === 'string') {
+ tag = TAG_HOST;//原生DOM组件
+ }
+ if (sameType) {
+ if (oldFiber.alternate) {
+ newFiber = oldFiber.alternate;
+ newFiber.props = newChild.props;
+ newFiber.alternate = oldFiber;
+ newFiber.effectTag = UPDATE;
+ newFiber.nextEffect = null;
+ } else {
+ newFiber = {
+ tag:oldFiber.tag,//标记Fiber类型,例如是函数组件或者原生组件
+ type: oldFiber.type,//具体的元素类型
+ props: newChild.props,//新的属性对象
+ stateNode: oldFiber.stateNode,//原生组件的话就存放DOM节点,类组件的话是类组件实例,函数组件的话为空,因为没有实例
+ return: currentFiber,//父Fiber
+ alternate: oldFiber,//上一个Fiber 指向旧树中的节点
+ effectTag: UPDATE,//副作用标识
+ nextEffect: null //React 同样使用链表来将所有有副作用的Fiber连接起来
+ }
+ }
+ } else {
+ if (newChild) {//类型不一样,创建新的Fiber,旧的不复用了
+ newFiber = {
+ tag,//原生DOM组件
+ type: newChild.type,//具体的元素类型
+ props: newChild.props,//新的属性对象
+ stateNode: null,//stateNode肯定是空的
+ return: currentFiber,//父Fiber
+ effectTag: PLACEMENT//副作用标识
+ }
+ }
+ if (oldFiber) {
+ oldFiber.effectTag = DELETION;
+ deletions.push(oldFiber);
+ }
+ }
+ if (oldFiber) { //比较完一个元素了,老Fiber向后移动1位
+ oldFiber = oldFiber.sibling;
+ }
if (newFiber) {
if (newChildIndex === 0) {
currentFiber.child = newFiber;//第一个子节点挂到父节点的child属性上
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;//然后newFiber变成了上一个哥哥了
}
prevSibling = newFiber;//然后newFiber变成了上一个哥哥了
newChildIndex++;
}
}

function updateDOM(stateNode, oldProps, newProps) {
setProps(stateNode, oldProps, newProps);
}
function completeUnitOfWork(currentFiber) {
const returnFiber = currentFiber.return;
if (returnFiber) {
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect;
}
if (!!currentFiber.lastEffect) {
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
}
returnFiber.lastEffect = currentFiber.lastEffect;
}

const effectTag = currentFiber.effectTag;
if (effectTag) {
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber;
} else {
returnFiber.firstEffect = currentFiber;
}
returnFiber.lastEffect = currentFiber;
}
}
}

function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);//执行一个任务并返回下一个任务
shouldYield = deadline.timeRemaining() < 1;//如果剩余时间小于1毫秒就说明没有时间了,需要把控制权让给浏览器
}
//如果没有下一个执行单元了,并且当前渲染树存在,则进行提交阶段
if (!nextUnitOfWork && workInProgressRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
//开始在空闲时间执行workLoop
requestIdleCallback(workLoop);

6.4 实现类组件

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from './react';
import ReactDOM from './react-dom';
class ClassCounter extends React.Component {
constructor(props) {
super(props);
this.state = { number: 0 };
}
onClick = () => {
this.setState(state => ({ number: state.number 1 }));
}
render() {
return (
<div id="counter">
<span>{this.state.number}</span>
<button onClick={this.onClick}>加1</button>
</div >
)
}
}
ReactDOM.render(
<ClassCounter />,
document.getElementById('root')
);

src/react.js

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
import { ELEMENT_TEXT } from './constants';
+import { Update, UpdateQueue } from './updateQueue';
+import { scheduleRoot } from './scheduler';
function createElement(type, config, ...children) {
delete config.__self;
delete config.__source;
return {
type,
props: {
...config,
children: children.map(
child => typeof child === "object" ?
child :
{ type: ELEMENT_TEXT, props: { text: child, children: [] } })
}
}
}
+class Component {
+ constructor(props) {
+ this.props = props;
+ this.updateQueue = new UpdateQueue();
+ }
+ setState(payload) {// 可能是对象,也可能是一个函数
// 源码中updateQueue其实是此类组件对应的fiber节点的
+ // this.internalFiber.updateQueue.enqueueUpdate(new Update(payload));
+ this.updateQueue.enqueueUpdate(new Update(payload));
+ scheduleRoot();// 从根节点开始调度
+ }
+}
+Component.prototype.isReactComponent = {}; // 说明是类组件
let React = {
createElement,
+ Component
}
export default React;

新建src/updateQueue.js 更新队列

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
export class Update {
constructor(payload) {
this.payload = payload;
}
}
// 数据结构是一个单链表
export class UpdateQueue {
constructor() {
this.firstUpdate = null;
this.lastUpdate = null;
}
enqueueUpdate(update) {
if (this.lastUpdate === null) {
this.firstUpdate = this.lastUpdate = update;
} else {
this.lastUpdate.nextUpdate = update;
this.lastUpdate = update;
}
}
forceUpdate(state) {
let currentUpdate = this.firstUpdate;
while (currentUpdate) {
state = typeof currentUpdate.payload == 'function' ? currentUpdate.payload(state) : currentUpdate.payload;
currentUpdate = currentUpdate.nextUpdate;
}
this.firstUpdate = this.lastUpdate = null;
return state;
}
}

6.5 实现hooks

useState是语法糖,是基于useReducer实现的

6.5.1 useReducer

src/index.js

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
import React from './react';
import ReactDOM from './react-dom';

function reducer(state, action) {
switch (action.type) {
case 'ADD':
return { count: state.count + 1 };
default:
return state;
}
}
function FunctionCounter() {
const [countState, dispatch] = React.useReducer(reducer, { count: 0 }); // hooks索引为0
// const [countState, dispatch] = React.useReducer(reducer, { count: 0 }); // hooks索引为1
return (
<div>
<h1 onClick={() => dispatch({ type: 'ADD' })}>
Count: {countState.count}
</h1 >
</div>
)
}
ReactDOM.render(
<FunctionCounter />,
document.getElementById('root')
);

src/schedule.js

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
44
45
import { setProps, deepEquals } from './utils';
+import { UpdateQueue, Update } from './updateQueue';
import {
ELEMENT_TEXT, TAG_ROOT, TAG_HOST, TAG_TEXT, TAG_CLASS, TAG_FUNCTION, PLACEMENT, DELETION, UPDATE
} from './constants';
let currentRoot = null; //当前的根Fiber
let workInProgressRoot = null; //正在渲染中的根Fiber
let nextUnitOfWork = null; //下一个工作单元
let deletions = []; //要删除的fiber节点
+let workInProgressFiber = null; //正在工作中的fiber
+let hookIndex = 0; //hook索引


function updateFunctionComponent(currentFiber) {
+ workInProgressFiber = currentFiber;
+ hookIndex = 0;
+ workInProgressFiber.hooks = [];
const newChildren = [currentFiber.type(currentFiber.props)];
reconcileChildren(currentFiber, newChildren);
}
+export function useReducer(reducer, initialValue) {
// 为什么函数组件重新渲染 const [countState, dispatch] = React.useReducer(reducer, { count: 0 }); 里的状态拿的是上一次的状态
// workInProgressFiber.alternate.hooks[hookIndex] 拿到的是当前工作中的fiber的上一个fiber的当前这个索引hookIndex对应的hook
+ let newHook =
+ workInProgressFiber.alternate &&
+ workInProgressFiber.alternate.hooks &&
+ workInProgressFiber.alternate.hooks[hookIndex];
+ if (newHook) {// 拿上一个forceUpdate(newHook.state)hook的状态去更新 得到新的状态
+ newHook.state = newHook.updateQueue.forceUpdate(newHook.state);
+ } else { // 第一次渲染
+ newHook = {
+ state: initialValue,
+ updateQueue: new UpdateQueue() // 空的更新队列
+ };
+ }
+ const dispatch = action => { // {type:'ADD'}
+ let payload = reducer ? reducer(newHook.state, action) : action;// reducer有值,返回reducer()后的新对象,没有返回action
+ newHook.updateQueue.enqueueUpdate( // 增加update对象
+ new Update(payload)
+ );
+ scheduleRoot();
+ }
+ workInProgressFiber.hooks[hookIndex++] = newHook;
+ return [newHook.state, dispatch];
+}

每个fiber都有自己的hook,每个hook都有自己的state,updateQueue
这里hook是通过索引hookIndex来取上一个hook的,所以hook的数量和顺序不能改变,这样前后两次比较就会出错

6.5.2 useState

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function FunctionCounter() {
const [countState, useStateCount] = React.useState({ count: 0 });
return (
<div>
<h1 onClick={useStateCount(count+1)}>
Count: {countState.count}
</h1 >
</div>
)
}
ReactDOM.render(
<FunctionCounter />,
document.getElementById('root')
);

src/schedule.js

1
2
3
+export function useState(initState) {
+ return useReducer(null, initState)
+}

相关文章

http://www.zhufengpeixun.cn/2020/html/96.2.fiber.html