目錄
首先,說下為什麼 JavaScript 是單線程?
並發模式與Event Loop
Runtime 概念
Stack(堆疊)
Heap(堆)
Queue(佇列)
Event Loop
「執行至完成」
絕不阻塞
計時器
計時器的一些概念
浏览器
浏览器不是单线程的
总结
首頁 web前端 js教程 詳細介紹JavaScript單線程的一些事(圖)

詳細介紹JavaScript單線程的一些事(圖)

Mar 10, 2017 pm 02:30 PM

詳細介紹JavaScript單線程的一些事(圖)

最近被同學問道 JavaScript 單線程的一些事,我竟回答不上。好吧,我覺得自己的 JavaScript 白學了。以下是我這幾天整理的一些關於 JavaScript 單線程的一些事。

首先,說下為什麼 JavaScript 是單線程?

總所周知,JavaScript是以單執行緒的方式運作的。說到執行緒就自然聯想到進程。那它們有什麼關聯呢?

行程和執行緒都是作業系統的概念。進程是應用程式的執行實例,每一個進程都是由私有的虛擬位址空間、程式碼、資料和其它系統資源所組成;進程在運行過程中能夠申請創建和使用系統資源(如獨立的記憶體區域等) ,這些資源也會隨著進程的終止而被銷毀。而執行緒則是進程內的一個獨立執行單元,在不同的執行緒之間是可以共享進程資源的,所以在多執行緒的情況下,需要特別注意對臨界資源的存取控制。在系統建立程序之後就開始啟動執行程序的主線程,而進程的生命週期和這個主線程的生命週期一致,主線程的退出也意味著進程的終止和銷毀。主線程是由系統進程所創建的,同時使用者也可以自主創建其它線程,這一系列的線程都會並發地運行於同一個進程中。

顯然,在多執行緒操作下可以實現應用的平行處理,從而以更高的CPU利用率提高整個應用程式的效能和吞吐量。特別是現在很多語言都支援多核心並行處理技術,然而JavaScript卻以單執行緒執行,為什麼呢?

其實這跟它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與使用者互動,以及操作DOM。若以多執行緒的方式操作這些DOM,則可能出現操作的衝突。假設有兩個執行緒同時操作一個DOM元素,執行緒1要求瀏覽器刪除DOM,而執行緒2卻要求修改DOM樣式,這時瀏覽器就無法決定採用哪個執行緒的操作。當然,我們可以為瀏覽器引入「鎖」的機制來解決這些衝突,但這會大大提高複雜性,所以 JavaScript 從誕生開始就選擇了單執行緒執行。

另外,因為 JavaScript 是單執行緒的,在某一時刻內只能執行特定的一個任務,並且會阻塞其它任務執行。那麼對於類似I/O等耗時的任務,就沒必要等他們執行完後才繼續後面的操作。在這些任務完成前,JavaScript完全可以往下執行其他操作,當這些耗時的任務完成後則以回呼的方式執行對應處理。這些就是JavaScript與生俱來的特性:非同步與回呼。

當然對於不可避免的耗時操作(如:繁重的運算,多重循環),HTML5提出了Web Worker,它會在當前JavaScript的執行主線程中利用Worker類別新開闢一個額外的線程來載入和運行特定的JavaScript文件,這個新的線程和JavaScript的主線程之間並不會互相影響和阻塞執行,而且在Web Worker中提供了這個新線程和JavaScript主線程之間數據交換的接口: postMessage和onMessage事件。但在HTML5 Web Worker中是不能操作DOM的,任何需要操作DOM的任務都需要委託給JavaScript主執行緒來執行,所以雖然引入HTML5 Web Worker,但仍然沒有改線JavaScript單執行緒的本質。

並發模式與Event Loop

JavaScript 有個基於「Event Loop」並發的模型。

啊,並發?不是說 JavaScript是單線程嗎? 沒錯,的確是單線程,但是並發與並行是有差別的。前者是邏輯上的同時發生,而後者是物理上的同時發生。所以,單核心處理器也能實現並發。

詳細介紹JavaScript單線程的一些事(圖)
並發與並行

並行大家都好理解,而所謂「並發」是指兩個或兩個以上的事件在同一時間間隔中發生。如上圖的第一個表,由於電腦系統只有一個CPU,故ABC三個程式從「微觀」上是交替使用CPU,但交替時間很短,用戶察覺不到,形成了「宏觀」意義上的並發操作。

Runtime 概念

下面的內容解釋一個理論上的模型。現代 JavaScript 引擎已著重實現和優化了以下所描述的幾個概念。

Stack(堆疊)

這裡放著JavaScript正在執行的任務。每個任務被稱為幀(stack of frames)。

function f(b){
  var a = 12;
  return a+b+35;
}

