事件循環理解

瀏覽器進程

在理解事件循環以前咱們先了解一下瀏覽器進程和線程二三事javascript

進程和線程

進程:cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)
線程:進程中執行的每個任務指的就是線程,系統不會爲其分配內存資源,各個線程共享進程擁有的內存資源。cpu調度的最小單位(線程是創建在進程的基礎上的一次程序運行單位)java

二者的關係:

一、一個程序至少有一個進程,一個進程至少有一個線程
二、線程不能脫離進程而獨立運行
三、線程沒有地址空間,線程包含在進程的地址空間中。線程上下文只包含一個堆棧、一個寄存器、一個優先權
四、進程內的任何線程都被看作是同位體,且處於相同的級別
五、進程中任何線程均可以經過銷燬主線程來銷燬進程,銷燬主線程將致使該進程的銷燬,對主線程的修改可能影響全部的線程。編程

瀏覽器的進程

瀏覽器是多線程的,通常來講每打開一個標籤頁就算是建立了一個獨立的進程
除了標籤進程完,瀏覽器進程還有:數組

一、Browser進程:瀏覽器主進程,負責協調、主控

負責瀏覽器界面顯示,與用戶交互,如前進、後退等
負責各個頁面的管理,建立和銷燬其餘進程
將Renderer進程獲得的內存中的Bitmap,繪製到用戶界面上
網絡資源的管理,下載等promise

二、CPU進程

用於硬件加速圖形繪製瀏覽器

三、渲染進程(也稱瀏覽器內核或Renderer進程)多線程

用於解析頁面,渲染頁面,執行腳本,處理事件等等網絡

四、第三方插件進程

每種類型的插件對應一個進程,僅當使用該插件時才建立數據結構

渲染進程

裏面有多個線程,靠着這些現成共同完成渲染任務多線程

一、圖形用戶界面GUI渲染線程

負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等
當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時異步

二、JS引擎線程

負責處理Javascript腳本程序。(例如V8引擎)
瀏覽器不管何時都只有一個JS引擎在運行JS程序

注意:GUI渲染線程與JS引擎線程是互斥的

三、事件觸發線程

歸屬於渲染(瀏覽器內核)進程,不受JS引擎線程控制
用於控制事件(例如鼠標,鍵盤等事件),當該事件被觸發時候,事件觸發線程就會把該事件的處理函數添加進任務隊列中,等待JS引擎線程空閒後執行

四、定時觸發器線程

setInterval與setTimeout所在線程
瀏覽器定時計數器並非由JavaScript引擎計數的,(由於JavaScript引擎是單線程的, 若是處於阻塞線程狀態就會影響記計時的準確)
所以經過單獨線程來計時並觸發定時(計時完畢後,添加到事件隊列中,等待JS引擎空閒後執行)
注意:W3C的HTML標準中規定,setTimeout中低與4ms的時間間隔算爲4ms

五、異步HTTP請求線程

在XMLHttpRequest在鏈接後是經過瀏覽器新開一個線程請求
將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中。再由JavaScript引擎執行。

總結:須要知道的是瀏覽器中js線程只有一個,若是發起一個異步IO請求,在等待響應的這段時間後面的代碼會被阻塞。因爲js主線程和GUI渲染線程是相互阻塞的,因此就形成了瀏覽器假死。

什麼是事件循環

事件循環本質是用來作調度的,決定什麼時候將資源分配給誰
V8只是負責Js代碼的解析和執行,而事件循環決定了V8何時執行什麼代碼。

基本概念

堆:用以表示一個大部分非結構化的內存區域

對象、數組被存放在堆中

棧:js中的調用棧(代碼執行的地方),是一種後進先出的數組結構

JavaScript 是一門單線程的語言,這意味着它只有一個調用棧,所以,它同一時間只能作一件事。
每調用一個函數,解釋器就會把該函數添加進調用棧並開始執行
正在調用棧中執行的函數還調用了其它函數,那麼新函數也將會被添加進調用棧,一旦這個函數被調用,便會當即執行。
當前函數執行完畢後,解釋器將其從棧頂移出調用棧,繼續執行當前執行環境下的剩餘的代碼
當分配的調用棧空間被佔滿時,會引起「堆棧溢出」。

注:這裏的堆棧,是數據結構的堆棧,不是內存中的堆棧(內存中的堆棧,堆存放引用類型的數據,棧存放基本類型的數據

任務隊列:是一種先進先出的一種數據結構

javascript是單線程。單線程就意味着,全部任務須要排隊
全部的任務能夠分爲同步任務和異步任務

同步任務:調用當即獲得結果的任務

同步任務通常會直接進入到主線程中執行

異步任務:調用沒法當即獲得結果,須要額外的操做才能預期結果的任務

異步任務不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。
異步任務運行機制:
一、全部同步任務都在主線程上執行,造成一個執行棧
二、主線程以外,還存在一個"任務隊列"。只要異步操做執行完成,就到任務隊列中排隊
三、一旦執行棧中的全部同步任務執行完畢,系統就會按次序讀取任務隊列中的異步任務,因而被讀取的異步任務結束等待狀態,進入執行棧,開始執行
四、主線程不斷重複上面的第三步。
主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)

