阅读 1147

浅谈event loop

Javascript引擎是单线程机制,首先我们要了解Javascript语言为什么是单线程

JavaScript的主要用途主要是用户互动,和操作DOM。如果JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时这两个节点会有很大冲突,为了避免这个冲突,所以决定了它只能是单线程,否则会带来很复杂的同步问题。此外HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程(UI线程, 异步HTTP请求线程, 定时触发器线程...),但是子线程完全受主线程控制,这个新标准并没有改变JavaScript单线程的本质。


在了解event loop之前,我们先了解一下什么是栈和队列,他们有什么特点?请先看两张图。

此处输入图片的描述
此处输入图片的描述
栈(stack) 是自动分配内存空间,它由系统自动释放,特点是先进后出。 队列的特点是先进先出。 再看一张图:
此处输入图片的描述
我们代码执行的时候,都是在栈里执行的,但是我调用多线程方法的时候是放到队列里的,先放进去的先执行。 那WebAPIs的方法什么时候放到栈里执行呢? 当栈里的代码执行完了,会在队列里面读取出来,放到栈执行。比如:写个事件,事件里面再调用异步方法,这些方法会在调用的时候,放到队列里,会不停的循环。等到队列的代码干净了,就停止循环了,不然就会一直循环。 看下面一串代码,会输出什么?

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

这段代码中,会先把setTimeout的方法移到队列中,当栈里的代码执行完之后,会把队列里方法取出来放到栈中执行,所以执行结果是:

1
ok
2
3
复制代码

再对这串代码进行扩展

console.log(1);
//A
setTimeout(function(){
    console.log(2);
    //C
    setTimeout(function(){
        console.log(4);
        //D
        setTimeout(function(){
            console.log(5);
        })
    })
},0)
//B
setTimeout(function(){
    console.log(3);
    //E
    setTimeout(function(){
        console.log(6);
    })
},0)
console.log('ok');
复制代码

这串代码中,栈的代码执行的时候,当触发回调函数时,会将回调函数放到队列中,所以,先输出1和ok。栈里的代码执行完之后,会先读取第一个setTimeout,输出2,这时发现里面还有一个setTimeout(既C行下的setTimeout),这个setTimeout又会放到队列中去。然后执行B行下的setTimeout,输出3,这时E行下还有个setTimeout,这个setTimeout又会放到队列中。当栈里代码执行完之后,又会在队列中读取代码,这时读取的是C行下的setTimeout,放到栈执行,输出4,紧接着又发现D行下有setTimeout,这个setTimeout又放到队列中排队。栈的代码执行完了,又在队列中读取E行下的setTimeout,输出6。执行完之后,又在队列里读取D行下的setTimeout,输出5。所以输出结果是:

1
ok
2
3
4
6
5
复制代码

附图讲解:

此处输入图片的描述

setTiemout(function(){
    console.log(1)
},0)
for(var i = 0;i<1000;i++){
    console.log(i)
}
复制代码

在当前队列里看到setTimeout,它会等着看事件什么时候成功。所以它会先往下走,走完以后,再把setTimeout里的回调函数放到队列中。即使for循环的代码走了10s,回调函数也会等到10s后再执行。 所以,浏览器的机制永远是:先走完栈里代码,才会到队列里去。


宏任务和微任务

任务可分为宏任务和微任务 宏任务:setTimeout,setInterval,setImmediate,I/O 微任务:process.nextTick,Promise.then 队列可以看成是一个宏任务。

微任务是怎么执行的? 同步代码先在栈中执行的,执行完之后,微任务会先执行,再执行宏任务。 先看一个例子:

console.log(1)
setTimeout(function(){
    console.log('setTimeout')
},0)
let promise = new Promise(function(resolve,reject){
    console.log(3);
    resolve(100);
}).then(function(data){
    console.log(200)
})
console.log(2)
复制代码

想一想会输出什么? 代码由上到下执行,所以肯定先输出1。setTimeout是宏任务,会先放到队列中。而new Promise是立即执行的,它是同步的,所以会先输出3。因为then是异步的,所以会先输出2。因为then是微任务,微任务走完,才会走宏任务。所以最终输出的结果是:1 3 2 200 setTimeout。 **注意:**浏览器的机制是把then方法放到微任务中。 浏览器机制:

此处输入图片的描述
代码会先走我们的执行栈,里面有变量,函数等等。栈的代码走完以后,会先去微任务,微任务里面可能有很多回调函数(比如:栈里有promise的then方法,then的回调函数会放到微任务里去),栈里面可能还有setTimeout,它会把setTimeout的回调函数放到宏任务中。什么时候放的呢?就是当时间到达的时候,会放到队列里。当栈的代码都执行完了,它会先取微任务的then,执行。执行完之后,再取宏任务的代码。(自己都快说晕了~~)

猜猜看:

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('ok')
    })
})
setTimeout(function(){
    console.log(3)
})
复制代码

你猜输出什么~ 分析:先默认走栈,输出1。此时并没有微任务,所以微任务不会执行。先走第一个setTimeout,输出2,同时将微任务放到队列中,执行微任务,输出ok,微任务执行完,再走宏任务,输出3。

**注意:**浏览器和node环境输出是不一样的哦~

---------此处是分割线------------

node的event loop

接下来说说node的事件环。 先画张图吧

此处输入图片的描述
由图可以看出微任务不在事件环里。那代码怎么走? 同样上面的例题:

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('ok')
    })
})
setTimeout(function(){
    console.log(3)
})
复制代码

先将2个定时器放到A中,先输出1;这时候栈里走完了,该走事件环了。在走事件环之前,会先将微任务清空,第一次微任务没有东西,就滤过了。之后该走事件环了,这时候先走timers。这时候setTimeout不一定到达时间,如果到达时间,就直接执行了。如果时间没到达,这时候可能先略过,接着往下走,走到poll轮询阶段,发现没有读文件之类的操作,然后它会等着,等到setTimeout的时间到达。如果时间到达了,它会把到达时间的定时器全部执行。比如先走第一个setTimeout,并且把then方法放到微任务中。它会把到达时间的setTimeout队列全部清掉(全部执行完),再走微任务。假如poll轮询有很多个I/O操作,它会把I/O操作都走完,再走timers。它是一个队列一个队列的清空,而不是取出一个,执行一下,取出一个,执行一下。所以它会把2个setTimeout都走完,再走then。所以在node的输出结果是:

1
2
3
ok
复制代码

再来个进阶的栗子:

process.nextTick(function(){
    console.log(1)
})
setImmediate(function(){
    console.log(2)
})
复制代码

它会先走栈的内容,栈啥都没有。当它要走事件环的时候,会将微任务清空。发现微任务有nextTick,它会把nextTick执行完,再走事件环。发现timers和poll都没有东西,它就会走theck阶段。 nextTick 和 then都是在阶段转化时才调用。所谓的阶段转化,就是刚开始走当前栈,在当前栈转到timers的时候,清空微任务。

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

Node.js的Event Loop

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

先说到这里吧,有欠缺的后续再补充。

关注下面的标签,发现更多相似文章
评论