function g(x){
  var m = 4;
  return f(m*x);
}

g(21);
登入後複製

上述程式碼呼叫 g 時,建立堆疊的第一幀,該幀包含了 g 的參數和局部變數。當 g 呼叫 f 時,第二幀就會被創建,並且置於第一幀之上,當然,該幀也包含了 f 的參數和局部變數。當 f 返回時,其對應的訊框就會出棧。同理,當 g 返回時,堆疊就為空了(堆疊的特定就是後進先出 Last-in first-out (LIFO))。

Heap(堆)

一個用來表示記憶體中一大片非結構化區域的名字,物件都被分配在這裡。

Queue(佇列)

一個 JavaScript runtime 包含了一個任務佇列,該佇列由一系列待處理的任務組成。而每個任務都有相對應的函數。當棧為空時,就會從任務佇列中取出一個任務,並處理之。此處理會呼叫與該任務相關聯的一系列函數(因此會建立初始堆疊幀)。當該任務處理完畢後,棧就會再次為空。 (Queue的特色是先進先出 First-in First-out (FIFO))。

為了方便描述與理解,作出以下約定:

  • Stack堆疊為主執行緒

  • Queue佇列為任務佇列(等待調度到主執行緒執行)

OK,上述知識點幫助我們釐清了一個JavaScript runtime 的相關概念,這有助於接下來的分析。

Event Loop

之所以被稱為Event loop,是因為它以以下類似方式實作:

while(queue.waitForMessage()){
  queue.processNextMessage();
}
登入後複製

如上述所說,「任務佇列」是一個事件的隊列,如果I/O設備完成任務或用戶觸發事件(該事件指定了回調函數),那麼相關事件處理函數就會進入“任務隊列”,當主線程空閒時,就會調度“任務隊列”裡第一個待處理任務,(FIFO)。當然,對於定時器,當到達其指定時間時,才會將對應任務插到「任務佇列」尾部。

「執行至完成」

每當某個任務執行完後,其它任務才會被執行。也就是說,當一個函數運行時,它不能被取代且會在其它程式碼運行前先完成。
當然,這也是Event Loop的缺點:當一個任務完成時間過長,那麼應用程式就無法及時處理使用者的互動(如點擊事件),甚至導致該應用程式奔潰。一個比較好解決方案是:將任務完成時間縮短,或盡可能將一個任務分成多個任務執行。

絕不阻塞

JavaScript與其它語言不同,其Event Loop的一個特性是永遠不會阻塞。 I/O操作通常是透過事件和回呼函數處理。所以,當應用程式等待 indexedDB 或 XHR 非同步請求返回時,其仍能處理其它操作(如使用者輸入)。

例外是存在的,如alert或同步XHR,但避免它們被認為是最佳實踐。注意的是,例外的例外也是存在的(但通常是實現錯誤而非其它原因)。

計時器

計時器的一些概念

上面也提到,在到達指定時間時,計時器就會將對應回呼函數插入「任務佇列」尾部。這就是「定時器(timer)」功能。

計時器包含setTimeout與setInterval兩個方法。它們的第二個參數是指定其回呼函數推遲\每隔多少毫秒數後執行。
對於第二個參數有以下需要注意的地方:

  • 當第二個參數缺省時,預設為0;

  • 當指定的值小於4毫秒,則增加到4ms(4ms是HTML5標準指定的,對於2010年及之前的瀏覽器則是10ms);

##如果你理解上述知識,那麼以下程式碼就應該對你沒什麼問題了:

console.log(1);
setTimeout(function(){
    console.log(2);
},10);
console.log(3);
// 输出:1 3 2
登入後複製

深入了解計時器

零延遲setTimeout(func, 0)

  • 零延遲並不是意味著回呼函數立刻執行。它取決於主執行緒目前是否空閒與「任務佇列」裡其前面正在等待的任務。

    看看以下程式碼:
  • (function () {
    
      console.log('this is the start');
    
      setTimeout(function cb() {
        console.log('this is a msg from call back');
      });
    
      console.log('this is just a message');
    
      setTimeout(function cb1() {
        console.log('this is a msg from call back1');
      }, 0);
    
      console.log('this is the  end');
    
    })();
    
    // 输出如下:
    this is the start
    this is just a message
    this is the end
    undefined // 立即调用函数的返回值
    this is a msg from callback
    this is a msg from a callback1
    登入後複製
  • setTimeout(func, 0)的作用

讓瀏覽器渲染目前的變化(很多瀏覽器UI render和js執行是放在一個線程中,線程阻塞會導致介面無法更新渲染)

重新評估”scriptis running too long”警告

改變執行順序############再看看以下程式碼:###
<button id=&#39;do&#39;> Do long calc!</button>
<p id=&#39;status&#39;></p>
<p id=&#39;result&#39;></p>

