详谈javascript和node的事件循环

Javascript中的事件循环javascript

javascript是一门单线程的非阻塞的脚本语言。单线程,即js代码在执行的任什么时候候,都只有一个主线程来处理全部任务。非阻塞,只要指的是执行异步任务(如I/O事件)时,主线程会挂起这个任务,而后在异步任务返回结果的时候再按照必定规则执行相应的回调。java

Web worker 技术所实现的多线程技术也存在诸多限制。如,全部新线程都受到主线程的彻底控制,不能独立执行。这意味着这些‘线程’其实是主线程的子线程。另外,这些子线程没有执行I/O操做的权限,只能为主线程分担一些如计算等任务。因此严格来说,web worker并无改变javascript的单线程本质。node

  1. 执行栈和同步执行

执行栈与存储对象指针和基础类型变量的栈是不一样的。执行栈是指,当调用一个方法时,js会生成与这个方法对应的一个执行环境(context),即执行上下文。这个执行环境中包含:这个执行环境的私有做用域、上层做用域的指向,方法的参数,私有变量以及该做用域的this指向。由于js是单线程的,同一时间只能执行一个方法,也就是说,当一个方法被执行的时候,其余方法会被排队到一个单独的地方,即执行栈。web

当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈,而后从头开始执行。当执行一个方法时,js会向执行栈中添加这个方法的执行环境,而后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码执行完毕并返回结果后,js会退出当前执行环境并撤销该环境,回到上一个方法的执行环境,这个过程反复执行,知道执行栈中的代码所有执行完毕。chrome

案列1:api

function Func1 () {浏览器

    console.log(1)多线程

    function Func2 () {异步

        console.log(2)ui

        function Func3 () {

            console.log(3)

        }

        Func3()

      }

      Func2()

}

Func1()

// 1 2 3

同步执行遵循先进后出的规则,在执行Func1时,会向执行栈加入该方法的执行环境,输出1,而后解析了Func2,执行时加入了Func2的执行环境,输出2,而后解析Func3并执行,输出3,Func3执行完毕后会撤销Func3的执行环境,接着是Func2执行完毕并撤销Func2的执行环境,最后撤销Func1的执行环境。该过程若没有终止,会无限进行直到栈溢出。

  1. 异步执行

方法执行时,异步执行事件挂起加入与执行栈不一样的另外一个队列,即事件队列中,并继续执行执行栈中的其余任务。被放入事件队列不会当即执行其回调,而是等待当前执行栈中的全部任务执行完毕,在主线程出于闲置状态时,主线程会查找事件队列是否有任务。若是有,则会取第一个事件并将该事件的回调放入执行栈中执行,而后执行其中的同步代码,如此反复就是事件循环。

异步任务由于各任务的不一样和执行优先级的区别,分为 宏任务 (macro task) 和 微任务 (micro task)

属于宏任务的事件:setTimeout(), setInterval()

属于微任务的事件:new Promise(), new MutaionObserver()(已废除)

当执行栈为空时,主线程会优先查看微任务是否有事件。若是没有,就会执行宏任务中的第一个事件并将对应的回调加入当前执行栈中;若是有,就会依次执行微任务中事件对应的回调,直到微任务队列为空,而后再执行宏任务中的第一个事件对应的回调,如此反复,进入循环。同一次事件循环中,微任务永远优先宏任务执行。

案列2:

setTimeout(function () {
    console.log(1);
});

new Promise(function(resolve,reject){
    console.log(2)
    resolve(3)
}).then(function(val){
    console.log(val);
})

// 2 3 1

node环境下的事件循环

在node中,事件循环与浏览器中的略有不一样。node中的事件循环的实现是依靠的libuv引擎。node选用chrome的v8引擎做为解释器,v8引擎将js代码解析后会调用node api,而api则是由libuv引擎驱动,所以node中的事件循环是在libuv引擎中执行。

node中,同步代码执行完,会先清空微任务队列,轮询时会清空当前队列全部任务,才会切换到下一个队列,在切换下一个队列以前也会先清空微任务队列。

  1. 事件循环模型


(来自:node官网)


  1. 事件循环说明

node的事件循环顺序:

外部输入数据—>poll阶段—>检查阶段(check)—>关闭事件回调阶段(close callback)—>定时器检测执行阶段(timers)—>I/O事件回调阶段(I/O callbacks)—>idle,prepare—>poll…

setTimeout(() => {console.log('setTimeout')} , 0)

setImmediate(() => {console.log('immediate')})

默认状况下setTimeout()和 setImmediate()不知道哪个会先执行,node执行也须要准备时间。setTimeout()延迟时间设置为0,实际仍是有4ms的延迟,假设node准备时间在4ms内,定时器没有执行,poll阶段没有执行setTimeout(),会先执行check中的setImmediate(),等到下一轮询进入时,poll检测到定时器已到时,再执行timer中的setTimeout()

队列中有一个特殊的推迟任务执行的方法process.nextTick再此执行。咱们知道,每一次事件循环都是从微任务开始的,而且每一阶段都是按照事件循环顺序进行执行。而在每一次的队列切换以前,都会检查nextTick queue中是否有事件,如有则优先执行。

案列3:

setImmediate(() => {

    console.log("setImmediate1");

    setTimeout(() => {

        console.log("setTimeout1");

    }, 0);

});

setTimeout(() => {

    process.nextTick(() => console.log("nextTick"));

    console.log("setTimeout2");

    setImmediate(() => {

        console.log("setImmediate2");

    });

}, 0);

// 结果一

// setImmediate1, setTimeout2, setTimeout1, nextTick, setImmediate2

// 结果二

// setTimeout2, nextTick, setImmediate1, setImmediate2, setTimeout1

产生上面两种结果的缘由,是node准备时间的差别。

案例4:

const fs = require('fs');

fs.readFile(__filename, () => {

    setImmediate(() => {

        console.log("setImmediate1");

        setTimeout(() => {

            console.log("setTimeout1");

        }, 0);

    });

    setTimeout(() => {

        process.nextTick(() => console.log("nextTick"));

        console.log("setTimeout2");

        setImmediate(() => {

            console.log("setImmediate2");

        });

      }, 0);

});

// setImmediate1, setTimeout2, setTimeout1, nextTick, setImmediate2

此时只会有一种结果,由于是在一个I/O事件的回调中,node准备已结束,setTimeout执行须要等待4ms,setImmediate则当即执行,又setTimeout2和setTimeout1在同一个timers队列中因此按顺序执行,以后须要切换到check队列执行setImmediate2,在切换以前会先检查nextTick队列并执行,所以最后输出nextTick,setImmediate2

注:欢迎你们监督指导,若有疑问或错误,请留言一块儿探讨~~

相关文章
相关标签/搜索