JS的浏览器环境的事件循环(event loop)机制

1. 事件循环

JavaScript引擎并不是独立运行的,它运行在宿主环境中,对多数开发者来说通常就是Web浏览器。经过最近几年的发展,JavaScript已经超出了浏览器的范围,进入了其他环境,比如通过像Node.js这样的工具进入服务器领域。实际上,JavaScript现如今已经嵌入到了从机器人到电灯泡等各种各样的设备中。
所有这些环境都有一个共同“点”,即它们都提供了一种机制来处理程序中多个块的执行,且执行每块时调用JavaScript引擎,这种机制被称为事件循环。
换句话说,JavaScript引擎本身并没有时间的概念,只是一个按需执行JavaScript任意代码片段的环境。“事件”(JavaScript代码执行)调度总是由包含它的环境进行。
引自《你不知道的JavaScript中卷》第二部分 异步和性能 第一章 异步

2. JS引擎的两大特点:单线程和非阻塞

单线程

JS引擎是基于单线程(Single-threaded)事件循环的概念构建的。同一时刻只运行一个代码块在执行,与之相反的是像JAVA和C++一样的语言,它们允许多个不同的代码块同时执行。对于基于线程的软件而言,当多个代码块同时访问并改变状态时,程序很难维护并保证状态不出错。

非阻塞

非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。非阻塞是通过事件循环机制实现的。
JS通常是非阻塞的,除了某些特殊情况,JS会停止代码执行:
- alert, confirm, prompt(除了Opera)
- “页面上的程序正忙”的系统对话框弹出

2.同步和异步

异步编程模型
包括事件模型,回调模式,Promise异步操作结果占位符。
事件模型会在触发事件后向任务队列中添加事件处理程序,但是只有当其前面的任务都完成后它才会执行。
回调模式和事件模型类似,异步代码都会在未来某个事件点执行,但是不同在于回调模型中被调用的回调函数是作为参数传入的,而且可以使用回调模式链接多个调用,也就是也嵌套的方式调用回调函数,但是这种方式的问题是会陷入“回调地狱”,嵌套的代码难以理解并难以维护。
Promise不会订阅一个事件或是传递回调函数给目标函数,而是让函数返回一个Promise对象。Promise有三种状态,保存在其内部属性中,分别是”pending”(进行中)、fulfilled(已完成)、rejected(已拒绝)。通过为执行器函数分别指定resolve和reject函数可以在执行器完成时(fulfilled或rejected)调用相应的函数。

3.执行栈与任务队列

栈内存和堆内存
栈内存保存着JS的变量和指向堆内存中对象的指针,堆内存保存着对象。
下面的执行栈和栈内存是两个概念。
执行栈和任务队列
这里写图片描述

调用栈中遇到DOM操作、ajax请求以及setTimeout等WebAPIs的时候就会交给浏览器内核的其他模块进行处理,webkit内核在Javasctipt执行引擎之外,有一个重要的模块是webcore模块。对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现。等到这些模块处理完这些操作的时候将回调函数放入任务队列中,之后等栈中的task执行完之后再去执行任务队列之中的回调函数。
函数调用形成了一个栈帧

function foo(b) {
  var a = 10;
  return a + b + 11;
}

function bar(x) {
  var y = 3;
  return foo(x * y);
}

console.log(bar(7));

调用bar时,创建了第一个帧 ,帧中包含了bar的参数和局部变量。当bar调用foo时,第二个帧就被创建,并被压到第一个帧之上,帧中包含了foo的参数和局部变量。当foo返回时,最上层的帧就被弹出栈(剩下bar函数的调用帧 )。当bar返回的时候,栈就是空的。以上都是同步代码的执行。

任务队列
一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈拥有足够内存时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着消息处理结束。

4.任务队列中的宏观任务和微观任务

任务队列并不是只有一个,不同的任务对应着不同的任务队列。宏观任务放入宏观任务队列,微观任务放入微观任务队列,这些任务队列在栈空的时候被调入的优先级是微观任务队列优于宏观任务队列,当微观任务队列都清空的时候才执行宏观任务队列中的任务

macro-task包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver,MessageChannel

5.实例

理解下列代码的输出顺序:
5.1SetTimeOut

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log(i);
    }, 1000);
}
console.log(i);

结果:
输出的状态就是:5 -> 5,5,5,5,5,即第1个 5 直接输出,最少等待1s之后,输出 5个5;
因为执行for循环的时候,执行引擎将setTimeout加入到执行栈中,调用栈发现setTimeout是之前提到的WebAPIs中的API,因此将其出栈之后将延时执行的匿名函数交给浏览器的timer模块进行处理。当timer模块中延时方法规定的时间到了之后就将其放入到任务队列之中。什么时候执行呢?等到执行栈空的时候执行回调函数。for循环之后执行了console语句,直接输出5,此时执行栈空,开始执行任务队列中的5个回调函数。因为这些回调函数都是在1秒之后依次加入到任务队列的,因此1秒之后,连续输出了5个5。
5.2 Promise

(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function executor(resolve) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})();

结果:
1,2,3,5,4
2,3,5resolve函数会加入微观任务队列,因此会先执行2再执行3,此时执行栈清空,开始清理微观任务队列中的函数,因此输出5
5,4先执行微观任务队列中的resolve函数再执行宏观任务中的SetTimeout函数,因此先输出5再输出4

6.参考

《深入理解ES6》Promise章节
深入浅出JS事件循环机制
MDN:并发模型与事件循环
《你不知道的JavaScript中卷》

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值