Javascript執行機制--單線程,同異步任務,事件循環

總所周知,javascript是一門依賴宿主環境的單線程的弱腳本語言,這意味着什麼?javascript

  • javascript的運行環境通常都由宿主環境(如瀏覽器、Node、Ringo等)和執行環境(Javascript引擎V8,JavaScript Core等)共同構成;
  • 弱類型定義語言:數據類型能夠被忽略的語言。例如計算時會在不一樣類型之間進行隱式轉換;
  • 在某一時刻內只能執行特定的一個任務,而且會阻塞其它任務執行;

本文主要講的就是第三點,從中引出下一個問題html

單線程的設計緣由?

Javascript當初誕生的目的其實就是由於當年網絡技術十分低效,如表單驗證等個幾十秒才能獲得反饋的用戶體驗十分糟糕,爲了給瀏覽器作些簡單處理之前由服務器端負責的一些表單驗證。被Netscape公司指派花了十天就負責設計出一門新語言的Javascript之父就是Brendan Eich。儘管他並不喜歡本身設計的這做品,就有了你們都聽過的一句話:前端

"與其說我愛Javascript,不如說我恨它。它是C語言和Self語言一晚上情的產物。十八世紀英國文學家約翰遜博士說得好:'它的優秀之處並不是原創,它的原創之處並不優秀。'(the part that is good is not original, and the part that is original is not good.)"

做爲瀏覽器腳本語言而誕生的JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只須要是單線程就足以解決目的,不然會帶來很複雜的同步問題。可是沒想到的是以後的網絡愈加的發達,這些年來的瀏覽器大戰爲了爭奪地盤,反而讓Javascript被賦予了更多的職責跟可能性,今時今日的Javascript必須千方百計把自身的潛力激發出來,而單線程的弱點就被無限放大了,由於在阻塞任務的過程當中不必定是由於CPU被佔用了,而多是由於I/O太慢(如AJAX請求,定時器任務,Dom事件交互等並不消耗CPU的等待形成資源時間浪費)。java

瀏覽器中Javascript執行線程

咱們一直都在說Javascript是單線程,但瀏覽器是多線程的,在內核控制下互相配合以保持同步,主要的常駐線程有:編程

  • GUI渲染線程:負責渲染界面,解析HTML,CSS,構建DOM和Render樹佈局繪製等。若是過程當中遇到JS引擎執行會被掛起線程,GUI更新保存在一個隊列中等待JS引擎空閒才執行;
  • JS引擎線程:負責解析運行Javascript;執行時間過程會致使頁面渲染加載阻塞;
  • 事件觸發線程,瀏覽器用以控制事件循環。當JS引擎執行過程當中觸發的事件(如點擊,請求等)會將對應任務添加到事件線程中,而當對應的事件符合觸發條件被觸發時會把對應任務添加處處理隊列的尾部等到JS引擎空閒時處理;
  • 定時器觸發線程:由於JS引擎是單線程容易阻塞,因此須要有單獨線程爲setTimeout和setInterval計時並觸發,一樣是符合觸發條件(記時完畢)被觸發時會把對應任務添加處處理隊列的尾部等到JS引擎空閒時處理;W3C標準規定時間間隔低於4ms被算爲4ms。
  • 異步http請求線程:XMLHttpRequest在鏈接後瀏覽器新開線程去請求,檢測到狀態變化若是有設置回調函數會產生狀態變動事件,而後把對應任務添加處處理隊列的尾部等到JS引擎空閒時處理;

好像鋪墊的有點多,往外偏了,接下來往回拉一點談談這些怎麼運行的。數組

什麼是堆(heap)和棧(stack)?

本身畫了一個醜醜的圖,你們將就看着吧。
圖片描述promise

function addOne(n) {
  var x = n + 1;
  return addTwo(x);
}

function addTwo(n) {
  return n + 2;
}

console.log(addOne(1)) //4;

以這個例子作說明。
當調用addOne時建立一個包含addOne入參和局部變量的幀並添加進去stack,當調用到addTwo時也一樣建立一個包含addTwo入參和局部變量的幀並添加進去在首部,執行完addTwo函數並返回時addTwo幀被移出stack,addOne執行完後addOne幀也被移除。
原理:當執行方法時都會創建本身的內存棧,在這個方法內定義的入參變量都會保存在棧內存裏,執行結束後該方法的內存棧也將天然銷燬了。瀏覽器

通常來講,程序會劃分有兩種分配內存的空間 -- 堆(heap)棧(stack)服務器

內存空間 分配方式 結構 大小 存取速度 釋放機制
stack 靜態分配 隨方法執行結束而銷燬
heap 動態分配 沒有 系統的垃圾回收機制銷燬

由於棧只能存放下肯定大小的簡單數據,因此像變量(其實也就是一個記錄了指向複雜結構數據的地址指向,因此變量也是保存在棧裏的)和基本類型Undefined、Null、Boolean、Number 和 String等是按值傳遞的都會保存在棧裏,隨着方法執行完畢而被銷燬。
堆負責存放複雜結構的對象,數組,函數等建立成本較高而且可重用數據,即便方法執行完也不會被銷燬,直到系統的垃圾回收機制覈實了沒有任何引用纔會回收。
其實這只是棧的含義之一,Stack的三種含義網絡

