為什麼會有異步? 什麼是事件隊列?
引言
相信所有學過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中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

人臉偵測辨識技術已經是一個比較成熟且應用廣泛的技術。而目前最廣泛的網路應用語言非JS莫屬,在Web前端實現人臉偵測辨識相比後端的人臉辨識有優勢也有弱勢。優點包括減少網路互動、即時識別,大大縮短了使用者等待時間,提高了使用者體驗;弱勢是:受到模型大小限制,其中準確率也有限。如何在web端使用js實現人臉偵測呢?為了實現Web端人臉識別,需要熟悉相關的程式語言和技術,如JavaScript、HTML、CSS、WebRTC等。同時也需要掌握相關的電腦視覺和人工智慧技術。值得注意的是,由於Web端的計

js和vue的關係:1、JS作為Web開發基石;2、Vue.js作為前端框架的崛起;3、JS與Vue的互補關係;4、JS與Vue的實踐應用。

lambda表達式跳出循環,需要具體程式碼範例在程式設計中,循環結構是常用的一種重要語法。然而,在特定的情況下,我們可能希望在循環體內滿足某個條件時,跳出整個循環,而不是僅僅終止當前的循環迭代。在這個時候,lambda表達式的特性可以幫助我們達成跳脫循環的目標。 lambda表達式是一種匿名函數的宣告方式,它可以在內部定義簡單的函數邏輯。它與普通的函數聲明不同,

jQuery是一個受歡迎的JavaScript函式庫,可以用來簡化DOM操作、事件處理、動畫效果等。在web開發中,常常會遇到需要對select元素進行改變事件綁定的情況。本文將介紹如何使用jQuery實作對select元素改變事件的綁定,並提供具體的程式碼範例。首先,我們需要使用標籤來建立一個包含選項的下拉式選單:

這篇文章將為大家詳細講解有關PHP返回數組所有值,組成一個數組,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章後可以有所收穫。使用array_values()函數array_values()函數傳回陣列中所有值的陣列。它不會保留原始數組的鍵。 $array=["foo"=>"bar","baz"=>"qux"];$values=array_values($array);//$values將是["bar","qux"]使用迴圈可以使用循環手動獲取數組的所有值並將其新增至一個新

在PHP中建構基於事件的應用程式的方法包括:使用EventSourceAPI建立事件來源,並在客戶端使用EventSource物件監聽事件。使用伺服器傳送的事件(SSE)傳送事件,並在客戶端使用XMLHttpRequest物件監聽事件。一個實用的例子是在電子商務網站中使用EventSource即時更新庫存計數,在伺服器端透過隨機更改庫存並發送更新來實現,客戶端則透過EventSource監聽庫存更新並即時顯示。

Iterator介面Iterator介面是一個用於遍歷集合的介面。它提供了幾個方法,包括hasNext()、next()和remove()。 hasNext()方法傳回布林值,指示集合中是否還有下一個元素。 next()方法傳回集合中的下一個元素,並將其從集合中刪除。 remove()方法從集合中刪除目前元素。以下程式碼範例示範如何使用Iterator介面來遍歷集合:Listnames=Arrays.asList("John","Mary","Bob");Iterator

深入理解jQuery中的關閉按鈕事件在前端開發過程中,經常會遇到需要實現關閉按鈕功能的情況,例如關閉彈跳窗、關閉提示框等。而在使用jQuery這個流行的JavaScript函式庫時,實作關閉按鈕事件也變得異常簡單又方便。本文將深入探討如何利用jQuery來實現關閉按鈕事件,並提供具體的程式碼範例,幫助讀者更好地理解和掌握這個技術。首先,我們需要了解在HTML中如何定
