-
Notifications
You must be signed in to change notification settings - Fork 80
/
Copy pathschedule_delay.html
334 lines (312 loc) · 11.6 KB
/
schedule_delay.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
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>schedule延迟任务源码</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
/>
<style>
#animation {
display: flex;
align-items: center;
justify-content: center;
width: 100px;
height: 100px;
background: red;
animation: myfirst 5s;
animation-iteration-count: infinite;
}
@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>
<button id="btn">perform work</button>
<div id="animation">Animation</div>
<script>
const btn = document.getElementById("btn");
const animate = document.getElementById("animation");
const sleep = (ms = 2) => {
const start = new Date().getTime();
while (new Date().getTime() - start < ms) {}
};
let startTime;
btn.onclick = () => {
function sleep(ms) {
const start = new Date().getTime();
while (new Date().getTime() - start < ms) {}
}
function printA(didTimeout) {
sleep(2);
console.log("A didTimeout:", didTimeout);
}
function printB(didTimeout) {
sleep(3);
console.log("B didTimeout:", didTimeout);
}
function printC(didTimeout) {
sleep(12);
console.log("C didTimeout:", didTimeout);
}
function printD(didTimeout) {
sleep(3);
console.log("D didTimeout:", didTimeout);
}
scheduleCallback(UserBlockingPriority, printA, { delay: 10 });
scheduleCallback(UserBlockingPriority, printB, { delay: 1000 });
scheduleCallback(UserBlockingPriority, printC);
scheduleCallback(UserBlockingPriority, printD);
};
</script>
<script>
// 将Message Channel触发宏任务事件封装成一个方法requestHostCallback,使用performWorkUntilDeadline监听Message Channel事件
const yieldInterval = 5;
let deadline = 0;
const channel = new MessageChannel();
let port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
function performWorkUntilDeadline() {
console.log("触发了performWorkUntilDeadline执行");
if (scheduledHostCallback) {
// 当前宏任务事件开始执行
let currentTime = new Date().getTime();
// 计算当前宏任务事件结束时间
deadline = currentTime + yieldInterval;
const hasMoreWork = scheduledHostCallback(currentTime);
if (!hasMoreWork) {
scheduledHostCallback = null;
} else {
// 如果还有工作,则触发下一个宏任务事件
port.postMessage(null);
}
}
}
function requestHostCallback(callback) {
scheduledHostCallback = callback;
port.postMessage(null);
}
function shouldYield() {
return new Date().getTime() >= deadline;
}
// 延迟任务相关的api
let taskTimeoutID = -1;
function requestHostTimeout(callback, ms) {
taskTimeoutID = setTimeout(function () {
callback(new Date().getTime());
}, ms);
}
function cancelHostTimeout() {
clearTimeout(taskTimeoutID);
taskTimeoutID = -1;
}
</script>
<script>
// 每次插入一个任务,都需要重新排序以确定新的优先级。就像没插入一个需求,都需要重新按照截止日期排期以确定新的优先级
// 高优先级的任务在前面
// 在react scheduler源码中,采用的是最小堆排序算法。这里为了简化,咱就不那么卷了
function push(queue, task) {
queue.push(task);
queue.sort((a, b) => {
return a.sortIndex - b.sortIndex;
});
}
</script>
<script>
const ImmediatePriority = 1;
const UserBlockingPriority = 2;
const NormalPriority = 3;
const LowPriority = 4;
const IdlePriority = 5;
// 以下过期时间单位都是毫秒
const maxSigned31BitInt = 1073741823; // 最大整数
const IMMEDIATE_PRIORITY_TIMEOUT = -1; // 过期时间-1毫秒,超高优先级,需要立即执行
const USER_BLOCKING_PRIORITY_TIMEOUT = 250;
const NORMAL_PRIORITY_TIMEOUT = 5000;
const LOW_PRIORITY_TIMEOUT = 10000;
const IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; // 永不过期,最低优先级
let taskQueue = []; // 普通任务
let timerQueue = []; // 延迟任务
let currentTask = null;
let isHostCallbackScheduled = false;
let isHostTimeoutScheduled = false;
function flushWork(initialTime) {
if (isHostTimeoutScheduled) {
console.log('取消定时器')
// 如果之前启动过定时器,则取消。因为在workLoop内部每执行一个任务,都会调用advanceTimers将
// timerQueue中到期执行的任务加入到taskQueue中去执行。但taskQueue全部执行完成,
// 如果timerQueue还有工作,此时就会重新启动定时器延迟执行timerQueue中的任务
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
return workLoop(initialTime);
}
function workLoop(initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = taskQueue[0];
while (currentTask) {
if (currentTask.expirationTime > currentTime && shouldYield()) {
// 当前的currentTask还没过期,但是当前宏任务事件已经到达执行的最后期限,即我们需要
// 将控制权交还给浏览器,剩下的任务在下一个事件循环中再继续执行
// console.log("yield");
break;
}
const callback = currentTask.callback;
if (typeof callback === "function") {
currentTask.callback = null;
const didUserCallbackTimeout =
currentTask.expirationTime <= currentTime;
callback(didUserCallbackTimeout);
currentTime = new Date().getTime();
if (currentTask === taskQueue[0]) {
taskQueue.shift();
}
advanceTimers(currentTime);
} else {
taskQueue.shift();
}
currentTask = taskQueue[0];
}
if (currentTask) {
// 如果taskQueue中还有剩余工作,则返回true
return true;
} else {
isHostCallbackScheduled = false;
// 如果taskQueue已经没有工作,同时timerQueue还有工作,则需要启用一个定时器延迟执行
var firstTimer = timerQueue[0];
if (firstTimer) {
console.log(
"taskQueue全部执行完成了,但是timerQueue还有任务,因此启动一个定时器"
);
requestHostTimeout(
handleTimeout,
firstTimer.startTime - currentTime
);
}
return false;
}
}
function scheduleCallback(priorityLevel, callback, options) {
let delay = 0;
if (typeof options === "object" && options !== null) {
delay = options.delay || 0;
}
const startTime = new Date().getTime() + delay;
let 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;
}
// 计算任务的截止时间
const expirationTime = startTime + timeout;
let newTask = {
callback: callback,
priorityLevel,
startTime,
expirationTime: expirationTime,
sortIndex: -1,
};
if (delay) {
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// 如果taskQueue为空,同时新添加的newTask是最早需要执行的延迟任务,则我们需要取消之前的定时器
// 启动一个更早的定时器
if (!taskQueue.length && newTask === timerQueue[0]) {
// 所有的任务都需要执行,但是新添加的这个newTask是最早需要执行的任务,因此我们需要取消之前的定时器
// 重新启动一个更早的定时器
if (isHostTimeoutScheduled) {
// 取消之前的定时器
console.log("取消之前的定时器");
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// 启动一个更早的定时器
// 开启一个settimeout定时器,startTime - currentTime,其实就是options.delay毫秒后执行handleTimeout
console.log("启动一个定时器,delay:", delay);
requestHostTimeout(handleTimeout, delay);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (!isHostCallbackScheduled) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}
// 延迟任务相关的api
// 找出那些到时的不需要再延迟执行的任务,添加到taskQueue中
function advanceTimers(currentTime) {
var timer = timerQueue[0];
while (timer) {
if (timer.callback === null) {
// 任务被取消了
timerQueue.shift();
} else if (timer.startTime <= currentTime) {
// 任务到时了,需要执行,添加到taskQueue调度执行
timerQueue.shift();
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
} else {
// 如果第一个任务都还没到时,说明剩下的都还需要延迟
return;
}
timer = timerQueue[0];
}
}
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime);
// 如果已经触发了一个message channel事件,但是事件还没执行。刚好定时器这时候执行了,就会
// 存在isHostCallbackScheduled为true的情况,此时就没必要再继续里面的逻辑了。因为
// message channel中就会执行这些操作
if (!isHostCallbackScheduled) {
// 如果timerQueue的第一个任务被取消了,则taskQueue可能为null,此时timerQueue后面的任务还是需要延迟执行
if (taskQueue[0]) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else {
var firstTimer = timerQueue[0];
if (firstTimer) {
requestHostTimeout(
handleTimeout,
firstTimer.startTime - currentTime
);
}
}
}
}
</script>
</body>
</html>