fiber以及concurrent mode

  首先感谢司徒正美大佬写的文章,让我大致了解了fiber是个什么东西。(愿天堂没有996 T_T)这里推荐两篇大佬的文章React Fiber架构React Fiber的优先级调度机制与事件系统,第一篇讲了fiber行为,第二篇讲解了fiber事件分片的机制

  先康一康react官方对于concurrent的介绍,其实就是当页面元素过多的时候,每次更改涉及到的元素过多时,由于原算法是深度遍历v-dom去修改状态,当遍历花费时间过多时会造成页面卡顿。而fiber架构允许遍历被打断去响应高优先级操作,让页面更加流畅。具体demo可以从以下例子中看到。https://claudiopro.github.io/react-fiber-vs-stack-demo/。

  为了让每一个节点都有能力去遍历整棵树,fiber设计了三个属性return child sibling,三者分别对应该节点的父节点、子节点、右侧兄弟节点。这样的话我们每次拿到一个节点都有能力去获得发生变化的根节点、访问到未被更新过的节点(如果是树状结构,每一个节点就只能访问到其子节点了)。fiber结构可以满足先前的所有特性,同时还可以衍生出current mode的特性,所以现在react的v-dom已经是fiber结构了(react 16开始)

  这里首先明确一个点,fiber并不能提高代码执行效率,相反,fiber降低了执行效率,但是由于某些高优先级事件会优先执行,所以它只是优化了用户体验,真正从diff开始到渲染结束花的时间时更长的。具体可以参考上述React Fiber架构文章内的例子,分批次渲染页面可以让页面提前显示出来(常说的首屏响应时间)。

  具体如何在diff过程中停止呢,react设计了多个优先级,优先级有对应值,这个对应值就是这个fiber的更新延迟时间,最快的值为-1,就是立即更新,最慢的10000,最佳计时api其实是,requestIdleCallback但是这个api兼容性不行,而且它是个浏览器api,react要支持server render是不能用这个api的,所以react源码定义了一个requestHostTimeout。本质上是requestIdleCallback的polyfill,兼容node和不同浏览器用(其实本质还是用的setTimeout和requestAnimationFrame,现在已经不用requestAnimationFrame了使用的messageChannel,具体原因可以看以下commit),似乎是考虑到硬件刷新率不同而做的更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 优先级定义如下
var maxSigned31BitInt = 1073741823; // Times out immediately

var IMMEDIATE_PRIORITY_TIMEOUT = -1; // Eventually times out

var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000; // Never times out

var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; // Tasks are stored on a min heap



requestHostTimeout = function (callback, ms) {
taskTimeoutID = _setTimeout(function () {
callback(getCurrentTime());
}, ms);
};

  现在如果不考虑如何将需要更新fiber节点添加进入更新队列,而只是单纯的看调度方法的话,主要有如下代码

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

function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = exports.unstable_now();
var startTime;

if (typeof options === 'object' && options !== null) {
var delay = options.delay;

if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}

var timeout;

switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;

case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;

case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;

case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;

case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}

var expirationTime = startTime + timeout;
var newTask = {
id: taskIdCounter++,
callback: callback,
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
sortIndex: -1
};

{
newTask.isQueued = false;
}

if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);

if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
} // Schedule a timeout.


requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);

{
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
} // Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.


if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}

return newTask;
}

var port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;

requestHostCallback = function (callback) {
scheduledHostCallback = callback;

if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};


var performWorkUntilDeadline = function () {
if (scheduledHostCallback !== null) {
var currentTime = exports.unstable_now(); // Yield after `yieldInterval` ms, regardless of where we are in the vsync
// cycle. This means there's always time remaining at the beginning of
// the message event.

deadline = currentTime + yieldInterval;
var hasTimeRemaining = true;

try {
var hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);

if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// If there's more work, schedule the next message event at the end
// of the preceding one.
port.postMessage(null);
}
} catch (error) {
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
} // Yielding to the browser will give it a chance to paint, so we can
};

  这里的主要入口时unstable_scheduleCallback,跑到react-dom里面找这个代码的相关引用,可以直接找到一下调用链commitWork -> commitHydratedContainer -> retryIfBlockedOn -> scheduleCallbackIfUnblocked -> unstable_scheduleCallback。这里就很清晰这个unstable_scheduleCallback就是最终commitWork的实现。

  这里就很容易就能看懂逻辑了!在commitWork之后,执行requestHostCallback,requestHostCallback执行postMessage,然后MessageChannel接收到消息之后继续执行requestHostCallback,一直持续到没有work为止,至于什么时候没有work呢,就是任务队列里面没有任务,或者时间到deadline了,具体是通过requestHostCallback来判断,我们通过这个函数去找,可以找到最终return 的是workLoop函数

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

function workLoop(hasTimeRemaining, initialTime) {
var currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);

while (currentTask !== null && !(enableSchedulerDebugging )) {
if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || exports.unstable_shouldYield())) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}

var callback = currentTask.callback;

if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
var didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
markTaskRun(currentTask, currentTime);
var continuationCallback = callback(didUserCallbackTimeout);
currentTime = exports.unstable_now();

if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
{
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}

if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}

advanceTimers(currentTime);
} else {
pop(taskQueue);
}

currentTask = peek(taskQueue);
} // Return whether there's additional work

  看见了终于看见了,currentTask.expirationTime > currentTime && (!hasTimeRemaining || exports.unstable_shouldYield())这个条件,判断是否时间结束或者是否应该暂停(暂停update也是concurrent mode的一个特性嗷),那具体这个task.expriationTime计算呢,是在unstable_scheduleCallback中,值就是当前时间+优先级的值。

  到此为止,concurrent mode的调度,大致就是这样了,具体关于如何去遍历fiber,什么时候去commitWork,阔以自己看看源码(因为我还没去看这块代码,XD)