確認過眼神 JavaScript是單線程的

簡介

起初我是用C#寫服務端窗體應用的,所以對JavaScript幾乎一竅不通,纔開始就是這樣的:

momoda

起初JavaScript是否是單線程的,我並不在乎的,纔開始我的狀態是這樣的:

路人甲: JavaScript是單線程的!
Me: 哇哦!,原來他是單線程的,哦,然後呢?

路人已:JavaScript是事件輪詢的!
Me: 哦,知道了,然後呢?

路人丙:爲什麼JavaScript是單線程的呢?
Me: 額……不知道,→_→,JavaScript不就是寫用戶交互操作的腳本嗎?我管他是不是單線程的。

最開始我就是聽別人說什麼就是什麼。但具體是什麼原因我是完全不關心的

直到我瞭解到NodeJS能做服務端程序以後,開始了我的JavaScript之旅,經過了漫長的三個月時間至現在最近一個星期,纔算是對JavaScript有了新的認識,就在最近一星期,我對回調函數,異步任務,瀏覽器內核,JavaScript引擎,有了比較新的認識,而是到了這個點以後,全部銜接在一起了,雞凍啊…加油!


爲什麼JavaScript是單線程的?

JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。那麼,爲什麼JavaScript不能有多個線程呢?這樣能提高效率啊。
JavaScript的單線程,與它的用途有關。作爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程爲準?
所以,爲了避免複雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,將來也不會改變。
爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單線程的本質。

瀏覽器的主要構成

瀏覽器的主要構成,先上圖 我們再扯淡:

這裏寫圖片描述

我個人大致把瀏覽器的構成分爲三大部分:
  1. 用戶界面
  
  2. 瀏覽器內核(瀏覽器引擎,渲染引擎,JavaScript引擎(也叫JavaScript解析器),網絡)
   其中渲染引擎用來解析執行Html與CSS的
   JavaScript引擎是用來解析執行JavaScript代碼
  
  3. 保存類似Cookie的各種數據


瀏覽器的主要組件包括:
  1. 用戶界面 - 包括地址欄、後退/前進按鈕、書籤目錄等,也就是你所看到的除了用來顯示你所請求頁面的主窗口之外的其他部分。
  2. 瀏覽器引擎 - 用來查詢及操作渲染引擎的接口。
  3. 渲染引擎 - 用來顯示請求的內容,例如,如果請求內容爲html,它負責解析html及css,並將解析後的結果顯示出來。
  4. 網絡 - 用來完成網絡調用,例如http請求,它具有平臺無關的接口,可以在不同平臺上工作。
  5. UI後端 - 用來繪製類似組合選擇框及對話框等基本組件,具有不特定於某個平臺的通用接口,底層使用操作系統的用戶接口。
  6. JavaScript解釋器 - 用來解釋執行JS代碼。
  7. 數據存儲 - 屬於持久層,瀏覽器需要在硬盤中保存類似cookie的各種數據,HTML5定義了web database技術,這是一種輕量級完整的客戶端存儲技術
  
 


閒談-爲什麼說Chrome運行很快

Chrome的瀏覽器內核用的是Webkit,JavaScript引擎用的是V8

在Chrome中,只有Html的渲染採用了WebKit的代碼,而在JavaScript上,重新搭建了一個NB哄哄的V8引擎。目標是,用WebKit + V8的強強聯手,打造一款上網衝浪的法拉利.
(1) V8在執行之前將JavaScript編譯成了機器碼,而非位元組碼或是直譯它,以此提升效能。更進一步,使用瞭如內聯緩存(inline caching)等方法來提高性能。有了這些功能,JavaScript程序與V8引擎的速度媲美二進制編譯。

(2) 傳統的javascript是動態語言.JavaScript繼承方法是使用prototype,透過指定prototype屬性,便可以指定要繼承的目標。屬性可以在運行時添加到或從對象中刪除,引擎會爲執行中的物件建立一個屬性字典,新的屬性都要透過字典查找屬性在內存中的位置。V8爲object新增屬性的時候,就以上次的hidden class爲父類別,創建新屬性的hidden class的子類別,如此一來屬性訪問不再需要動態字典查找了。

(3) 爲了縮短由垃圾收集造成的停頓,V8使用stop-the-world, generational, accurate的垃圾收集器

JavaScript引擎(JavaScript解釋器)

還是老規矩,先上圖 ,再扯淡:

這裏寫圖片描述

(1).JavaScript 引擎的基本工作是把開發人員寫的 JavaScript 代碼轉換成高效、優化的代碼。它主要就是,分析、解釋、優化、垃圾回收 JavaScript 代碼

(2).JavaScript引擎是一個“進程虛擬機”,它給JavaScript代碼提供了運行環境,用於執行JavaScript代碼

(3).JavaScript引擎是單線程的,維護了一個事件隊列(和瀏覽器事件輪詢有關係)

(4).JavaScript引擎根據ECMAScript定義的語言的標準來實現

