相信所有學過JavaScript 都知道它是一門單線程的語言,這也意味著JS 無法進行多線程編程,但是JS 當中卻有著無處不在的異步概念。在初期許多人會把非同步理解成類似多執行緒的程式模式,其實他們中有著很大的差別,要完全理解異步,就需要了解JS 的運行核心-事件循環(event loop)。在之前我對事件循環的認識也是一知半解的,直到我看了Philip Roberts 的演講 What the heck is the event loop anyway?,我才對事件循環有了一個全面的認識,所以我想寫一篇介紹JS事件循環的文章,供大家學習和參考。
為什麼 JS 當中會有異步?我們想像一下,如果我們同步的執行一下程式碼會發生什麼:
1 $.get(url, function(data) {2 //do something3 });
在我們使用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 引擎便檢查事件佇列,如果不為空的話,事件佇列便將第一個任務壓入執行棧中執行。
現在,我們對上面的程式碼做一點修改:
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 something10 }11 12 console.log("script end");
你可以在这里查看结果:
可以看出,无论后面我们做了多少延迟性的工作,'setTimeout' 总是会在 'script end' 之后输出。所以究竟发生了什么?这是因为 setTimeout 的回调函数只是会被添加至事件队列,而不是立即执行。由于当前的任务没有执行结束,所以 setTimeout 任务不会执行,直到输出了 'script end' 之后,当前任务执行完毕,执行栈为空,这时事件队列才会把 setTimeout 回调函数压入执行栈执行。
执行栈则像是函数的调用栈,是一个树状的栈:
通过以上的 demo 相信同学们都会对事件队列和执行栈有了一个基本的认识,那么事件队列有何作用?最简单易懂的一点就是之前我们所提到的异步问题。由于 JS 是单线程的,同步执行任务会造成浏览器的阻塞,所以我们将 JS 分成一个又一个的任务,通过不停的循环来执行事件队列中的任务。这就使得当我们挂起某一个任务的时候可以去做一些其他的事情,而不需要等待这个任务执行完毕。所以事件循环的运行机制大致分为以下步骤:
检查事件队列是否为空,如果为空,则继续检查;如不为空,则执行 2;
取出事件队列的首部,压入执行栈;
执行任务;
检查执行栈,如果执行栈为空,则跳回第 1 步;如不为空,则继续检查;
然而目前为止我们讨论的仅仅是 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 上,如需轉載,註明下出處就好了。若您發現本文有所紕漏,歡迎在評論區指出。
以上是為什麼會有異步? 什麼是事件隊列?的詳細內容。更多資訊請關注PHP中文網其他相關文章!