forked from neroneroffy/react-source-code-debug
-
Notifications
You must be signed in to change notification settings - Fork 0
/
test.html
312 lines (274 loc) · 11.2 KB
/
test.html
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Frame</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<style>
#animation {
width: 30px;
height: 30px;
background: red;
animation: myfirst 3s infinite alternate;
}
@keyframes myfirst {
from {
width: 30px;
height: 30px;
border-radius: 0;
background: red;
}
to {
width: 300px;
height: 300px;
border-radius: 50%;
background: yellow;
}
}
</style>
</head>
<body>
<div style="height: 500px;">
<div id="animation">test</div>
</div>
<button id="schedule">
schedule
</button>
<script>
// *平均用时 2ms
function work() {
const start = Date.now();
while (Date.now() - start < 2) {
}
}
function createWorks(length) {
return new Array(length).fill(0).map(() => {
return work;
})
}
const works = createWorks(3000);
const yieldInterval = 5;
let deadLine = 0;
const channel = new MessageChannel();
let port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadLine;
let scheduledHostCallback = null;
let taskQueue = [];
let isHostCallbackScheduled = false;
function performWorkUntilDeadLine() {
if (scheduledHostCallback) {
const currentTime = Date.now();
deadLine = currentTime + yieldInterval;
const hasMoreWork = scheduledHostCallback(currentTime);
if (!hasMoreWork) {
scheduledHostCallback = null
} else {
port.postMessage(null);
}
}
}
function requestHostCallback(callback) {
scheduledHostCallback = callback;
port.postMessage(null);
}
// *schedule 使用的是最小堆,我这里就直接使用 sort 了
function push(queue, task) {
queue.push(task);
queue.sort((a, b) => {
return a.sortIndex - b.sortIndex;
})
}
let taskIdCounter = 1;
const ImmediatePriority = 1;
const UserBlockingPriority = 2;
const NormalPriority = 3;
const LowerPriority = 4;
const IdlePriority = 5;
const IMMEDIATE_PRIORITY_TIMEOUT = -1;
const USER_BLOCKING_PRIORITY_TIMEOUT = 250;
const NORMAL_PRIORITY_TIMEOUT = 5000;
const LOWER_PRIORITY_TIMEOUT = 10000;
const IDLE_PRIORITY_TIMEOUT = 1073741823;// Math.pow(2, 30) - 1
const timerQueue = [];
// *标志是否有计时器在进行
let isHostTimeoutScheduled = false;
function scheduleCallback(priorityLevel, callback, options) {
const currentTime = Date.now();
let startTime;
if (typeof options === 'object' && options !== null) {
const delay = options.delay;
if (typeof delay === 'number' && delay > 0) startTime = startTime + options.delay;
else startTime = currentTime;
} else startTime = currentTime;
let timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case LowerPriority:
timeout = LOWER_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
const expiredTime = startTime + timeout;
let newTask = {
id: taskIdCounter++,
startTime,
callback: callback,
priorityLevel,
expiredTime,
sortIndex: -1
}
// *如果开始的时间 > 大于当前时间,那么说明该任务是一个延迟任务
if (startTime > currentTime) {
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// !既然有延迟任务,那么在延迟完成后,应该将该任务从 timerQueue 转移到 taskQueue,于是需要一个定时器,用来执行这个操作;
// !Schedule中 即使有很多个延迟任务,但是只会使用一个 setTimeout 这样会有效降低复杂性,schedule 中使用 requestHostTimeout 请求定时器
// !并且每次 taskQueue 中的一个任务完成后都会去检查 timerQueue 中的是否有任务延迟完成;
// *因为每次 taskQueue 中的任务完成后都会去检查 timerQueue, 所以如果 taskQueue 里面如果有任务话,就不需要请求定时器了;所以这里需要判断 taskQueue 是否为空
if (taskQueue.length === 0 && timerQueue[0] === newTask) {
// *如果已经有一个定时器了,那么应该取消这个定时器;这是因为上面已经检查了此时的 newTask 才是最紧急的延迟任务;所以应该取消上一个定时器,然后请求新的定时器
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expiredTime;
// *使用 push 方法可以每次添加新任务时都进行排序,高优先级任务插队就是这样的插的;
push(taskQueue, newTask);
}
if (!isHostCallbackScheduled) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
return newTask;
}
let taskTimeoutId = -1;
function requestHostTimeout(handle, ms) {
taskTimeoutId = setTimeout(() => {
handle(Date.now());
}, ms)
}
function cancelHostTimeout() {
clearTimeout(taskTimeoutId);
taskTimeoutId = -1;
}
// *延迟完成后的回调,那么最主要的任务就是把 timerQueue 中延迟完成的任务转移到 taskQueue
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime);
// *此时已经将 timerQueue 中延迟完成的任务转移到 taskQueue 了
if (!isHostCallbackScheduled) {// isHostCallbackScheduled 代表着 taskQueue 原先就有任务,并且正在调度
if (taskQueue.length) {
// *所以,这里表示没有在调度中,那么应该进入调度状态
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else {
const timer = timerQueue[0];
if (timer) {
// *如果没有任务了,那么应该继续请求定时器;
// ?为什么这里没有将 isHostTimeoutScheduled 置为 true 啊?
// !注意这里并没有将 isHostTimeoutScheduled 设置为 true;并不是 bug 而是开发团队有意为之,因为此时根本不需要设置为 true 了
// !isHostTimeoutScheduled 的目的是为了防止请求多个 setTimeout;目前请求 setTimeout 的地方有三个 scheduleCallback, handleTimeout, workLoop 中;
// !其中 handleTimeout 和 workLoop 都没有将 isHostTimeoutScheduled 设置为 true
// !handleTimeout 就是 setTimeout 的回调函数;也就是说如果只请求了一个 setTimeout 那么就只会有一个 handleTimeout,如果只有一个 handleTimeout 那么 handleTimeout 内部也就只会请求一个 setTimeout;所以要从另外两个函数下手;
// !在 workLoop 中当 taskQueue 为空,并且 timerQueue 不会空时将会请求一个 setTimeout;在 scheduleCallback 中如果 taskQueue 为空,并且 timerQueue 的堆顶等于 newTask 的话,会请求一个 setTimeout;
// !假设 workLoop 中请求了一个 setTimeout,这个很好实现,在一个任务的回调中再调度一个延迟任务即可;此时我们只需要在 scheduleCallback 中再次请求一个 setTimeout 就可以证明这里的错误;
// !我们分为两种情况使用 scheduleCallback
// !1. 直接使用 scheduleCallback 此时在 scheduleCallback 中会将 isHostTimeoutScheduled 赋值为 true 防止请求多个 setTimeout
// !2. 在 scheduleCallback 的回调中再进行调度;在回调中进行调度就相当于在 workLoop 中调用 scheduleCallback,而 workLoop 是执行完回调之后,再 taskQueue.pop() 的,所以此时的 taskQueue 是有值,在 scheduleCallback 中无法请求 setTimeout
// !所以在 handleTimeout 和 workLoop 中无需将 isHostTimeoutScheduled 设置为 true;但是如果不会影响实际效果的话,我还是喜欢设置为 true 更容易理解一些
requestHostTimeout(handleTimeout, timer.startTime - currentTime);
}
}
}
}
// *该函数就负责检查 timerQueue 中的任务并进行转移到 taskQueue
function advanceTimers(currentTime) {
let currentTimer = timerQueue[0];
while (currentTimer) {
// *如果这个 timer 被取消掉了,那么 callback 会为 null
if (currentTimer.callback === null) timerQueue.shift();
else if (currentTimer.startTime <= currentTime) {// 如果开始时间 < 当前时间 那么说明已经延迟完成了
currentTimer.sortIndex = currentTimer.expiredTime;
push(taskQueue, currentTimer);
timerQueue.shift();
} else return;// *如果上面两个条件都不符合,那么说明当前的 timer 延迟还没有完成;如果堆顶的延迟都还没有完成,那么其他的也就没有完成;所以直接返回
currentTimer = timerQueue[0];
}
}
let currentTask = null;
function flushWork(initialTime) {
// 在 workLoop 中将会检查 timerQueue 里的任务,所以这里要清除 setTimeout
if (isHostTimeoutScheduled) {
cancelHostTimeout();
}
return workLoop(initialTime)
}
// *添加了优先级后,workLoop 应该判断当前任务是否过期,如果过期了,即使应该 yield 也要继续将该任务执行完成;
function workLoop(initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = taskQueue[0];
while (currentTask) {
const isExpired = currentTime > currentTask.expiredTime;
if (!isExpired && shouldYield()) {
break;
}
const callback = currentTask.callback;
// !注意:此次引入了优先级的概念,如果在 callback 中调度了高优先级的任务,那么就会插入到堆顶,需要对这种情况进行处理;
if (typeof callback === 'function') {
currentTask.callback = null;
const continuationCallback = callback(isExpired);
// *callback 执行完成后,应该更新 currentTime 判断后续 task 是否过期将会不准确
currentTime = Date.now();
if (currentTask === taskQueue[0]) {
taskQueue.shift();
}
advanceTimers(currentTime);
} else {
taskQueue.shift();
}
currentTask = taskQueue[0];
}
if (currentTask) {
return true;
} else {
isHostCallbackScheduled = false;
const firstTimer = timerQueue[0];
if (firstTimer) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
const btn = document.getElementById("schedule");
const div = document.getElementById("animation");
btn.addEventListener('click', () => {
const last = works.length - 1;
const start = Date.now();
works.forEach((work, i) => {
scheduleCallback(() => {
if (i === last) {
work();
div.textContent = `${Date.now() - start}ms`;
} else work();
})
})
})
</script>
</body>
</html>