注: “虛擬機”是指軟件驅動的給定的計算機系統的模擬器。有很多類型的虛擬機,它們根據自己在多大程度上精確地模擬或代替真實的物理機器來分類。
“系統虛擬機”提供了一個可以運行操作系統的完整仿真平臺
“進程虛擬機”不具備全部的功能,能運行一個程序或者進程

事件輪詢-Event Loop

Event Loop:其實也就是JavaScript引擎一直在執行任務隊列中的任務。

由於JavaScript引擎是單線程的,單線程意味着所有任務需要排隊,前一個任務結束,纔會執行下一個任務,假設第一個任務是執行(a++;),第二個任務是通過ajax從網絡讀取數據-[很耗時的任務],假如直接放在JavaScript引擎中執行,那麼JavaScript引擎會一直等待服務端的數據(這時就阻塞線程了),JavaScript引擎會一直等待到數據從服務端傳遞過來纔會執行下一個任務,同時,在JavaScript等待的時候,CPU是空閒的,大大的資源浪費啊!,也會出現界面“假死”狀態,那麼怎麼辦呢?

解:瀏覽器內核是多線程的
1.Ajax操作是由瀏覽器內核中的瀏覽器Http異步線程執行的:發送—-等待—-接收
2.JavaScript引擎遇到Ajax操作會交給瀏覽器內核中的Http異步線程執行,從而自身繼續執行下面的任務
3.當Http異步線程接收到數據以後,數據會以回調函數的形式放入任務隊列中,JavaScript下次空閒的時候執行該回調函數。

異步任務簡單點說 就是不佔用當前線程,通過當前線程交給其他線程處理的任務,其他線程處理完畢後,再以回調函數的方式通知當前線程。

這裏寫圖片描述

JavaScript引擎是單線程運行的,瀏覽器無論在什麼時候都只且只有一個線程在運行JavaScript程序,但是瀏覽器內核是多線程的

(1). 瀏覽器內核實現允許多個線程異步執行,這些線程在內核制控下相互配合以保持同步.假如某一瀏覽器內核的實現至少有三個常駐線程:javascript引擎線程,界面渲染線程,瀏覽器事件觸發線程,除些以外,也有一些執行完就終止的線程,如Http請求線程,這些異步線程都會產生不同的異步事件.
(2). 界面渲染線程負責渲染瀏覽器界面HTML元素,當界面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該線程就會執行.本文雖然重點解釋JavaScript定時機制,但這時有必要說說渲染線程,因爲該線程與JavaScript引擎線程是互斥的,這容易理解,因爲 JavaScript腳本是可操縱DOM元素,在修改這些元素屬性同時渲染界面,那麼渲染線程前後獲得的元素數據就可能不一致了.
在JavaScript引擎運行腳本期間,瀏覽器渲染線程都是處於掛起狀態的,也就是說被“凍結”了.

接下來我們用代碼來論證上面所說的:

這裏寫圖片描述

                window.onload = function() {

            var date1 = new Date();
            //異步任務-js引擎線程發現setTimeout這個方法,然後通知瀏覽器內核啓動瀏覽器定時線程,瀏覽器定時線程開始定時,js引擎線程執行這個代碼塊只花了不到1毫秒的時間,然後js引擎就繼續往下執行
            //當定時線程到了30s後,就把回調函數放在js引擎隊列裏面,JS引擎會一直遍歷自己的隊列,是否有任務要處理,如果js引擎隊列正在執行其他方法,那麼該回調函數就會等其他任務執行完了再執行,如果js引擎是空閒的,那麼就會立即執行
            setTimeout(function() {
                alert("setTimeOut Finish");
            }, 1000 * 30);

            var date2 = new Date();
            var haomiao = date2.getTime() - date1.getTime();
            console.log(date1.getMilliseconds() - date2.getMilliseconds())
//同步任務-立即執行 不會等待30秒後再執行
var a=0;
console.log(a++);
            //同步任務-立即執行,當JS引擎執行在這裏的時候,JS引擎是空閒的,JS引擎就立即執行該方法 ,因爲每次循環都在佔用js線程,所以js引擎不會執行下面的方法
            delayTwentyMilliseconds();

            //異步方法-js引擎 -發現ClickMe() 交給瀏覽器內核,瀏覽器內核再交給瀏覽器事件觸發線程,瀏覽器事件觸發線程就會註冊點擊事件ClickMe,然後Js引擎就不管,然後js引擎就繼續往下執行
            function clickMe() {
                var date1 = new Date();
                var date2 = new Date();
                var haomiao = date2.getTime() - date1.getTime();
                alert("點擊完成時間執行完成 耗時:" + haomiao / 1000 + '秒');

            }
            //異步方法
            setTimeout(function() {
                alert("setTimeOut Finish");
            }, 1000 * 30);
        }


//定義執行一秒的同步方法
        function delayOneMilliseconds() {
            for (var i = 1; i < 1000; i++) {
                for (var j = 1; j < 10; j++) {
                    for (var k = 1; k < 10; k++) {
                        var b = k * 10;
                    }
                }
            }
        }
