JavaScript執行順序分析

前言

上星期面試被問到了事件執行順序的問題,想起來以前看《深刻淺出Node.js》時看到這一章就忽略了,此次來分析一下JavaScript的事件執行順序。廢話少說,正題開始。javascript

單線程JavaScript

首先咱們要知道JavaScript是一門單線程解釋型語言。這就意味着在同一個時間下,咱們只能執行一條命令。之因此它是一門單線程語言,和它的用途有關。
JavaScript設計出來的初衷是爲了加強瀏覽器與用戶的交互,尤爲是表單的交互,而以後的Ajax技術也是爲了使表單的交互更加人性化而發明出來的。由於JavaScript是一門解釋型的語言,而解釋器內嵌於瀏覽器,這個解釋器是單線程的。
之因此不設計成多線程是由於渲染網頁的時候多線程容易引發死鎖或者資源衝突等問題。可是瀏覽器自己是多線程的,好比解釋運行JavaScript的同時還在加載網絡資源。html

Why doesn't JavaScript support multithreading?前端

<!-- more -->html5

事件循環

單線程就意味着若是你要運行不少命令,那麼這些命令須要排序,通常狀況下,這些命令是從上到下排序執行(由於解釋器是從文件頂部開始)。好比如下代碼是按照順序執行的。java

console.log("1");
console.log("2");
console.log("3");
//1
//2
//3

可是咱們還有知道在JavaScript裏有異步編程的說法,好比Ajax,setTimeout,setInterval或者ES6中的Promise,async,await。面試

那麼什麼是同步和異步呢?

一條命令的執行在計算機裏的意思就是它此時在使用CPU等資源,那麼由於想要得到CPU資源的命令有不少,而CPU執行命令也須要時間去運算得到結果,因而就有了同步異步的概念。ajax

同步就是在發出一個CPU請求時,在沒有獲得結果以前,該CPU請求就不返回。可是一旦調用返回,就獲得返回值了。編程

異步表示CPU請求在發出以後,這個調用就直接返回了,因此沒有返回結果。在運行結束後,須要經過一系列手段來得到返回值promise


這時候就要引入進程和線程的概念。瀏覽器

進程與線程

進程

概念:進程是一個具備必定獨立功能的程序在一個數據集上的一次動態執行的過程,是操做系統進行資源分配和調度的一個獨立單位,是應用程序運行的載體。

線程

因爲進程對於CPU的使用是輪流的,那麼就存在進程的切換,可是因爲如今的程序都比較大,切換的開銷很大會浪費CPU的資源,因而就發明了線程,把一個大的進程分解成多個線程共同執行。

區別

  • 進程是操做系統分配資源的最小單位,線程是程序執行的最小單位。
  • 一個進程由一個或多個線程組成,線程是一個進程中代碼的不一樣執行路線;
  • 進程之間相互獨立,但同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)及一些進程級的資源(如打開文件和信號)。
  • 調度和切換:線程上下文切換比進程上下文切換要快得多。

舉個例子

假如我是鳴人,我想吃不少拉麪,若是我一我的吃10碗的話,那我就是一個進程一個線程完成吃拉麪這件事情。
可是若是我用9個分身和我一塊兒吃10碗拉麪,那我就是一個進程用9個線程去完成吃拉麪這件事情。
而多進程這表示名人在一樂拉麪裏面吃拉麪的同時,好色仙人在偷看妹子洗澡~ ~。好色仙人是單進程單線程去偷看的哦!

瀏覽器的線程

瀏覽器的內核是多線程的,在內核控制下各線程相互配合以保持同步,一個瀏覽器一般由一下線程組成:

  • GUI 渲染線程
  • JavaScript引擎線程
  • 事件觸發線程
  • 異步http請求線程
  • EventLoop輪詢的處理線程

這些線程的做用:

  • UI線程用於渲染頁面
  • js線程用於執行js任務
  • 瀏覽器事件觸發線程用於控制交互,響應用戶
  • http線程用於處理請求,ajax是委託給瀏覽器新開一個http線程
  • EventLoop處理線程用於輪詢消息隊列

圖片

JavaScript事件循環和消息隊列(瀏覽器環境)