task(任務)

在事件循環中,每進行一次循環操做稱爲tick
分爲宏任務(macrotask )和爲任務(microtask )
而且每一個宏任務結束後, 都要清空全部的微任務,這裏的 Macro Task也是咱們常說的 task ,

宏任務

包括:script( 總體代碼)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 環境)

微任務(特殊的任務)

包括:Promise、MutaionObserver、process.nextTick(Node.js 環境)
執行順序:
JS 引擎會將全部任務按照類別分到這兩個隊列中
一、在 宏任務 的隊列中取出第一個任務(通常最初始,宏任務隊列中,只有一個 scrip t(總體代碼)任務)
二、執行完畢後取出 微任務 隊列中的全部任務順序執行
三、再取 宏任務,周而復始,直至兩個隊列的任務都取完
tip: 因爲microtask 優先於 task 執行,因此若是有須要優先執行的邏輯,放入microtask 隊列會比 task 更早的被執行

異步編程

回調函數

優勢是簡單、容易理解和部署
缺點是容易產生回調地獄

事件監聽

採用事件驅動模式
任務的執行不取決於代碼的順序,而取決於某個事件是否發生。

function f1(){
    setTimeout(function () {
      // f1的任務代碼
        f1.trigger('done');  // 執行完成後,當即觸發done事件,從而開始執行f2
    }, 1000);
}
f1.on('done', f2);    // 當f1發生done事件,就執行f2

優勢:能夠綁定多個事件,每一個事件能夠指定多個回調函數,並且能夠"去耦合"(Decoupling),有利於實現模塊化
缺點: 整個程序都要變成事件驅動型,運行流程會變得很不清晰。

發佈/訂閱(觀察者模式)

jQuery.subscribe("done", f2);

function f1(){
  setTimeout(function () {
    // f1的任務代碼
    jQuery.publish("done") ;   // f1執行完成後,向"信號中心"jQuery發佈"done"信號,從而引起f2的執行。
  }, 1000);
}

jQuery.unsubscribe("done", f2);  // f2完成執行後,也能夠取消訂閱(unsubscribe)

Promise

對象的狀態不受外界影響
一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果
Promise對象的狀態改變,只有兩種可能:從Pending變爲Resolved和從Pending變爲Rejected。
優勢:將異步操做以同步操做的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操做更加容易
缺點:
一、沒法取消Promise,一旦新建它就會當即執行,沒法中途取消;
二、若是不設置回調函數,Promise內部拋出的錯誤,不會反應到外部
三、當處於Pending狀態時,沒法得知目前進展到哪個階段(剛剛開始仍是即將完成)

注:在使用new建立Promise(...)時,裏面的代碼是自動執行的,Promise參數中的匿名函數與主線程同步執行,只有then裏面的東西纔是異步執行的

Generator函數

Generator函數返回的遍歷器對象,yield語句暫停,調用next方法恢復執行,若是沒遇到新的yeild,一直運行到return語句爲止,return 後面表達式的值做爲返回對象的value值,若是沒有return語句,一直運行到結束,返回對象的value爲undefined。

async/await

async函數就是Generator函數的語法糖。
async函數返回一個 Promise對象,可使用then方法添加回調函數
關鍵字await使async函數一直等待(執行棧固然不可能停下來等待的,await將其後面的內容包裝成promise交給Web APIs後,執行棧會跳出async函數繼續執行),直到promise執行完並返回結果。
await只在async函數函數裏面奏效。
await關鍵字後面的函數裏面的同步代碼和主線程同步執行
await語句後面的語句就至關因而await XXX promise resolved後then裏面的東西

Promise、async/await和setTimeout的執行順序

async function async1() {
     console.log('async1 start');
     await async2();
     console.log('asnyc1 end');
    }
    async function async2() {
     console.log('async2');
    }
    console.log('script start');
    setTimeout(() => {
     console.log('setTimeOut');
    }, 0);
    async1();
    new Promise(function (reslove) {
     console.log('promise1');
     reslove();
    }).then(function () {
     console.log('promise2');
    })
    console.log('script end');

執行結果:

script start
async1 start
async2
promise1
script end
asnyc1 end
promise2
setTimeOut

理解:
一、new Promise是同步的任務,會被放到主進程中去當即執行。而.then()函數是異步任務會放到異步隊列中去,那何時放到異步隊列中去呢?當你的promise狀態結束的時候,就會當即放進異步隊列中去了
二、帶async關鍵字的函數會返回一個promise對象,若是裏面沒有await,執行起來等同於普通函數,和主線程同步執行
三、若是帶await,此時的await會讓出線程,阻塞async內後續的代碼,先去執行async外的代碼。等外面的同步代碼執行完畢,纔會執行裏面的後續代碼。就算await的不是promise對象,是一個同步函數,也會等這樣操做
注:setTimeOut並非直接的把你的回掉函數放進上述的異步隊列中去,而是在定時器的時間到了以後,把回掉函數放到執行異步隊列中去。若是此時這個隊列已經有不少任務了,那就排在他們的後面。這也就解釋了爲何setTimeOut爲何不能精準的執行的問題了。

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    //async2作出以下更改:
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');

輸出結果:

script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
相關文章
相關標籤/搜索