广告

前端开发必读:深入解析 JavaScript await 的执行机制与事件循环中的 Tick 概念

1. JavaScript await 的执行机制总览

await 的核心作用是让异步操作完成前暂停当前函数执行,并将后续代码转移到一个微任务中继续执行。这一机制确保在等待期间不会阻塞主线程,事件循环会在等待的 Promise 变为就绪时继续调度,从而实现高效的非阻塞 I/O。

使用 await 的前置条件是所在的函数必须被标记为 async,这会让函数返回一个 Promise,从而允许调用端以链式方式处理结果。错误处理也围绕 Promise 的拒绝处理

1.1 Promise 与 async/await 的关系

在 JavaScript 中,async 函数总是返回一个 Promise,而 await 只能在 async 函数内部使用。当遇到 await 时,表达式左侧的 Promise 会被解析,如果是已解决的 Promise,直接替换为其值继续执行;若是未解决,函数执行会暂停,直到 Promise 变为就绪。

下面的示例展示了 等待 Promise 结果的简单模式,以及如何在错误发生时用 try/catch 捕获异常。

async function getUser(id){const user = await fetch(`/api/user/${id}`).then(r => r.json());return user;
}try {const u = await getUser(1);console.log(u.name);
} catch (err) {console.error('请求失败', err);
}

1.2 在一个宏任务中的执行流

await 的暂停点本质上是把后续处理放到微任务队列中,再由事件循环在当前宏任务结束后执行。你会看到本轮的 宏任务可能包含一些同步工作,之后紧跟着的 微任务队列先清空再渲染

下面的例子演示了 宏任务与微任务的交互,以及 setTimeout 如何与 Promise.resolve().then 共同影响执行顺序。

前端开发必读:深入解析 JavaScript await 的执行机制与事件循环中的 Tick 概念

console.log('start');
setTimeout(() => {console.log('timeout');
}, 0);Promise.resolve().then(() => console.log('microtask 1'));
Promise.resolve().then(() => {console.log('microtask 2');
});
console.log('end');

2. 事件循环中的 Tick 概念

Tick 指事件循环中的一次调度轮次,在这一次轮次内会执行位于宏任务队列的任务,以及随后清空微任务队列的阶段。理解 Tick 能帮助判断同步代码、异步任务以及浏览器渲染之间的实际执行顺序。

每个 Tick 结束后,浏览器有机会进行一次重绘与回流,若微任务队列仍有任务,则会在渲染前继续执行,从而影响帧率和用户体验。

2.1 Tick 的触发点与循环顺序

在单线程模型中,事件循环不断遍历宏任务与微任务队列,每次从宏任务队列取出一个执行,然后执行其内的代码。微任务队列在当前宏任务执行完毕后清空,这就是一个完整 Tick 的核心顺序。

下面通过示例演示一个简单的 Tick 顺序:先执行同步代码,再执行宏任务,最后清空微任务并渲染。

console.log('tick start');setTimeout(() => {console.log('tick macro');
}, 0);Promise.resolve().then(() => {console.log('tick microtask');
});console.log('tick end');

2.2 Tick 与页面渲染的关系

微任务的执行可能会延迟渲染,因为在同一个 Tick 内,微任务需要全部完成后再进行渲染阶段。如果微任务很密集,渲染可能被推后,从而影响页面的流畅性。

为了获得更平滑的体验,可以将关键渲染相关的逻辑放在合适的时机,避免在一个 Tick 内堆积大量微任务。

3. await 如何在事件循环中产生暂停与继续

await 会将后续代码的执行封装为一个微任务,从而把当前 async 函数的执行暂停,直到等待的 Promise 就绪,然后再以微任务的形式触发继续执行。

通过 将继续执行放到微任务队列,可以确保其他同步代码先执行,随后再处理异步结果,这也是非阻塞式 UI 更新的核心

3.1 await 的实际执行顺序演示

下面的示例直观展示了 await 会让后续代码在当前宏任务结束后才执行,并且与微任务的紧密关系相关。

async function demo(){console.log('A');await new Promise(resolve => setTimeout(resolve, 0));console.log('B');
}
console.log('C');
demo();
console.log('D');

3.2 误区:await 只是睡眠

await 不是阻塞等待,它只是将后续代码放入微任务队列中,当 Promise 就绪时才会继续执行。这使得 UI 线程在等待期间仍然可以处理用户事件

为了避免对渲染造成不可预测的影响,应使用合理的微任务长度与避免在微任务中做耗时计算。

广告