相信全部学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 没法进行多线程编程,可是 JS 当中却有着无处不在的异步概念 。在初期许多人会把异步理解成相似多线程的编程模式,其实他们中有着很大的差异,要彻底理解异步,就须要了解 JS 的运行核心——事件循环(event loop)。在以前我对事件循环的认识也是只知其一;不知其二的,直到我看了 Philip Roberts 的演讲 What the heck is the event loop anyway?,我才对事件循环有了一个全面的认识,因此我想写一篇介绍 JS 事件循环的文章,以供你们学习和参考。ajax
为何 JS 当中会有异步?咱们想象一下,若是咱们同步的执行一下代码会发生什么:编程
1 $.get(url, function(data) { 2 //do something 3 });
在咱们使用 ajax 进行通讯的时候,咱们都默认了它是异步的,可是若是咱们设置其为同步执行,会发生什么?若是你本身写一个小的测试程序,将后台代码延迟5s你会发现浏览器会出现阻塞,直到 ajax 响应了以后才会正常运行。这即是异步模式要解决的首要问题,如何使浏览器非阻塞的运行任务。想象一下若是咱们同步的执行 ajax 请求的话,咱们的等待的时间是一个未知数,在网络通讯中可能很快也可能很慢,也可能永远也不会响应,这也就会致使浏览器会阻塞在一个未知的任务上面,这也是咱们不但愿看到的。因此咱们但愿有一种方式可以异步的处理程序,咱们并不须要关心一个 ajax 请求会在什么时候完成,甚至它能够永远不会响应,咱们只须要知道在请求响应后该如何处理,而且在等待响应的这段时间内咱们还能够作一些其余的工做。所以,便有了 JavaScript Event Loop。数组
首先,咱们先来看一段简单的代码:浏览器
1 console.log("script start"); 2 3 setTimeout(function () { 4 console.log("setTimeout"); 5 }, 1000); 6 7 console.log("script end");
你能够在这里查看结果:网络
咱们能够看到,首先,程序输出 'script start' 和 'script end',在大约1s以后输出了 'setTimeout' 。该程序的 'script end' 并无等待1s以后输出,而是当即输出。这是由于 setTimeout 是一个异步的函数。意思也就是说当咱们设置一个延迟函数的时候,当前脚本并不会阻塞,它只是会在浏览器的事件表中进行记录,程序会继续向下执行。当延迟的时间结束以后,事件表会将回调函数添加至事件队列(task queue)中,事件队列拿到了任务事后便将任务压入执行栈(stack)当中,执行栈执行任务,输出 'setTimeout'。多线程
事件队列是一个存储着待执行任务的队列,其中的任务严格按照时间前后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕以后,再执行下一个任务。执行栈则是一个相似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,若是不为空的话,事件队列便将第一个任务压入执行栈中运行。异步
如今,咱们对上面的代码进行一点修改:async
1 console.log("script start"); 2 3 setTimeout(function () { 4 console.log("setTimeout"); 5 }, 0); 6 7 console.log("script end");
将延迟时间设置为0,看看程序会以何种顺序输出?不管咱们设置多少的延迟时间,'setTimeout' 老是会在 'script end' 以后输出。有些浏览器可能会有一个最小延迟时间,有的是 15ms,有的是 10ms,这个在不少书当中都有提到,这可能会给同窗们形成一种错觉:因为程序运行速度很快,而且有最小延迟时间,因此 'setTimeout' 会在 'script end' 以后输出。如今让咱们在稍微变一下,来消除你的错觉:函数
1 console.log("script start"); 2 3 setTimeout(function () { 4 console.log("setTimeout"); 5 }, 0); 6 7 //具体数字不定,这取决于你的硬件配置和浏览器 8 for(var i = 0; i < 999999999; i ++){ 9 //do something 10 } 11 12 console.log("script end");
你能够在这里查看结果:oop
能够看出,不管后面咱们作了多少延迟性的工做,'setTimeout' 老是会在 'script end' 以后输出。因此究竟发生了什么?这是由于 setTimeout 的回调函数只是会被添加至事件队列,而不是当即执行。因为当前的任务没有执行结束,因此 setTimeout 任务不会执行,直到输出了 'script end' 以后,当前任务执行完毕,执行栈为空,这时事件队列才会把 setTimeout 回调函数压入执行栈执行。
执行栈则像是函数的调用栈,是一个树状的栈:
经过以上的 demo 相信同窗们都会对事件队列和执行栈有了一个基本的认识,那么事件队列有何做用?最简单易懂的一点就是以前咱们所提到的异步问题。因为 JS 是单线程的,同步执行任务会形成浏览器的阻塞,因此咱们将 JS 分红一个又一个的任务,经过不停的循环来执行事件队列中的任务。这就使得当咱们挂起某一个任务的时候能够去作一些其余的事情,而不须要等待这个任务执行完毕。因此事件循环的运行机制大体分为如下步骤:
然而目前为止咱们讨论的仅仅是 JS 引擎如何执行 JS 代码,如今咱们结合 Web APIs 来讨论事件循环在当中扮演的角色。
在开始咱们讨论过 ajax 技术的异步性和同步性,经过事件循环机制,咱们则不须要等待 ajax 响应以后再进行工做。咱们则是设置一个回调函数,将 ajax 请求挂起,而后继续执行后面的代码,至于请求什么时候响应,对咱们的程序不会有影响,甚至它可能永远也不响应,也不会使浏览器阻塞。而当响应成功了之后,浏览器的事件表则会将回调函数添加至事件队列中等待执行。事件监听器的回调函数也是一个任务,当咱们注册了一个事件监听器时,浏览器事件表会进行登记,当咱们触发事件时,事件表便将回调函数添加至事件队列当中。
咱们知道 DOM 操做会触发浏览器对文档进行渲染,如修改排版规则,修改背景颜色等等,那么这类操做是如何在浏览器当中奏效的?至此咱们已经知道了事件循环是如何执行的,事件循环器会不停的检查事件队列,若是不为空,则取出队首压入执行栈执行。当一个任务执行完毕以后,事件循环器又会继续不停的检查事件队列,不过在这间,浏览器会对页面进行渲染。这就保证了用户在浏览页面的时候不会出现页面阻塞的状况,这也使 JS 动画成为可能, jQuery 动画在底层均是使用 setTimeout 和 setInterval 来进行实现。想象一下若是咱们同步的执行动画,那么咱们不会看见任何渐变的效果,浏览器会在任务执行结束以后渲染窗口。反之咱们使用异步的方法,浏览器会在每个任务执行结束以后渲染窗口,这样咱们就能看见动画的渐变效果了。
考虑以下两种遍历方式:
1 var arr = new Array(999); 2 arr.fill(1); 3 function asyncForEach(array, handler){ 4 var t = setInterval(function () { 5 if(array.length === 0){ 6 clearInterval(t); 7 }else { 8 handler(arr.shift()); 9 } 10 }, 0); 11 } 12 13 //异步遍历 14 asyncForEach(arr, function (value) { 15 console.log(value); 16 }); 17 18 //同步遍历 19 arr.forEach(function (value, index, arr) { 20 console.log(value); 21 });
通过测试,咱们能够看出,采用同步遍历的方法,当数组长度上升到3位数的时候,便会出现阻塞,可是异步遍历却不会出现阻塞现象(除非数组长度很是大,那是由于计算机的内存空间不足)。这是由于同步遍历方法是一个单独的任务,这个任务会将全部的数组元素遍历一遍,而后才会开始下一个任务。而异步遍历的方法将每一次遍历拆分红一个单独的任务,一个任务只遍历一个数组元素,因此在每一个任务之间,咱们的浏览器能够进行渲染,因此咱们不会看见阻塞的状况。下面这个 demo 演示了在异步遍历先后发生的事情:
如今,相信你已经认识了 JavaScript 的真实面目了吧。 JavaScript 是一门单线程的语言,可是其事件循环的特性使得咱们能够异步的执行程序。这些异步的程序也就是一个又一个独立的任务,这些任务包括了 setTimeout、setInterval、ajax、eventListener 等等。关于事件循环,咱们须要记住如下几点:
本文 demo 放在 jsfiddle 上,如需转载,注明下出处就行了。若您发现本文有所纰漏,欢迎在评论区指出。