Event Loop事件循环(浏览器)

in JS基础 with 0 comments, 89 views

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引擎空闲下来时就会处理队列中的异步任务。

感谢花时间看完的你, 文章中的结论是我综合网上资料后自己的理解,如有不对的地方欢迎在下方留言☕~

参考

并发模型与事件循环

深入理解 JavaScript Event Loop

带你彻底弄懂Event Loop

Responses ${replyToWho} / Cancel Reply