这段代码到底怎么走?终于搞定Event loop

众所周知,js是一门单线程的编程语言,在设计之初,它就注定了单线程的命运,好比当咱们处理dom时,若是有多个线程同时操做一个dom,那将很是混乱。html

既然是单线程,那么它必定有一套严谨的规则,来使代码可以乖乖的按开发者的设计运行,今天咱们就来研究其中的奥秘,了解一下js的event loop(事件循环)。node

同步/异步

聊js事件环,绕不开聊异步(在个人另外一篇文章拥抱并扒光Promise中对Promise这种异步解决方案有详细介绍)ajax

为何要异步?假设没有异步,咱们发送一个ajax请求,后端代码运行的很慢,这时浏览器会发生阻塞,若是十秒才响应,这十秒咱们该干吗?(或许能够看博尔特跑个百米)编程

虽然在网页诞生之初,确实有这样的状况,但现在这样的页面是会被用户骂娘的。因而异步的做用显露无遗,js开启一个异步线程,何时请求完成,何时执行回调函数,而这期间,其余代码也能够正常运行。后端

任务队列(task queue)

既然是单线程,就像一次只能过一我的的独木桥,人要排队,那么代码也要排队。这时,同步代码和异步代码的排队机制是不同的api

同步:在主线程(至关于独木桥上)上排队的任务,前一个任务执行完,下一个任务才能够执行,若是前一个任务没执行完,下一个任务要一直等待。就像过独木桥,前面的人不过去,你死等也得等,否则就5253B翻腾两周半入水。浏览器

异步:主线程先无论IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回告终果,再回过头,把挂起的任务继续执行下去。就像过独木桥,你惧怕不敢过,你就让后面的人先过,何时你敢了你再过。而你调整心态的过程,主线程不考虑。dom

  • 同步任务在主线程上执行,造成一个执行栈(xecution context stack)
  • 主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  • 一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。

主线程会不断的重复以上三步,这样就构成了事件环,用图表示异步

浏览器中的Event Loop

  • 堆(heap)在JS运行时用来存放对象。
  • 栈(stack)遵循“先进后出”原则,咱们知道栈能够存放对象的地址,但本文中的栈是指用来执存放行JS主线程的执行栈(execution context stack)。

经过这张图,咱们能够知道,主线程运行时,产生堆和执行栈,栈中的代码会调用一些api,好比seTtimeou、click等,这些异步操做会讲他们的回调放入callback queue中,当执行栈中的代码运行完,主线程回去读取queue中的任务。编程语言

console.log(1)
setTimeout(function(){
  console.log(2)  
})
console.log(3)
复制代码

咱们都知道结果是1 3 2,结合上面咱们梳理一下这段代码的执行顺序

一、从上到下运行执行栈中的同步代码console.log(1)

二、看到setTimeout,把回调函数放入任务队列中去

三、执行console.log(3)

四、主线程上没有任务了,去任务队列中执行setTimeout的回调,console.log(2)

Node中的Event Loop

显然node要比浏览器复杂一些,它的流程是这样的:

  • V8引擎解析JavaScript脚本。
  • 解析后的代码,调用Node API。
  • libuv库负责Node API的执行。它将不一样的任务分配给不一样的线程,造成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
  • V8引擎再将结果返回给用户。

Node还有一些不一样,它提供了另外两个与"任务队列"有关的方法:process.nextTick和setImmediate。它们能够帮助咱们加深对"任务队列"的理解。

process.nextTick方法能够在当前"执行栈"的尾部,下一次Event Loop(主线程读取"任务队列")以前,触发回调函数。也就是说,它指定的任务老是发生在全部异步任务以前。

setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务老是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。

大概能够理解成process.nextTick有权插队

setTimeout(function(){
  console.log(1)
})
process.nextTick(function () {
  console.log(2);
  process.nextTick(function (){
    console.log(3)
  });
});
setTimeout(function () {
  console.log(4);
})
复制代码

虽然1在上面,但结果是2 3 1 4,就像咱们上面说的,process.nextTick会在主线程读取任务队列时插队

再看setImmediate

setImmediate(function () {
  console.log(1);
  setImmediate(function B(){
    console.log(2)
  })
})
setTimeout(function () {
  console.log(3);
}, 0)
复制代码

结果多是312,也多是132

微任务/宏任务

为何会出现上面有的先有的后的状况呢,难道除了人类社会代码世界也有特权么,是的,咱们将任务分为两种:

微任务Microtask,有特权,能够插队,包括原生Promise,Object.observe(已废弃), MutationObserver, MessageChannel;

宏任务Macrotask,没有特权,包括setTimeout, setInterval, setImmediate, I/O;

最后,一段比较复杂的代码收尾。

console.log("1");
setTimeout(()=>{
    console.log(2)
    Promise.resolve().then(()=>{
        console.log(3);
        process.nextTick(function foo() {
            console.log(4);
        });
    })
})
Promise.resolve().then(()=>{
    console.log(5);    
    setTimeout(()=>{
        console.log(6)
    })
    Promise.resolve().then(()=>{
        console.log(7);
    })
})
process.nextTick(function foo() {
    console.log(8);
    process.nextTick(function foo() {
        console.log(9);
    });
});
console.log("10")
复制代码

执行顺序:

1,输出1

2,将setTimeout(2)push进宏任务

3,将then(5)push进微任务

4,在执行栈底部添加nextTick(8)

5,输出10

6,执行nextTick(8)

7,输出8

8,在执行栈底部添加nextTick(9)

9,输出9

10,执行微任务then(5)

11,输出5

12,将setTimeout(6)push进宏任务

13,将then(7)push进微任务

14,执行微任务then(7)

15,输出7

16,取出setTimeout(2)

17,输出2

18,将then(3)push进微任务

19,执行微任务then(3)

20,输出3

21,在执行栈底部添加nextTick(4)

22,输出4

23,取出setTimeout(6)

24,输出6


参考:

图解搞懂JavaScript引擎Event Loop

JavaScript 运行机制详解:再谈Event Loop

相关文章
相关标签/搜索