//定義執行20秒的同步方法
        function delayTwentyMilliseconds() {
            for (var i = 1; i < 10000; i++) {
                for (var j = 1; j < 1000; j++) {
                    for (var k = 1; k < 2000; k++) {
                        var b = k * 100;
                    }
                }
            }
        }

總結:
1.JavaScript引擎一直在執行任務隊列中的任務,當遇到同步任務的時候會立即執行,遇到異步任務會交給瀏覽器內核的其他線程執行,當其他線程執行完畢,會以回調函數任務的形式放入到JavaScript任務隊列中,JavaScript引擎會繼續往下執行,當JavaScript引擎空閒時會執行回調函數任務。
2.delayTwentyMilliseconds().這個算是大規模的運算操作,執行時間是20s,[同步任務],會一直阻塞JavaScript線程,從而說明了爲什麼NodeJS不適合做大規模的運算操作

瀏覽器與NodeJS

NodeJs和瀏覽器是差不多的,Node.js也是單線程的Event Loop.只不過瀏覽器是瀏覽器內核來管理異步線程,NodeJs是libuv這模塊來管理異步線程,同樣的NodeJS也是利用V8-JavaScript引擎來進行執行任務隊列-Event Loop

這裏寫圖片描述

NodeJS能處理高併發連接並且達到比較良好的吞吐量的真正原因:
以下是年輕的時候的錯誤理解
實際上是NodeJS中的http模塊是異步的,NodeJS只負責接收海量的Http請求連接,而處理這些連接是由libuv來處理,只是把壓力轉給了libuv。
NodeJS對數據庫的操作也是同樣的道理。

正解:
其實看上面的論述,可以得知不管是NodeJS還是瀏覽器的JavaScript引擎都是單線程的,所以所有的請求都會排隊 然而,真正提高併發吞吐量的是I/O多路複用(解釋:I/O多路複用其意思就是所有的接受請求響應都是用同一個線程來處理),正因爲Node接受用戶請求時是用一個線程接收所有請求的(不需要開其他線程來處理請求),由於每個進程/線程需要佔用不少資源(典型的是內存,一個線程通常需要2M的棧空間),更重要的是,線程/進程創建,切換,銷燬時的開銷是非常大的。
然而Node的異步事件輪訓:
1.異步:
異步表現在於Node在處理比較耗時的I/O(比如請求第三方API,讀寫文件,讀寫數據)的時候,Node使用異步回調的形式來處理,這樣當遇見比較耗時的I/O時,Node不會等待,而是繼續接受其他用戶的請求,從而達到更高的併發吞吐量。
2.事件輪訓:
事件輪訓表現在於,libuv也維護了一個事件隊列,所有比較耗時的I/O操作都由libuv來處理,同時libuv一直輪訓事件隊列的事件是否完成(因爲所有事件都是異步的只能輪訓),然後以回調函數的方式及時響應給JavaScript解釋器

這裏寫圖片描述

啊!,目前還是不知道libuv的工作原理,覺得libuv好強大,有時間我一定要研究一下

window.onload

界面渲染線程負責渲染瀏覽器界面HTML元素,當界面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該線程就會執行.本文雖然重點解釋JavaScript定時機制,但這時有必要說說渲染線程,因爲該線程與JavaScript引擎線程是互斥的,這容易理解,因爲 JavaScript腳本是可操縱DOM元素,在修改這些元素屬性同時渲染界面,那麼渲染線程前後獲得的元素數據就可能不一致了.

流程:
渲染引擎解析Html和CSS同時,如果這個時候JavaScript引擎在操作Html元素,瀏覽器是相信渲染引擎的還是JavaScript引擎的?所以window.onload就解決了,當渲染引擎執行完頁面渲染,才執行JavaScript引擎來執行JavaScript腳本

回調函數

回調函數 :其實就是函數指針 無異步,無回調,解決了異步函數傳值的問題

  //假如A是異步方法。
    function A(callback){
        var a=0;
            //假設執行異步任務是a++,
            a++;
            //當異步任務執行完,調用callback這個函數(也就是B(biu)這個函數),所以只能通過回調函數的方法把a值傳遞給B,方便B來操作數據
            callback(a);
        }
        //function B(piu)是一個函數,不是函數指針,bb纔是函數指針
        var bb=function B(piu){
            piu++;
            alter(piu);
        }

        A(bb);
----------
A(bb){
var a=0;
//假設執行異步任務是a++,
a++;
B(a){
a++;
alter(a);
}
}
//方法B被方法A回調,方法B是方法A的回調函數
//流程:由於A是異步函數,由瀏覽器內核來執行這個函數,當瀏覽器內核執行完畢,會把回調函數放入任務隊列中,javaScript引擎在空閒的時候就會執行這個回調函數

        //假如A是異步方法。
    function A(callback){
            var a=0;
            //假設執行異步任務是a++,
            a++;
            //如果這樣 返回的a將等於0.
            retrun a;
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章