关于js事件循环的那点一知半解

起源

最近在回顾一些知识点,也解惑了一些之前不懂的知识点,在这个过程中也饶有兴致的搞了一个个人博客,现在看来还不错,即可以总结记录一些知识点,又可以分享,虽然现在是给自己的看到哈哈哈~~~

其中在网上看到了一道面试题,跟之前去某公司给的一道面试题一毛一样,涉及到的是长数组递归可能造成js的堆栈溢出,而后又从堆栈溢出引申了解到了JS的事件循环,总算是对此有了一知半解。

先看看下面这道面试题:

如果 list 很大,下面的这段递归代码会造成堆栈溢出。如果在不改变递归模式的前提下修善这段代码:

    var list = readHugeList();

    var nextListItem = function() {
        var item = list.pop();

        if (item) {
                // process the list item...
            nextListItem();
        }
    };

之前是刚毕业不久去某公司碰到的一道面试题,当时对js的一些知识掌握的不是很深刻,这道题完全不知道怎么去解决?后面这道就没写,最后面试以失败告终。不过那次之后也发现自己的不足,任重而道远啊。其他废话不多说了,进入正题吧!

上面那道题的答案是加一个setTimeout的定时器:

    var list = readHugeList();

    var nextListItem = function() {
        var item = list.pop();

        if (item) {
            // process the list item...
            setTimeout( nextListItem, 0);
        }
    };

之后还是有些困惑不解,后面查了一些资料之后,原来涉及到了JavaScript的事件循环机制。

JS的事件循环机制

我们都知道,JS是单线程的编程语言,即在js运行时,所有的任务是一个队列,只有前一个任务完成了,下一个任务才会继续执行。因此,如果前面一个任务耗时很长,那么下一个任务就要长时间等待,可想而知,这样的处理任务效率非常低。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

由此,JavaScript的运行机制可以简单理解为一个主线程和一个任务队列,脚本运行时先运行主线程,主线程运行完后,从”任务队列”中读取事件,运行任务队列的任务,这个过程是循环不断的,又称Event Loop(事件循环)。注意只要主线程的任务没有运行完,异步队列中的任务永远也无法运行,所以会导致浏览器出现假死的情况。我们可以看一下图解(转引自阮老师的JavaScript 运行机制详解:再谈Event Loop)。

图解

解决

所谓的事件循环不是单纯只有JS的事件类型去触发,类似setTimeout开启的延时线程,当主线程运行之后,在一定的时间会触发setTimeout的异步任务,此时任务便从消息队列移除进入主线程完成执行,所以setTimeout亦可以看做是由时间的长短来定时触发的一种事件.

我们来看下面的代码:

    console.log('a');
    setTimeout(function(){
        console.log('b');
    },0);
    console.log('c');

上述运行结果依次输出的是 acb;原因是js执行时同步加以上任务放入主线程,而遇到setTimeout之后开启了一个延时线程,进入了异步任务队列,等到主线程的任务都完成之后,再读取任务队列,所以输出的结果是acb。
需要进一步解释的是:这里的零延迟不是说setTimeout会被立刻执行,是说等到主线程执行完成,并且任务队列中没有其他任务时,setTimeout的回调函数才会被执行,最重要的一点就是setTimeout设置的时间并非回调函数会在这个时间间隔之后立即运行,仅仅表示的是最少时间而非确切的时间,因为主线程任务需要执行完成为空时,才会处理任务队列的消息处理即回调函数,所以setTimeout执行回调函数所等待的时间比它设定的时间参数要长。

现在我们可以回过头去解释刚开始那道用setTimeout来解决内存溢出的面试题:

在没有用setTimeout进行递归时,当数组过大,不断进行递归调用,主线程的任务数量会不断叠加,当达到一定大小就会造成堆栈溢出,而当递归函数放到setTimeout进行异步执行,此时相当于开了另外一个延时线程,主线程就负责调用setTimeout这个异步线程执行任务就可以,一执行完成就释放,就不会造成堆栈溢出。