有時候咱們代碼有問題致使棧堆溢出緣由大概是這種狀況:

常見狀況 可能狀況
棧溢出 無限遞歸死循環,遞歸越深層分配內存越多直至超過限制
堆溢出 循環生成複雜結構數據

好了,如今再看回上圖,除了heap和stack以外還有一個。。。

什麼是Queue(任務隊列)?

Javascript裏分兩種隊列:

  • 宏任務隊列(macro tasks):事件循環中能夠有多個macro tasks,每次循環只會提取一個,包括script(全局任務), setTimeout, setInterval, setImmediate, I/O, UI rendering等.
  • 微任務隊列(micro tasks):事件循環中只有一個而且有優先級區別micro tasks,每次循環會提取屢次直至隊列清空,包括process.nextTick, Promise, Object.observer, MutationObserver等.
console.log('log start!');
setTimeout(function () {
  console.log('setTimeout300');
}, 300)

Promise.resolve().then(function () {
  console.log('promise resolve');
}).then(function () {
  console.log('promise resolve then');
})

new Promise(function (resolve, reject) {
  console.log('promise pending');
  resolve();
}).then(function () {
  console.log('promise pending then');
})

setTimeout(function () {
  console.log('setTimeout0');
  Promise.resolve().then(function () {
    console.log('promise3 in setTimeout');
  })
}, 0)
console.log('log end!');

// log start!
// promise pending
// log end!
// promise resolve
// promise pending then
// promise resolve then
// setTimeout0
// promise3 in setTimeout
// setTimeout300

例子過程,具體分析下面再說。
第一次執行事件打印:log start!, promise pending, log end!, promise resolve,promise pending then,promise resolve then;
第二次執行事件打印:setTimeout0,promise3 in setTimeout;
第三次執行事件打印:setTimeout300;

下面終於開始走到正題了

事件循環(Event Loop)!

我在上面鋪墊了這麼多東西,你們大概都能有個初步印象,而後所謂的Event Loop就是把這些東西串聯起來的一種機制吧,由於這東西各有理解,好比兩位前端大牛之間就有分歧。
阮一峯:JavaScript 運行機制詳解:再談Event Loop
樸靈:樸靈評註
我看過他們不少的博客和書籍,對我幫助都很大,我就用本身的見解講講我眼中的Event Loop。

1,全部的任務都被放主線程上運行造成一個 執行棧(execution context stack),其中的方法入參變量保存在棧內存中,複雜結構對象被保存在堆內存中;
2,同步任務直接執行並阻塞後續任務等待結束,其中遇到一些異步任務會新開線程去執行該任務(如上面提到的定時器觸發線程,異步http請求線程等)而後往下執行,異步任務執行完返回結果以後就把回調事件加入到 任務隊列(Queue)
3,當 執行棧(execution context stack)全部任務執行完以後,會到 任務隊列(Queue)裏提取全部的 微任務隊列(micro tasks)事件執行完;
4,一次循環結束,GUI渲染線程接管檢查,從新渲染界面;
5, 執行棧(execution context stack)宏任務隊列(macro tasks)提取一個事件到執行,接着主線程就一直重複第3步;

圖片描述
大概理解就這樣子,固然可能會有點誤差,歡迎指正!

特殊的定時器

我在上面線程說過

定時器觸發線程:由於JS引擎是單線程容易阻塞,因此須要有單獨線程爲 setTimeoutsetInterval計時並觸發,一樣是符合觸發條件(記時完畢)被觸發時會把對應任務添加處處理隊列的尾部等到JS引擎空閒時處理;W3C標準規定時間間隔低於4ms被算爲4ms。

裏面有一些須要特別注意的地方:
1,計時完畢只是把對應任務添加處處理隊列,依然要等執行棧空閒纔會去提取隊列執行,這個概念很重要,切記!即便設置0秒也不會立馬執行,由於W3C標準規定時間間隔低於4ms被算爲4ms,具體看瀏覽器,我我的認爲無論怎樣始終都會被放置處處理隊列等待處理;
2,setTimeout重複執行過程當中每次時間偏差會影響後續執行時間,而setInterval是每次精確時間執行,固然這是指他們把對應任務添加處處理隊列的精確性;

可是setInterval也有一些問題:

  • 累計效應,若是執行棧阻塞時間足夠長以致於隊列中已經存在多個setInterval的對應任務的狀況,執行時間會遠低於開發者指望的結果;
  • 部分瀏覽器(如Safari等)滾動過程不執行JS,容易形成卡頓和未知錯誤;
  • 瀏覽器最小化顯示時setInterval會繼續執行,可是對應任務會等到瀏覽器還原再一瞬間所有執行;

結語

坦白講,我本來時打算寫一篇關於異步編程的文章,而後在鋪墊前文的路上拉不回來了就變成了一篇梳理Javascript執行機制了,不過不要緊,理解這些也是很重要的

相關文章
相關標籤/搜索