JavaScript 逐點突破之單線程與異步,做爲前端必知必會

已知,JavaScript 是單線程的,天生異步,適合 IO 密集型,不適合 CPU 密集型,可是,爲何是異步的喃,異步由何而來的喃,咱們將在這裏逐漸討論實現。html

1、進程與線程

1. 瀏覽器是多進程的

它主要包括如下進程:前端

  • Browser 進程:瀏覽器的主進程,惟一,負責建立和銷燬其它進程、網絡資源的下載與管理、瀏覽器界面的展現、前進後退等。
  • GPU 進程:用於 3D 繪製等,最多一個。
  • 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才建立。
  • 瀏覽器渲染進程(瀏覽器內核):內部是多線程的,每打開一個新網頁就會建立一個進程,主要用於頁面渲染,腳本執行,事件處理等。

2. 渲染進程(瀏覽器內核)

瀏覽器的渲染進程是多線程的,頁面的渲染,JavaScript 的執行,事件的循環,都在這個進程內進行:web

  • GUI 渲染線程:負責渲染瀏覽器界面,當界面須要重繪(Repaint)或因爲某種操做引起迴流(Reflow)時,該線程就會執行。
  • JavaScript 引擎線程:也稱爲 JavaScript 內核,負責處理 Javascript 腳本程序、解析 Javascript 腳本、運行代碼等。(例如 V8 引擎)
  • 事件觸發線程:用來控制瀏覽器事件循環,注意這不歸 JavaScript 引擎線程管,當事件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待 JavaScript 引擎的處理。
  • 定時觸發器線程:傳說中的 setIntervalsetTimeout 所在線程,注意,W3C 在 HTML 標準中規定,規定要求 setTimeout 中低於 4ms 的時間間隔算爲 4ms 。
  • 異步 http 請求線程:在 XMLHttpRequest 鏈接後經過瀏覽器新開一個線程請求,將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中。再由 JavaScript 引擎執行。

注意,GUI 渲染線程與 JavaScript 引擎線程是互斥的,當 JavaScript 引擎執行時 GUI 線程會被掛起(至關於被凍結了),GUI 更新會被保存在一個隊列中等到 JavaScript 引擎空閒時當即被執行。因此若是 JavaScript 執行的時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞。面試

2、單線程的 JavaScript

所謂單線程,是指在 JavaScript 引擎中負責解釋和執行 JavaScript 代碼的線程惟一,同一時間上只能執行一件任務。ajax

問題:首先爲何要引入單線程喃?編程

咱們知道:json

  • 瀏覽器須要渲染 DOM
  • JavaScript 能夠修改 DOM 結構
  • JavaScript 執行時,瀏覽器 DOM 渲染中止

若是 JavaScript 引擎線程不是單線程的,那麼能夠同時執行多段 JavaScript,若是這多段 JavaScript 都修改 DOM,那麼就會出現 DOM 衝突。瀏覽器

你可能會說,web worker 就支持多線程,可是 web worker 不能訪問 window 對象,document 對象等。網絡

緣由:避免 DOM 渲染的衝突多線程

固然,咱們能夠爲瀏覽器引入 的機制來解決這些衝突,但其大大提升了複雜性,因此 JavaScript從誕生開始就選擇了單線程執行。

引入單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。這同時又致使了一個問題:若是前一個任務耗時很長,後一個任務就不得不一直等着。

// 實例1
let i, sum = 0
for(i = 0; i < 1000000000; i ++) {
    sum += i
}
console.log(sum)
複製代碼

在實例1中,sum 並不能馬上打印出來,必須在 for 循環執行完成以後才能執行 console.log(sum)

// 實例2
console.log(1)
alert('hello')
console.log(2)
複製代碼

在實例2中,瀏覽器先打印 1 ,而後彈出彈框,點擊肯定後才執行 console.log(2)

總結:

  • 優勢:實現比較簡單,執行環境相對單純
  • 缺點:只要有一個任務耗時很長,後面的任務都必須排隊等着,會拖延整個程序的執行。常見的瀏覽器無響應(假死),每每就是由於某一段 Javascript 代碼長時間運行(好比死循環),致使整個頁面卡在這個地方,其餘任務沒法執行。

爲了解決這個問題,JavaScript 語言將任務的執行模式分爲兩種:同步和異步

3、同步與異步

1. 同步

func(args...)
複製代碼

若是在函數 func 返回的時候,調用者就可以獲得預期結果(即拿到了預期的返回值或者看到了預期的效果),那麼這個函數就是同步的。

let a = 1
Math.floor(a)
console.log(a) // 1
複製代碼

2. 異步

若是在函數 func 返回的時候,調用者還不可以獲得預期結果,而是須要在未來經過必定的手段獲得,那麼這個函數就是異步的。

fs.readFile('foo.txt', 'utf8', function(err, data) {
    console.log(data);
});
複製代碼

總結:

JavaScript 採用異步編程緣由有兩點,

  • 一是 JavaScript 是單線程;
  • 二是爲了提升 CPU 的利用率。