$(&#39;#do&#39;).on(&#39;click&#39;, function(){

  $(&#39;#status&#39;).text(&#39;calculating....&#39;);// 此处会触发redraw事件,但会放到队列里执行,直到long()执行完。

  // 没设定定时器,用户将无法看到“calculating...”
  long();// 执行长时间任务,造成阻塞

  // 设定了定时器,用户就如期看到“calculating...”
  //setTimeout(long,50);// 大约50ms后,将耗时长的long回调函数插入“任务队列”末尾,根据先进先出原则,其将在redraw之后被调度到主线程执行

 });

function long(){
  var result = 0
  for (var i = 0; i<1000; i++){
    for (var j = 0; j<1000; j++){
      for (var k = 0; k<1000; k++){
        result = result + i+j+k
      }
    } 
  }
  $(&#39;#status&#39;).text(&#39;calclation done&#39;); // 在本案例中,该语句必须放到这里,这将使它与回调函数的行为类似
}
登入後複製
######正版與翻版setInterval的差異#### #####大家可能都知道透過setTimeout可以模仿setInterval的效果,下面我們來看看以下程式碼的差別:###
// 利用setTimeout模仿setInterval
setTimeout(function(){
    /* 执行一些操作. */
    setTimeout(arguments.callee, 10);
}, 1000);

setInterval(function(){
    /* 执行一些操作 */
}, 1000);
登入後複製
###可能你認為這沒什麼差別。的確,當回呼函數裡的操作耗時很短時,並不能看出它們有什麼不同。 ###

其实:上面案例中的 setTimeout 总是会在其回调函数执行后延迟 10ms(或者更多,但不可能少)再次执行回调函数,从而实现setInterval的效果,而 setInterval 总是 10ms 执行一次,而不管它的回调函数执行多久。

所以,如果 setInterval 的回调函数执行时间比你指定的间隔时间相等或者更长,那么其回调函数会连在一起执行。

你可以试试运行以下代码:

var counter = 0;
var initTime = new Date().getTime();
var timer = setInterval(function(){
    if(counter===2){
        clearInterval(timer);
    }
    if(counter === 0){
        for(var i = 0; i < 1990000000; i++){
            ;
        }
    }

    console.log("第"+counter+"次:" + (new Date().getTime() - initTime) + " ms");

    counter++;
},1000);
登入後複製

我电脑Chrome浏览器的输入如下:

第0次:2007 ms
第1次:2013 ms
第2次:3008 ms
登入後複製

浏览器

浏览器不是单线程的

上面说了这么多关于JavaScript是单线程的,下面说说其宿主环境——浏览器。

浏览器的内核是多线程的,它们在内核制控下相互配合以保持同步,一个浏览器至少实现三个常驻线程:

  1. javascript引擎线程 javascript引擎是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序。

  2. GUI渲染线程 GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。但需要注意GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

  3. 浏览器事件触发线程 事件触发线程,当一个事件被触发时该线程会把事件添加到“任务队列”的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS是单线程执行的,所有这些事件都得排队等待JS引擎处理。

在Chrome浏览器中,为了防止因一个标签页奔溃而影响整个浏览器,其每个标签页都是一个进程。当然,对于同一域名下的标签页是能够相互通讯的,具体可看 浏览器跨标签通讯。在Chrome设计中存在很多的进程,并利用进程间通讯来完成它们之间的同步,因此这也是Chrome快速的法宝之一。对于Ajax的请求也需要特殊线程来执行,当需要发送一个Ajax请求时,浏览器会开辟一个新的线程来执行HTTP的请求,它并不会阻塞JavaScript线程的执行,当HTTP请求状态变更时,相应事件会被作为回调放入到“任务队列”中等待被执行。

看看以下代码:

document.onclick = function(){
    console.log("click")
}

for(var i = 0; i< 100000000; i++);
登入後複製

解释一下代码:首先向document注册了一个click事件,然后就执行了一段耗时的for循环,在这段for循环结束前,你可以尝试点击页面。当耗时操作结束后,console控制台就会输出之前点击事件的”click”语句。这视乎证明了点击事件(也包括其它各种事件)是由额外单独的线程触发的,事件触发后就会将回调函数放进了“任务队列”的末尾,等待着JavaScript主线程的执行。

总结

  • JavaScript是单线程的,同一时刻只能执行特定的任务。而浏览器是多线程的。

  • 异步任务(各种浏览器事件、定时器等)都是先添加到“任务队列”(定时器则到达其指定参数时)。当Stack栈(JS主线程)为空时,就会读取Queue队列(任务队列)的第一个任务(队首),然后执行。

JavaScript为了避免复杂性,而实现单线程执行。而今JavaScript却变得越来越不简单了,当然这也是JavaScript迷人的地方。


以上是詳細介紹JavaScript單線程的一些事(圖)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

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

AI Clothes Remover

AI Clothes Remover

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

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
威爾R.E.P.O.有交叉遊戲嗎?
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

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

如何使用WebSocket和JavaScript實現線上語音辨識系統 如何使用WebSocket和JavaScript實現線上語音辨識系統 Dec 17, 2023 pm 02:54 PM

如何使用WebSocket和JavaScript實現線上語音辨識系統引言:隨著科技的不斷發展,語音辨識技術已成為了人工智慧領域的重要組成部分。而基於WebSocket和JavaScript實現的線上語音辨識系統,具備了低延遲、即時性和跨平台的特點,成為了廣泛應用的解決方案。本文將介紹如何使用WebSocket和JavaScript來實現線上語音辨識系

WebSocket與JavaScript:實現即時監控系統的關鍵技術 WebSocket與JavaScript:實現即時監控系統的關鍵技術 Dec 17, 2023 pm 05:30 PM

WebSocket與JavaScript:實現即時監控系統的關鍵技術引言:隨著互聯網技術的快速發展,即時監控系統在各個領域中得到了廣泛的應用。而實現即時監控的關鍵技術之一就是WebSocket與JavaScript的結合使用。本文將介紹WebSocket與JavaScript在即時監控系統中的應用,並給出程式碼範例,詳細解釋其實作原理。一、WebSocket技

如何利用JavaScript和WebSocket實現即時線上點餐系統 如何利用JavaScript和WebSocket實現即時線上點餐系統 Dec 17, 2023 pm 12:09 PM

如何利用JavaScript和WebSocket實現即時線上點餐系統介紹:隨著網路的普及和技術的進步,越來越多的餐廳開始提供線上點餐服務。為了實現即時線上點餐系統,我們可以利用JavaScript和WebSocket技術。 WebSocket是一種基於TCP協定的全雙工通訊協議,可實現客戶端與伺服器的即時雙向通訊。在即時線上點餐系統中,當使用者選擇菜餚並下訂單

如何使用WebSocket和JavaScript實現線上預約系統 如何使用WebSocket和JavaScript實現線上預約系統 Dec 17, 2023 am 09:39 AM

如何使用WebSocket和JavaScript實現線上預約系統在當今數位化的時代,越來越多的業務和服務都需要提供線上預約功能。而實現一個高效、即時的線上預約系統是至關重要的。本文將介紹如何使用WebSocket和JavaScript來實作一個線上預約系統,並提供具體的程式碼範例。一、什麼是WebSocketWebSocket是一種在單一TCP連線上進行全雙工

JavaScript與WebSocket:打造高效率的即時天氣預報系統 JavaScript與WebSocket:打造高效率的即時天氣預報系統 Dec 17, 2023 pm 05:13 PM

JavaScript和WebSocket:打造高效的即時天氣預報系統引言:如今,天氣預報的準確性對於日常生活以及決策制定具有重要意義。隨著技術的發展,我們可以透過即時獲取天氣數據來提供更準確可靠的天氣預報。在本文中,我們將學習如何使用JavaScript和WebSocket技術,來建立一個高效的即時天氣預報系統。本文將透過具體的程式碼範例來展示實現的過程。 We

簡易JavaScript教學:取得HTTP狀態碼的方法 簡易JavaScript教學:取得HTTP狀態碼的方法 Jan 05, 2024 pm 06:08 PM

JavaScript教學:如何取得HTTP狀態碼,需要具體程式碼範例前言:在Web開發中,經常會涉及到與伺服器進行資料互動的場景。在與伺服器進行通訊時,我們經常需要取得傳回的HTTP狀態碼來判斷操作是否成功,並根據不同的狀態碼來進行對應的處理。本篇文章將教你如何使用JavaScript來取得HTTP狀態碼,並提供一些實用的程式碼範例。使用XMLHttpRequest

javascript如何使用insertBefore javascript如何使用insertBefore Nov 24, 2023 am 11:56 AM

用法:在JavaScript中,insertBefore()方法用於在DOM樹中插入一個新的節點。這個方法需要兩個參數:要插入的新節點和參考節點(即新節點將要插入的位置的節點)。

Golang 的單線程特性及優勢 Golang 的單線程特性及優勢 Mar 18, 2024 am 11:51 AM

Golang的單執行緒特性及優勢隨著網路和行動應用的蓬勃發展,對於高效能、高並發的程式語言需求日益增加。在這種背景下,Go語言(簡稱Golang)由Google公司開發並於2009年首次發布,迅速受到廣大開發者的歡迎。 Golang是一種使用靜態型別、並發設計的開源程式語言,其最大的優點之一就是其單執行緒特性。 Golang採用Goroutine的並發模型,

See all articles