JS 事件循环
抛出问题
- JS是单线程,如何做到不阻塞(异步)?
- JS中的 macrotask 和 microtask 是什么,有什么区别?
- 事件循环过程是怎样的?
单线程 异步
单线程是指JS引擎中负责解析JS代码的线程只有一个(主线程),即每次只能做一件事,而我们知道一个Ajax请求,主线程在等待它请求过程会去做其他事,浏览器在事件表中注册Ajax的回调函数,相应返回后回调函数被添加到任务队列中等待执行,不会造成线程阻塞,所以说JS处理Ajax的方式是异步
调用栈 任务队列
为了更好的理解调用栈和任务队列,我也引用一张图加以说明
调用栈就是一个栈结构,栈中包含了当前执行函数的执行上下文信息,函数执行完毕,它的执行上下文会从栈中弹出
引用一张网上的图来说调用栈的执行过程
1,执行函数a()先入栈
2,a()中执行函数b(),函数b()入栈
3,执行函数b(),console.log('b')入栈
4,输出b,console.log('b')出栈
5,函数b()执行完成,出栈
6,console.log('a')入栈,执行,输出a,出栈
7,函数a执行完成,出栈
事件循环
从规范理解,浏览器至少有一个事件循环,一个事件循环至少有一个任务队列(macrotask),每个任务都有自己的分组,浏览器会为不同的任务设置优先级
macrotask & microtask
macrotask: 代表一个个离散的、独立的工作单元, 浏览器完成一个宏任务,在下一个宏任务执行开始前,会对页面进行重新渲染
具体的macrotask有下面一些:
- 整体的js代码
- 事件回调
- XHR回调
- 定时器(setTimeout/setInterval)
- IO 操作
- UI 渲染
microtask: 微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务, 如果存在微任务,浏览器会清空微任务之后再重新渲染
具体的microtask有下面一些:
- Promise.then()的回调
- MutationObserver
- DOM变化
事件处理过程
关于macrotask和microtask的处理过程,下面这种图介绍的非常清晰
一次事件循环的步骤包括:
- 检查macrotask队列是否为空,非空则到2,为空则到3
- 执行macrotask中的一个任务
- 继续检查microtask队列是否为空,若有则到4,否则到5
- 取出microtask中的任务执行,执行完成返回到步骤3
- 执行视图更新
上面都是一些理论,下面通过代码感受一下
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(function () {
console.log('promise1');
})
.then(function () {
console.log('promise2');
});
console.log('script end');
执行过程:
- 全局代码(script)入栈,打印script start
- setTimeout callback压入macrotask队列
- 第一个Promise.then压入microtask任务队列
- 继续执行全局代码的最后一行,打印script end
- 全局代码(macrotask)执行完
- 执行microtask队列,执行Promise.then的第一回调,打印promise1
- 第一个Promise.then的回调返回undefined,Promise状态变为fullfill,触发下一个then回调
- Promise的第二个then压入microtask队列
- 由于microtask队列没有清空,继续执行microtask,打印promise2
- microtask队列为空,如果有UI渲染,会去执行UI渲染
- 开启下一轮的事件循环,执行下一个macrotask任务,也就是setTimeout的回调,打印setTimeout
上述过程不断重复就是所谓的事件循环
UI渲染的时机
回顾上面的事件循环示意图,UI渲染发生在本轮事件循环的microtask队列执行完之后,也就是说任务的执行绘影响视图的渲染时机,通常浏览器以每秒60帧的速率刷新页面,大概16.7ms渲染一帧,所以如果要让用户觉得顺畅,单个macrotask任务及其相关的microtask任务最好控制在16.7ms内完成
总结
- 事件循环是JS实现异步的核心
- 每轮事件循环分为3个步骤:
a) 执行macrotask任务队列的一个任务
b) 执行完当前microtask队列的所有任务
c) UI 渲染