@@ -4,3 +4,167 @@ title: "VSCode:Centralized Animation Frame Scheduling."
44categories : [VSCode, UI]
55---
66
7+ ## 简介
8+ 今天介绍一个VSCode源码中一个简单且小巧的功能。在此之前我先简单介绍一下` requestAnimationFrame ` 这个原生API。
9+
10+ ## ` requestAnimationFrame ` API
11+
12+ ### 工作原理
13+ ` requestAnimationFrame ` 会将提供的回调函数放入队列,并在下一次浏览器重绘前调用该函数。与传统的 ` setTimeout ` 不同,` requestAnimationFrame ` 的优势在于:
14+ - ** 与屏幕刷新同步** :浏览器会在适当的时间调用回调函数,通常是每秒 60 帧(即 16.67ms 间隔)。
15+ - ** 节能效果** :当页面处于后台或不可见状态时,浏览器会暂停 ` requestAnimationFrame ` 的调用,从而节省资源。
16+ - ** 平滑的动画** :由于与浏览器刷新周期一致,动画会显得更加流畅。
17+
18+ ### 基本用法
19+ ``` javascript
20+ function animate () {
21+ console .log (' Animating...' );
22+ }
23+ requestAnimationFrame (animate);
24+ ```
25+ ### 问题点
26+ 当应用中的多个模块独立调用 ` requestAnimationFrame ` 时,可能出现以下问题:
27+ 1 . ** 缺乏全局优先级控制** :浏览器无法直接区分任务的优先级,导致关键任务与次要任务并行执行,影响性能。
28+ 2 . ** 复杂任务调度** :对于需要动态更新任务优先级或取消任务的场景,原生 API 支持不足。
29+
30+ 为了解决这些问题,VSCode 设计了一套中央式动画帧调度器来管理动画和 UI 渲染任务。
31+
32+ ## 中央调度器
33+ > 相关文件:` src\vs\base\browser\dom.ts `
34+
35+ ` VSCode ` 整个软件中避免直接使用` window.requestsAnimationFrame ` ,而是提供了以下两个APIs:
36+ ``` ts
37+ /**
38+ * Schedule a callback to be run at the next animation frame.
39+ * This allows multiple parties to register callbacks that should run at the next animation frame.
40+ * If currently in an animation frame, `runner` will be executed immediately.
41+ * @return token that can be used to cancel the scheduled runner (only if `runner` was not executed immediately).
42+ */
43+ export let runAtThisOrScheduleAtNextAnimationFrame: (targetWindow : Window , runner : () => void , priority ? : number ) => IDisposable ;
44+ /**
45+ * Schedule a callback to be run at the next animation frame.
46+ * This allows multiple parties to register callbacks that should run at the next animation frame.
47+ * If currently in an animation frame, `runner` will be executed at the next animation frame.
48+ * @return token that can be used to cancel the scheduled runner.
49+ */
50+ export let scheduleAtNextAnimationFrame: (targetWindow : Window , runner : () => void , priority ? : number ) => IDisposable ;
51+ ```
52+
53+ VSCode把函数的定义写在了一个 immediate call function 里面。因为代码量很少,我会直接把大部分代码复制过来。首先是在 function body 中定义了一些map,用来全局储存数据:
54+ ``` ts
55+ (function () {
56+ // The runners scheduled at the next animation frame
57+ const NEXT_QUEUE = new Map < number /* window ID */ , AnimationFrameQueueItem[]>();
58+ // The runners scheduled at the current animation frame
59+ const CURRENT_QUEUE = new Map < number /* window ID */ , AnimationFrameQueueItem[]>();
60+ // A flag to keep track if the native requestAnimationFrame was already called
61+ const animFrameRequested = new Map < number /* window ID */ , boolean>();
62+ // A flag to indicate if currently handling a native requestAnimationFrame callback
63+ const inAnimationFrameRunner = new Map < number /* window ID */ , boolean>();
64+
65+ // ...
66+ })();
67+ ```
68+
69+ 而` runAtThisOrScheduleAtNextAnimationFrame ` 和` scheduleAtNextAnimationFrame ` 的定义如下:
70+ ``` ts
71+ (function () {
72+ // ...
73+ scheduleAtNextAnimationFrame = (targetWindow : Window , runner : () => void , priority : number = 0 ) => {
74+ const targetWindowId = getWindowId (targetWindow );
75+ const item = new AnimationFrameQueueItem (runner , priority );
76+ let nextQueue = NEXT_QUEUE .get (targetWindowId );
77+ if (! nextQueue ) {
78+ nextQueue = [];
79+ NEXT_QUEUE .set (targetWindowId , nextQueue );
80+ }
81+ nextQueue .push (item );
82+ if (! animFrameRequested .get (targetWindowId )) {
83+ animFrameRequested .set (targetWindowId , true );
84+ targetWindow .requestAnimationFrame (() => animationFrameRunner (targetWindowId ));
85+ }
86+ return item ;
87+ };
88+
89+ runAtThisOrScheduleAtNextAnimationFrame = (targetWindow : Window , runner : () => void , priority ? : number ) => {
90+ const targetWindowId = getWindowId (targetWindow );
91+ if (inAnimationFrameRunner .get (targetWindowId )) {
92+ const item = new AnimationFrameQueueItem (runner , priority );
93+ let currentQueue = CURRENT_QUEUE .get (targetWindowId );
94+ if (! currentQueue ) {
95+ currentQueue = [];
96+ CURRENT_QUEUE .set (targetWindowId , currentQueue );
97+ }
98+ currentQueue .push (item );
99+ return item ;
100+ } else {
101+ return scheduleAtNextAnimationFrame (targetWindow , runner , priority );
102+ }
103+ };
104+ })();
105+ ```
106+ * ` AnimationFrameQueueItem ` 是任务的基本单位。每个任务都被封装成一个实例,包含了以下信息:
107+ * ** 任务逻辑** (` runner ` ):需要执行的具体函数。
108+ * ** 优先级** (` priority ` ):用于控制任务执行的顺序。
109+ * ** 取消标志** (` _canceled ` ):支持任务的动态取消。
110+ ``` typescript
111+ class AnimationFrameQueueItem {
112+ private _runner: () => void ;
113+ public priority: number ;
114+ private _canceled: boolean ;
115+
116+ constructor (runner : () => void , priority : number = 0 ) {
117+ this ._runner = runner ;
118+ this .priority = priority ;
119+ this ._canceled = false ;
120+ }
121+
122+ dispose(): void {
123+ this ._canceled = true ;
124+ }
125+
126+ execute(): void {
127+ if (this ._canceled ) return ;
128+ this ._runner ();
129+ }
130+
131+ static sort(a : AnimationFrameQueueItem , b : AnimationFrameQueueItem ): number {
132+ return b .priority - a .priority ;
133+ }
134+ }
135+ ```
136+ 调度的核心逻辑是 ` animationFrameRunner ` 函数,它通过 ` requestAnimationFrame ` 在每帧执行任务:
137+ 1 . 从 ` NEXT_QUEUE ` 中提取任务到 ` CURRENT_QUEUE ` 。
138+ 2 . 对 ` CURRENT_QUEUE ` 按优先级排序。
139+ 3 . 按顺序依次执行任务。
140+ ``` typescript
141+ (function () {
142+ // ...
143+ const animationFrameRunner = (targetWindowId : number ) => {
144+ animFrameRequested .set (targetWindowId , false );
145+
146+ const currentQueue = NEXT_QUEUE .get (targetWindowId ) ?? [];
147+ CURRENT_QUEUE .set (targetWindowId , currentQueue );
148+ NEXT_QUEUE .set (targetWindowId , []);
149+
150+ while (currentQueue .length > 0 ) {
151+ currentQueue .sort (AnimationFrameQueueItem .sort );
152+ const top = currentQueue .shift ()! ;
153+ top .execute (); // actual animation execution
154+ }
155+ // ...
156+ };
157+ })();
158+ ```
159+
160+ ## 额外内容
161+ VSCode还提供了一些简单的helper function方便做测试或者补丁等等:
162+ ``` ts
163+ export function measure(targetWindow : Window , callback : () => void ): IDisposable {
164+ return scheduleAtNextAnimationFrame (targetWindow , callback , 10000 /* must be early */ );
165+ }
166+
167+ export function modify(targetWindow : Window , callback : () => void ): IDisposable {
168+ return scheduleAtNextAnimationFrame (targetWindow , callback , - 10000 /* must be late */ );
169+ }
170+ ```
0 commit comments