|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "VSCode:Centralized Animation Frame Scheduling" |
| 4 | +categories: [VSCode, UI] |
| 5 | +--- |
| 6 | + |
| 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