由於JavaScript是單線程的,而瀏覽器是多線程的,因此爲了執行不一樣的同步異步的代碼,JavaScript運行的環境採用裏事件循環和消息隊列來達到目的。
每一個線程的任務執行順序都是FIFO(先進先出)
在JavaScript運行的環境中,有一個負責程序自己的運行,做爲主線程;另外一個負責主線程與其餘線程的通訊,被稱爲Event Loop 線程
每當主線程遇到異步的任務,把他們移入到Event Loop 線程,而後主線程繼續運行,等到主線程徹底運行完以後,再去Event Loop 線程拿結果。
而每一個異步任務都包含着與它相關聯的信息,好比運行狀態,回調函數等。
事件循環
由此咱們能夠知道,同步任務和異步任務會被分發到不一樣的線程去執行。
如今咱們就能夠分析一下一下代碼的運行結果了。

setTimeout(()=>{console.log("我纔是第一");},0);
console.log("我是第一");
  1. 由於setTimeout是異步的事件,因此主線程把它調入Event Loop線程進行註冊。
  2. 主線程繼續執行console.log("我是第一");
  3. 主線程執行完畢,從Event Loop 線程讀取回調函數。再執行console.log("我纔是第一");;

setTimeout 和 setInterval

setTimeout

這裏值得一提的是,setTimeout(callback,0)指的是主線程中的同步任務運行完了以後馬上由Event Loop 線程調入主線程。
而計時是在調入Event Loop線程註冊時開始的,此時setTimeout的回調函數執行時間與主線程運行結束的時間相關。
關於setTimeout要補充的是,即使主線程爲空,0毫秒實際上也是達不到的。根據HTML的標準,最低是4毫秒。

setInterval

須要注意的是,此函數是每隔一段時間將回調函數放入Event Loop線程。
一旦setInterval的回調函數fn執行時間超過了延遲時間ms,那麼就徹底看不出來有時間間隔了

micro-task(微任務)macro-task(宏任務)

Event Loop線程中包含任務隊列(用來對不一樣優先級的異步事件進行排序),而任務隊列又分爲macro-task(宏任務)micro-task(微任務),在最新標準中,它們被分別稱爲taskjobs

  • macro-task大概包括:script(總體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
  • setTimeout/Promise等咱們稱之爲任務源。而進入任務隊列的是他們指定的具體執行任務(回調函數)。

來自不一樣的任務源的任務會進入到不一樣的任務隊列中,而不一樣的任務隊列執行過程以下:
執行過程以下:
JavaScript引擎首先從macro-task中取出第一個任務,
執行完畢後,將micro-task中的全部任務取出,按順序所有執行;
而後再從macro-task中取下一個,
執行完畢後,再次將micro-task中的所有取出;
循環往復,直到兩個隊列中的任務都取完。

舉個大例子

console.log("start");
var promise = new Promise((resolve) => {
    console.log("promise start..");
    resolve("promise");
}); //3
promise.then((val) => console.log(val));
setTimeout(()=>{console.log("setTime1")},0);
console.log("test end...")

這裏咱們按順序來分析。

第一輪
  1. 總體script代碼做爲一個宏任務進入主線程,運行console.log("start");
  2. 而後遇到Promises直接運行console.log("promise start..")
  3. 而後遇到promise.then,存入到micro-task隊列中。
  4. 而後遇到setTimeout,存入到macro-task隊列中。
  5. 於而後運行console.log("test end...");
  6. 在這一輪中,宏任務運行結束,運行micro-task隊列中的 promise.then,輸出promise
第二輪
  1. 取出macro-task隊列中的setTimeout,運行console.log("setTime1");
結果

輸出的順序就是

// start
// promise start
// test end...
// promise
//setTime1

留一個案例大家去分析

async function testSometing() {
    console.log("執行testSometing");
    return "testSometing";
}

async function testAsync() {
    console.log("執行testAsync");
    return Promise.resolve("hello async");
}

async function test() {
    console.log("test start...");
    const v1 = await testSometing();
    console.log(v1);
    const v2 = await testAsync();
    console.log(v2);
    console.log(v1, v2);
}

test();

var promise = new Promise((resolve) => {
    console.log("promise start..");
    resolve("promise");
}); //3
promise.then((val) => console.log(val));
setTimeout(()=>{console.log("setTime1")},3000);
console.log("test end...")

感謝如下文章

相關文章
相關標籤/搜索