4、異步過程

fs.readFile('data.json', 'utf8', function(err, data) {
    console.log(data)
})
複製代碼

在執行這段代碼時,fs.readFile 函數返回時,並不會馬上打印 data ,只有 data.json 讀取完成時纔打印。也就是異步函數 fs.readFile 執行很快,但後面還有工做線程執行異步任務、通知主線程、主線程回調等操做,這個過程就叫作異步過程。

主線程發起一個異步操做,相應的工做線程接受請求並告知主線程已收到(異步函數返回);主線程繼續執行後面的任務,同時工做線程執行異步任務;工做線程完成任務後,通知主線程;主線程收到通知後,執行必定的動做(調用回調函數)。

工做線程在異步操做完成後通知主線程,那麼這個通知機制又是如何顯現喃?答案就是就是消息隊列與事件循環。

5、消息隊列與事件循環

工做線程將消息放在消息隊列,主線程經過事件循環過程去取消息。
  • 消息隊列:消息隊列是一個先進先出的隊列,它裏面存放着各類消息。
  • 事件循環:事件循環是指主線程重複從消息隊列中取消息、執行的過程。

1. 事件循環(eventloop)

主線程不斷的從消息隊列中取消息,執行消息,這個過程稱爲事件循環,這種機制叫事件循環機制,取一次消息並執行的過程叫一次循環。

大體實現過程以下:

while(true) {
    var message = queue.get()
    execute(message)
}
複製代碼

例如:

$.ajax({
    url: 'xxxx',
    success: function(result) {
        console.log(1)
    }
})
setTimeout(function() {
    console.log(2)
}, 100)
setTimeout(function() {
    console.log(3)
})
console.log(4)
// output:4321 或 4312
複製代碼

其中,主線程:

// 主線程
console.log(4)
複製代碼

異步隊列:

// 異步隊列
function () {
    console.log(3)
}
function () { // 100ms後
    console.log(2)
}
function() { // ajax加載完成以後
    console.log(1)
}
複製代碼

事件循環是JavaScript實現異步的具體解決方案,其中同步代碼,直接執行;異步函數先放在異步隊列中,待同步函數執行完畢後,輪詢執行 異步隊列 的回調函數。

2. 消息隊列

其中,消息就是註冊異步任務時添加的回調函數。

$.ajax('XXX', function(res) {
    console.log(res)
})
...
複製代碼

主線程在發起 AJAX 請求後,會繼續執行其餘代碼,AJAX 線程負責請求 XXX,拿到請求後,會封裝成 JavaScript 對象,而後構造一條消息:

// 消息隊列裏的消息
var message = function () {
    callback(response)
}
複製代碼

其中 callback 是 AJAX 網絡請求成功響應時的回調函數。

主線程在執行完當前循環中的全部代碼後,就會到消息隊列取出這條消息(也就是 message 函數),並執行它。到此爲止,就完成了工做線程對主線程的 通知 ,回調函數也就獲得了執行。若是一開始主線程就沒有提供回調函數,AJAX 線程在收到 HTTP 響應後,也就不必通知主線程,從而也不必往消息隊列放消息。

異步過程當中的回調函數,必定不在當前這一輪事件循環中執行。

6、異步與事件

消息隊列中的每條消息實際上都對應着一個事件。

其中一個重要的異步過程就是: DOM事件

var button = document.getElementById('button')
button.addEventListener('click', function(e) {
    console.log('事件')
})
複製代碼

從異步的角度看,addEventListener 函數就是異步過程的發起函數,事件監聽器函數就是異步過程的回調函數。事件觸發時,表示異步任務完成,會將事件監聽器函數封裝成一條消息放在消息隊列中,等待主線程執行。

事件的概念實際上並非必須的,事件機制實際上就是異步過程的通知機制。

另外,全部的異步過程也均可以用事件來描述。例如:

setTimeout(func, 1000)
// 能夠當作:
timer.addEventListener('timeout', 1000, func)
複製代碼

7、生產者與消費者

生產者和消費者問題是線程模型中的經典問題:生產者和消費者在同一時間段內共用同一個存儲空間,生產者往存儲空間中添加數據,消費者從存儲空間中取走數據,當存儲空間爲空時,消費者阻塞,當存儲空間滿時,生產者阻塞。

從生產者與消費者的角度看,異步過程是這樣的:

工做線程是生產者,主線程是消費者(只有一個消費者)。工做線程執行異步任務,執行完成後把對應的回調函數封裝成一條消息放到消息隊列中;主線程不斷地從消息隊列中取消息並執行,當消息隊列空時主線程阻塞,直到消息隊列再次非空。

那麼異步的實現方式有哪些喃?

  • ES6以前:callback、eventloop、Promise
  • ES6:Generator
  • ES7:Async/Await

    8、前端JS面試資料


    小編整了些JS面試題資料,小編放個小尾巴給你們點擊領取哦:JS面試題資料

相關文章
相關標籤/搜索