深入理解事件循环与任务队列

JS是单线程的

JS是单线程的,也就是它一次只能执行一段代码。JS中其实是没有线程概念的,所谓的单线程也只是相对于多线程而言。JS的设计初衷就没有考虑这些,针对JS这种不具备并行任务处理的特性,我们称之为“单线程”。
虽然JS运行在浏览器中是单线程的,但是浏览器是事件驱动的(Event driven),浏览器中很多行为是异步(Asynchronized)的,会创建事件并放入执行队列中。浏览器中很多异步行为都是由浏览器新开一个线程去完成,一个浏览器至少实现三个常驻线程:

  • JS引擎线程
  • GUI渲染线程
  • 事件触发线程

JS引擎
JavaScript引擎是一个专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中,比如最出名的就是Chrome浏览器的V8引擎,如下图所示,JS引擎主要有两个组件构成:

  • 堆-内存分配发生的地方
  • 栈-函数调用时会形一个个栈帧(frame)


调用栈

function multiply(x, y) {
    return x * y;
}
function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}
printSquare(5);
  • 调用一个函数时,返回地址(return address)、参数(arguments)、本地变量(local variables)等都会被推入栈中。当函数执行完毕弹出堆栈的时候,局部变量(简单数据类型)也会跟着弹出,复杂的数据类型的话则是弹出相应的指针。
  • 只有简单的数据类型(Number,String,Boolean,Undefined,Null,Symbol)是存放在栈中,复杂的数据类型譬如对象,数组,只是把对应的指针存放在栈中,真正的值是存放在Heap中的,当这个对象没有用处的时候,由垃圾回收机制进行释放空间。
  • 当一个函数嵌套另一个函数时,则这个函数的相关参数也会被推入栈顶。

事件循环与任务队列

事件循环可以简单描述为:

1、函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空;
2、在此期间WebAPIs完成这个事件,把回调函数放入CallbackQueue中等待;
3、当执行栈为空时,Event Loop把Callback Queue中的一个任务放入Stack中,回到第1步。


运行机制图
  • Event Loop是由javascript宿主环境(像浏览器)来实现的;
  • WebAPIs是由C++实现的浏览器创建的线程,处理诸如DOM事件、http请求、定时器等异步事件;
  • JavaScript 的并发模型基于"事件循环";
    Callback Queue(Event Queue 或者 Message Queue) 任务队列,存放异步任务的回调函数

setTimeout和setInterval的运行机制是,将指定的代码移出本次执行,等到下一轮Event Loop时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮Event Loop时重新判断。这意味着,setTimeout指定的代码,必须等到本次执行的所有代码都执行完,才会执行。

setTimeout的作用是将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f,0),那么不会立刻执行

setTimeout(f,0)将第二个参数设为0,作用是让f在现有的任务(脚本的同步任务和“任务队列”中已有的事件)一结束就立刻执行。也就是说,setTimeout(f,0)的作用是,尽可能早地执行指定的任务。

看两个例子更好理解

var flag = true;
setTimeout(function(){
    flag = false;
},0)
while(flag){}
console.log(flag);

以上这段代码给人一种感觉setTimeout(f,0)就是立即执行,运行没有什么问题啊 ,其实不然,看运行图所示我们知道 setTimeout(f,0)会被放到异步操作线程里面,等同步执行函数完成之后在接着执行。具体执行顺序流程实例:

var start=new Date();
setTimeout(function cb(){
    console.log("时间间隔:",new Date()-start+'ms');
},500);
while(new Date()-start<1000){};
  • main()入栈,局部变量start初始化;
  • setTimeout入栈,出栈,丢给WebAPIs,开始定时500ms;
  • while循环入栈,开始阻塞1000ms;
  • 500ms过后,WebAPIs把cb()放入任务队列,此时while循环还在栈中,cb()等待;
  • 又过了500ms,while循环执行完毕从栈中弹出,main()弹出,此时栈为空,Event
    Loop,cb()进入栈,log()进栈,输出'时间间隔:1003ms',出栈,cb()出栈

Microtasks和Macrotasks

macro-task(Task)包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。

micro-task(Job)包括:process.nextTick, Promises.then(), Object.observe(已被废弃), MutationObserver

根据 WHATVG 的说明,在一个事件循环的周期(cycle)中一个 (macro)task 应该从 macrotask 队列开始执行。当这个 macrotask 结束后,所有的 microtasks 将在同一个 cycle 中执行。在 microtasks 执行时还可以加入更多的 microtask,然后一个一个的执行,直到 microtask 队列清空。

setTimeout(function cb() {
    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 onFulfilled() {
    console.log(5);
});
console.log(3);
//执行结果:1 2 3 5 4

或者可以简单写成这样:

setTimeout();
var promise = new Promise(executor);
promise.then(callback);
console.log(3);
  • main()入栈;
  • setTimeout入栈,出栈,丢给WebAPIs,开始定时0ms(实际上不一定是多少,总之大于0),到时之后,将回调函数cb()放入macrotask queue;
  • Promise构造函数executor()入栈,log(1)入栈,输出1,出栈;
  • for循环入栈,当i=9999时,resolve()入栈,Promise实例的状态变为fulfilled(完成),resolve()出栈。构造函数执行完后,我们得到了promise(它是resolved);
  • promise.then入栈,onFulfilled(then方法绑定的resolved状态的回调函数)放入microtask queue;
  • log(2)入栈,输出2,出栈;
  • executor()出栈;
  • log(3)入栈,输出3,出栈,main()出栈;
  • 此时栈为空,microtask queue中的任务可以进栈了,onFulfilled()入栈,log(5)入栈,输出5,出栈;
  • 此时Stack和microtask queue都为空,Event Loop,将macrotask queue中的cb()入栈,log(4)入栈,输出4,log(4)出栈,cb()出栈

Promise构造函数是同步函数,该executor函数由Promise实现立即执行
resolve函数由 JavaScript 引擎提供,不用自己部署。

参考文章:
https://www.jianshu.com/u/0bdfa5b7cd52
饥人谷

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容