Event Loop是什么?
了解Event Loop其实可以更好的帮助我们理解异步编程🙃
首先我们知道JS(JS引擎线程)是单线程的,除开后来出来的web wrokers(可以分担js中一些耗时的计算任务而不干扰用户界面)。也就是说同一时间内只做一件事情。为什么会是单线程呢,试想下这样的场景,如果JS是多线程的,你在尝试给#div1
设置一个border
,发现#div1
被线程B
给移除了,这就会乱套了。所以JS从一开始就被设计成了单线程,因为这样足够简单。JS又是异步的,支持并发,而js的并发模型就是基于"Event loop"事件循环的, 因为计算机足够快,看起来就像同时做了多件事情。
运行时概念
Event Loop是一个理论模型, 现代JS引擎(比如V8)实现并优化了这套模型。Node和浏览器的实现有差别,暂时只讨论浏览器中的情况。
先上一个图
Stack(执行栈)
后进先出
比如以下代码:
function a() {
console.log('a');
}
function b() {
console.log('b');
a();
}
function c() {
console.log('c');
b();
}
c(); // 输出 c, b, a
function a() {
console.log('a');
}
function b() {
a();
console.log('b');
}
function c() {
b();
console.log('c');
}
c(); // 输出 a, b, c
执行c()
时c函数被压入执行栈创建第一个栈帧开始执行,c函数内部调用了b函数,同样b函数被压入执行栈,此时b函数位于栈的顶端,同理b函数内调用了a函数,执行到a函数的代码行时,a函数被压入执行栈顶端。当函数执行完毕时,执行栈会弹出对应栈帧,当最后a函数执行完,栈也就空了。
注: 同步任务代码会直接进入执行栈执行,而异步任务代码会进入一个单独的队列等待。
Heap(堆)
引用类型对象被分配在一个堆中,即用以表示一大块非结构化的内存区域。
Queue(队列)
一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都关联着一个用以处理这个消息的函数。前面提到过,JS是异步的,这个队列中存放的就是待处理的异步任务(当异步任务返回结果后)。主线程空闲下来后,即执行栈空了后,会在消息队列中取一个任务,压入执行栈进行执行。
事件循环
之所以称为事件循环,因为这个机制就像流水线一样,通常会实现成像这个样子:
while (queue.waitForMessage()) {
queue.processNextMessage();
}
如果没有事件消息到达,queue.waitForMessage() 会同步地等待消息到达,然后进入执行栈处理。
异步任务也有区别对待
异步任务分两大类,宏任务和微任务。微任务优先级更高。
比如以下代码:
setTimeout(function() { console.log('setTimeout')});
new Promise((resolve, reject) => {
console.log('promise'); // Promise中的同步代码
resolve();
}).then(() => {
console.log('then');
})
console.log(1);
// 输出 promise, 1, then setTimeout
// 优先执行了promise.then中的微任务代码,最后才执行setTiemout的宏任务代码
宏任务
名称 | 浏览器 | Node |
---|---|---|
I/O (onClick ajax onLoad...) | Yes | Yes |
setTimeout | Yes | Yes |
setInterval | Yes | Yes |
setImmediate | No | Yes |
微任务
名称 | 浏览器 | Node |
---|---|---|
process.nextTick | - | Yes |
MutationObserver(监听DOM) | Yes | - |
Promise | Yes | Yes |
做个小实验
如果在微任务中插入宏任务代码会是什么情况:
setTimeout(function() { console.log('setTimeout')});
new Promise((resolve, reject) => {
console.log('promise'); // Promise中的同步代码
resolve();
}).then(() => {
console.log('then');
setTimeout(function(){
console.log('setTimeout in then');
})
})
console.log(1);
// 输出 promise, 1, then, setTimeout, setTimeout in then
// 插入的宏任务代码setTimeout in then 插在了上一个宏任务之后执行
特殊的requestAnimationFrame
这个事件的代码不按常理出牌, requestAnimationFrame回调是跟着浏览器走的,告诉浏览器会在下一次重绘之前执行。 仔细一想也是合理的,它的执行时机和浏览器重绘(UI Render)相关的,一轮event loop中是否执行浏览器渲染是由浏览器的策略决定的。
setTimeout(function() { console.log('setTimeout')});
new Promise((resolve, reject) => {
console.log('promise'); // Promise中的同步代码
resolve();
}).then(() => {
console.log('then');
})
requestAnimationFrame(_ => console.log('animationFrame'));
// 可以尝试多次运行结果
// animationFrame和setTimeout执行顺序无法保证
// 不过微任务一定优先于requestAnimationFrame执行
// animationFrame则可能会等到下一轮事件循环,且由于UI Render在事件循环结束 后才执行,所以排到seTimeout之后执行。
附一张图加深理解:
测试
console.log(1);
setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});
new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
})
setTimeout(() => {
console.log(6);
})
console.log(7);
测试下之前的理解没, 先尝试着自己做下看看
运行结果:
1, 4, 7, 5, 2, 3, 6
写在最后
JS虽然是单线程的,但浏览器是多线程的,大致包括GUI渲染线程、JS引擎线程、事件触发线程、定时触发器线程、异步HTTP请求线程。其中GUI渲染线程与JS引擎线程互斥,所以如果JS引擎线程处理长耗时任务时界面看起来会卡顿。其它几个线程在满足一定条件时就会往任务队列中添加任务(微任务、宏任务),等待JS引擎空闲下来时就会处理队列中的异步任务。
感谢花时间看完的你, 文章中的结论是我综合网上资料后自己的理解,如有不对的地方欢迎在下方留言☕~