總所周知,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是單線程,但瀏覽器是多線程的,在內核控制下互相配合以保持同步,主要的常駐線程有:編程
好像鋪墊的有點多,往外偏了,接下來往回拉一點談談這些怎麼運行的。數組
本身畫了一個醜醜的圖,你們將就看着吧。
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以外還有一個。。。
Javascript裏分兩種隊列:
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就是把這些東西串聯起來的一種機制吧,由於這東西各有理解,好比兩位前端大牛之間就有分歧。
阮一峯: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引擎是單線程容易阻塞,因此須要有單獨線程爲setTimeout
和setInterval
計時並觸發,一樣是符合觸發條件(記時完畢)被觸發時會把對應任務添加處處理隊列的尾部等到JS引擎空閒時處理;W3C標準規定時間間隔低於4ms被算爲4ms。
裏面有一些須要特別注意的地方:
1,計時完畢只是把對應任務添加處處理隊列,依然要等執行棧空閒纔會去提取隊列執行,這個概念很重要,切記!即便設置0秒也不會立馬執行,由於W3C標準規定時間間隔低於4ms被算爲4ms,具體看瀏覽器,我我的認爲無論怎樣始終都會被放置處處理隊列等待處理;
2,setTimeout重複執行過程當中每次時間偏差會影響後續執行時間,而setInterval是每次精確時間執行,固然這是指他們把對應任務添加處處理隊列的精確性;
可是setInterval也有一些問題:
坦白講,我本來時打算寫一篇關於異步編程的文章,而後在鋪墊前文的路上拉不回來了就變成了一篇梳理Javascript執行機制了,不過不要緊,理解這些也是很重要的