高级前端开发进阶指南——Google V8(一):事件循环

前言

早期浏览器的页面是运行在 UI线程 上的,为了在页面中引入JavaScript,方便JS操作DOM,JS也需要运行在和页面相同的线程中,所以JS是单线程的。

一、基本概念

事件循环系统由主线程、调用栈、宏任务、微任务、消息队列等构成。

主线程:UI线程
调用栈:一种数据结构、用来管理在主线程上执行的函数的调用关系
消息队列:用于存放等待主线程执行的宏任务 ‘事件’列表

任务:UI线程每次从消息队列中取出事件、执行事件的过程称为一次任务
宏任务:消息队列中等待被主线程执行的事件、宏任务包含鼠标、键盘、触控板事件
微任务:一个需要异步执行的函数、执行的时机在主线程执行结束之后、当前宏任务结束之前

常见宏任务有:主js、UI渲染、setTimeout、setInterval、setImmediately、requestAnimationFrame、I/O等

常见微任务有:process.nextTick()、promise.then()(new Promise不算!)、Object.observe()等

二、事件循环

生命周期

=> 初始化

1、从消息队列中取出任务
2、全局执行上下文压入调用栈中
3、在全局上下文中创建微任务队列

=> 执行任务

事件执行中:
1、增加新的上下文到调用栈中
2、执行函数中的代码
3、封装新的事件并添加到消息队列中

事件执行后:
1、在函数执行结束后将当前函数的执行上下文从调用栈中弹出
2、查询并执行全局上下文中的微任务队列
3、垃圾回收?

=> 结束当前事件

从消息队列中取出新的事件开始执行、循环往复
图文讲解

备注:本文章部分图片来自 《极客时间:图解 Google V8》,如有侵权,请告知删除,感谢!

用以下代码为例,理解事件循环机制:
function foo() {
    console.log('setTimeout star');
    out && out();
    
    console.log('Promise star');
    pro && pro();
    
    console.log('bar star');
    bar && bar();
}

function bar() {
    console.log('bar');
}

function pro() {
    Promise.resolve().then(res => {
        console.log('Promise');
    });
}

function out() {
    setTimeout(function() {
        console.log('setTimeout:');
    }, 100);
}

foo();

从消息队列中取出foo()事件
创建调用栈并将全局执行函数上下文压入栈中
:将foo函数执行上下文压入栈中
:执行console.log('setTimeout star');
:将out函数执行上下文压入栈中
:将setTimeout的回调函数封装成一个新的事件100ms后添加到消息队列中
out函数执行结束、将out函数上下文从调用栈弹出
:执行console.log('Promise star');
:将pro函数执行上下文压入栈中
:将微任务添加到调用栈中全局执行上下文的微任务队列
pro函数执行结束、将pro函数上下文从调用栈弹出
:执行console.log('bar star');
:将bar函数执行上下文压入栈中
:执行console.log('bar');
bar函数执行结束、将bar函数上下文从调用栈弹出
从全局执行函数上下文中将微任务队列中得函数取出,执行console.log('Promise');
结束当前事件、当从setTimeout事件添加到消息队列后取出事件,执行console.log('setTimeout');

三、常见关联问题

堆栈溢出
function foo() {
    foo();
}

foo()

上述代码中foo函数中调用了自身,根据上面我们了解的事件循环机制,我们会发现在执行foo函数的过程中,主线程会不断的向调用栈中压入foo函数的执行上下文,而因为foo函数始终没有执行结束,所以函数上下文并不会从调用栈中移除,然而调用栈的容量是有限的,所以就会造成调用栈的溢出。

面对此类问题,我们有两种解决方案:
1、宏任务异步调用

function foo() {
    setTimeout(function() {
        foo();
    }, 0);
}

// 这里直接给出代码,可以根据上述内容分析为什么这样就不会造成堆栈溢出(我是不会承认自己懒得写了~)
// 另外需要注意,即便这样可以不造成堆栈溢出,但我们仍旧不推荐这样写,大量的事件阻塞了消息队列中其他事件的执行

2、按条件结束调用——递归函数

宏任务和微任务的执行顺序

将上面案例的代码复制到浏览器中执行,我们会发现foo();执行的结果是:

// 打印结果:
// setTimeout star
// Promise star
// bar star
// bar
// Promise
// setTimeout

这里我们注意到一个问题,那便是虽然在代码中三个函数的执行顺序是setTimeout、Promise、bar,但是由于浏览器对他们的处理方式不一致,导致最终的执行结果是bar、Promise、setTimeout,这里其实就是同步与异步微任务与宏任务的代码执行顺序问题,根据上面代码的结果与我们对这段代码的执行过程分析可以得出以下结论:

  • 先同步、后异步
  • 微任务在主线程执行事件结束之后、当前宏任务结束之前执行
  • 根据第二条可得、当一个事件中如果同时存在微任务和宏任务、微任务在宏任务之前执行。
setTimeout、setInterval等定时器函数的执行时间间隔

这里我们先想一个问题,上面代码中setTimeout得延迟设定为100ms,则理论上打印setTimeout star和setTimeout之间的时间间隔也应该为100ms,但是真实的情况是这样的嘛。

因为本篇文章主要内容为事件循环,这里就直接说出答案:并不是!!!

原因将在之后单独开一篇文章解释,敬请关注!

阅读 291

推荐阅读