H5 JavaScript 事件循环与任务调度
H5 JavaScript 事件循环与任务调度
JavaScript 在浏览器主线程上执行时,需要与用户输入、样式计算、布局和绘制共享时间。异步代码并不自动代表不会阻塞:Promise 回调过多、长循环和连续同步 DOM 操作都可能让页面迟迟无法渲染。理解任务、微任务和帧调度,有助于安排真正合适的执行时机。
一、调用栈必须先清空
同步代码进入调用栈并依次执行。只要当前函数没有返回,浏览器就不能处理下一项主线程任务。
function calculateTotal(items: CartItem[]): number {
let total = 0;
for (const item of items) {
total += item.priceInCent * item.quantity;
}
return total;
}
少量数据没有问题,但数十万条复杂计算会长时间占用主线程。是否拆分应由真实耗时决定。
二、任务与微任务的顺序
计时器、用户事件等通常进入任务队列;Promise 回调进入微任务队列。当前任务结束后,浏览器会清空微任务,再获得渲染机会。
console.log('同步开始');
setTimeout(() => {
console.log('定时任务');
}, 0);
Promise.resolve().then(() => {
console.log('微任务');
});
console.log('同步结束');
输出顺序是同步开始、同步结束、微任务、定时任务。零延迟计时器表示尽快排队,不表示立即执行。
三、微任务也可能阻塞渲染
持续创建新的微任务,会让微任务队列长期无法清空,浏览器没有机会绘制。
function scheduleMicrotaskLoop(): void {
Promise.resolve().then(scheduleMicrotaskLoop);
}
这类代码即使没有传统同步死循环,也会让页面失去响应。需要让出渲染机会时,应切换到新的任务或按帧继续,而不是只使用 Promise 链。
四、按批次拆分长任务
不要求一次同步完成的数据处理可以拆成小批次:
async function processInBatches<T>(
items: readonly T[],
batchSize: number,
handle: (item: T) => void
): Promise<void> {
for (let start = 0; start < items.length; start += batchSize) {
const end = Math.min(start + batchSize, items.length);
for (let index = start; index < end; index += 1) {
handle(items[index]);
}
await new Promise<void>(resolve => setTimeout(resolve, 0));
}
}
拆分会增加总调度成本,批次大小需要测量。金额提交等必须原子完成的业务逻辑不能随意拆开。
五、视觉更新使用帧回调
动画和滚动联动应在下一绘制帧统一更新:
let scheduled = false;
let latestOffset = 0;
function onScroll(offset: number): void {
latestOffset = offset;
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
header.style.transform = `translateY(${-latestOffset}px)`;
scheduled = false;
});
}
一帧内多次滚动事件只产生一次 DOM 写入,并使用最新值。
六、计时器不能承担精确计时
页面进入后台、设备繁忙或节电策略生效时,计时器会被延迟。倒计时不应每次简单减一,而应根据目标时刻和当前时刻重新计算剩余时间。
function remainingSeconds(deadline: number, now: number): number {
return Math.max(0, Math.ceil((deadline - now) / 1000));
}
关键业务时间以服务端权威时间和规则为准,前端计时只负责展示。
七、计算密集任务考虑工作线程
大型解析、图像处理和复杂统计可以放入工作线程,但线程通信需要复制或转移数据,也有启动成本。
只有任务足够重、与 DOM 无关且输入输出边界清晰时才值得迁移。工作线程不能直接操作页面元素,结果仍需回到主线程渲染。
八、避免同步等待
主线程同步等待资源或锁会冻结整个页面。异步接口应以 Promise 或事件返回结果,让浏览器在等待期间处理其他任务。
多个异步任务之间有共同成败或先后依赖时,应在代码结构中明确,而不是通过固定延迟猜测完成顺序。
九、页面生命周期影响调度
页面进入后台后,动画帧会暂停或降频,计时器也会节流。回到前台时,应根据最新业务状态重新计算界面,而不是补执行所有错过的动画帧。
页面销毁时取消仍在等待的任务、计时器和监听,防止旧回调更新新页面。
十、用时间线验证
性能分析中可以查看:
- 单个任务持续时间。
- 微任务数量是否异常。
- 渲染帧是否被长任务跨越。
- 输入事件到视觉反馈的间隔。
- 批处理后总耗时与交互响应是否改善。
总结
事件循环决定了异步工作与渲染如何共享主线程。同步栈结束后会先清空微任务,过多微任务同样会阻塞绘制。计算任务按业务允许的批次让出执行权,视觉更新按帧合并,并用真实时间而非计时器次数计算状态,页面才能保持响应。
- 点赞
- 收藏
- 关注作者
评论(0)