起源
最近在回顾一些知识点,也解惑了一些之前不懂的知识点,在这个过程中也饶有兴致的搞了一个个人博客,现在看来还不错,即可以总结记录一些知识点,又可以分享,虽然现在是给自己的看到哈哈哈~~~
其中在网上看到了一道面试题,跟之前去某公司给的一道面试题一毛一样,涉及到的是长数组递归可能造成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这个异步线程执行任务就可以,一执行完成就释放,就不会造成堆栈溢出。