博客 / 詳情

返回

看完還不懂JavaScript執行機制(EventLoop),你來捶我

上一篇文章介紹了進程與線程,知道渲染進程都有一個主線程,並且主線程工作很多,要處理DOM、計算樣式、佈局、還有鼠標、鍵盤等各種JS任務

我們都知道JS是單線程,任務只能一件一件地執行,那麼瀏覽器是怎麼讓這麼多類型的任務在主線程上有條紊地執行的呢?

這就需要任務隊列事件循環

任務隊列(消息隊列)

什麼是任務隊列呢?

它是一種數據結構,存放要執行的任務。然後事件循環系統再以先進先出原則按順序執行隊列中的任務。產生新任務時IO線程就將任務添加在隊列尾部,要執行任務渲染主線程就會循環地從隊列頭部取出執行,如圖

未標題-12.jpg

如果其他進程也有任務想讓主線程執行的話,也是一樣的通過 IO 線程接收並將任務添加到任務隊列就可以了

可任務隊列裏的任務類型太多了,而且是多個線程操作同一個任務隊列,比如鼠標滾動、點擊、移動、輸入、計時器、WebSocket、文件讀寫、解析DOM、計算樣式、計算佈局、JS執行.....

這些任務都在主線程中執行,而JS是單線程的,一個任務執行需要等前面的任務都執行完,所以就需要解決單個任務佔用主線程過久的問題

比如如果一個動畫任務前面有一個JS任務執行時間很長,那我們看到的就是卡卡的感覺,用户體驗就很不好

如果是DOM頻繁發生變化的JS任務,每次變化都需要調用相應的JavaScript接口,無疑會導致任務時間拉長,如果把DOM變化做成異步任務,那可能添加到任務隊列過程中,前面又有很多任務在排隊了

所以為了處理高優先級的任務和解決單任務執行過長的問題,所以需要將任務劃分,所以微任務和宏任務它來了

在説微任務之前,要知道一個概念就是同步異步

同步和異步

我們知道了瀏覽器頁面是由任務隊列和事件循環系統來驅動的,但是隊列要一個一個執行,如果某個任務(http請求)是個耗時任務,那瀏覽器總不能一直卡着,所以為了防止主線程阻塞,JavaScript 又分為同步任務和異步任務

同步任務:就是任務一個一個執行,如果某個任務執行時間過長,後面的就只能一直等下去

異步任務:就是進程在執行某個任務時,該任務需要等一段時間才能返回,這時候就把這個任務放到專門處理異步任務的模塊,然後繼續往下執行,不會因為這個任務而阻塞

也就是説,除了任務隊列,還有一個專門處理需要延遲執行的模塊(延遲哈希表)

常見的異步任務:定時器、ajax、事件綁定、回調函數、async await、promise

好了,我們再來説微任務吧

微任務和宏任務

JS執行時,V8會創建一個全局執行上下文,在創建上下文的同時,V8也會在內部創建一個微任務隊列

有微任務隊列,自然就有宏任務隊列,任務隊列中的每一個任務則都稱為宏任務,在當前宏任務執行過程中,如果有新的微任務產生,就添加到微任務隊列中

  • 微任務包括: promise回調、proxy、MutationObserver(監聽DOM)、node 中的 process.nextTick等
  • 宏任務包括: 渲染事件、請求、script、setTimeout、setInterval、Node中的setImmediate、I/O 等

來看栗子搞懂她

你和一個大爺在銀行辦業務,大爺排在你前面,大爺是要存錢,存完錢之後,工作人員問大爺還要不要辦理其他業務,大爺説那我再改個密碼吧,這時候總不能讓大爺到隊伍最後去排隊再來改密碼吧

這裏面大爺要辦業務就是一個宏任務,而在錢存完了又想改密碼,這就產生了一個微任務,大爺還想辦其他業務就又產生新微任務,直到所有微任務執行完,隊伍的下一個人再來

這個隊伍就是任務隊列,工作人員就是單線程的JS引擎,排隊的人只能一個一個來讓他給你辦事

也就是説當前宏任務裏的微任務全部執行完,才會執行下一個宏任務

用代碼來舉例

<script> // 宏任務
    console.log(1)
    setTimeout(()=>{ // 宏任務
        console.log(2)
    },0)
    console.log(3)
</script>

輸出結果就是 1 3 2 ,因為setTimeout是宏任務,哪怕它的時間為0,當前宏任務裏的任務沒執行完,她插隊也沒用。然後就算計時時間為0,它也是一個延遲任務,所以放到異步處理模塊去先

注意:異步處理模塊(延遲哈希表)是一個和任務隊列同等級的數據結構。每個宏任務結束後,主線程就會檢查延遲哈希表,將裏面到期的任務拿出來依次執行,比如回調/計時器達到觸發條件等。不明白的話下面有圖,看着就很清晰了

再來

<script> // 宏任務
    console.log(1)
    new Promise( resolve => {
        resolve(2) // 回調 是微任務
        console.log(3)
    }).then( num => {
        console.log(num)
    })
    console.log(4)
</script>

輸出結果就是 1 3 4 2 ,遇到微任務(這裏的回調)就放到微任務隊列裏,等着執行棧中的任務執行完,再拿出來執行

看圖,必須要搞懂她

367e4062e66b2c2512768749e533393.jpg

沒理解的話可以多看一會兒這個圖,這一塊兒也是面試很愛問的

如圖可以看出來執行過程形成了一個循環,這就是事件循環( EventLoop )

事件循環( EventLoop )

事件循環:一句話概括就是入棧到出棧的循環

即:一個宏任務,所有微任務,渲染,一個宏任務,所有微任務,渲染.....

循環過程

  1. 所有同步任務都在主線程上依次執行,形成一個執行棧(調用棧),異步任務處理完後則放入一個任務隊列
  2. 當執行棧中任務執行完,再去檢查微任務隊列裏的微任務是否為空,有就執行,如果執行微任務過程中又遇到微任務,就添加到微任務隊列末尾繼續執行,把微任務全部執行完
  3. 微任務執行完後,再到任務隊列檢查宏任務是否為空,有就取出最先進入隊列的宏任務壓入執行棧中執行其同步代碼
  4. 然後回到第2步執行該宏任務中的微任務,如此反覆,直到宏任務也執行完,如此循環

練習一下,徹底搞懂她

<script>
    setTimeout(function () {
        console.log('setTimeout')
    }, 0)
    new Promise(function (resolve) {
        console.log('promise1')
        for( let i = 0; i < 1000; i++ ) {
            i === 999 && resolve()
        }
        console.log('promise2')
    }).then(function ()  {
        console.log('promise3')
    })
    console.log('script')
</script>

輸出結果:promise1 -> promise2 -> script -> promise3 -> setTimeout

想一下為什麼?

  • script 是宏任務,先執行它裏面的微任務
  • 遇到宏任務setTimeout放到異步處理模塊(延遲哈希表)
  • 繼續執行promise,打印promise1
  • 遇到循環,執行,遇到回調 resolve(),上面説了回調屬於微任務,放到微任務隊列
  • 繼續執行,打印 promise2
  • 繼續執行,打印 script
  • 執行棧的任務執行完了,去微任務列隊裏拿
  • 有一個 then 回調,執行,打印 promise3
  • 微任務都執行完了,去任務隊列拿下一個宏任務
  • 執行 setTimeout,打印 setTimeout

沒有理解的話,再想想

當遇到 async / await 呢?

async/await 是 ES7 引入的重大改進的地方,可以在不阻塞主線程的情況下,使用同步代碼實現異步訪問資源的能力,讓我們的代碼邏輯更清晰

説白了

async:就是異步執行和隱式返回Promise
await:返回的就是一個Promise對象

看題

async function fun() {
    console.log(1)
    let a = await 2
    console.log(a)
    console.log(3)
}
console.log(4)
fun()
console.log(5)

輸出結果:4 1 5 2 3

結合 async / await 的特點,我們來把這個題用 ES6 翻譯一下

function fun(){
    return new Promise(() => {
        console.log(1)
        Promise.resolve(2).then( a => {
            console.log(a)
            console.log(3)
        })
    })
}
console.log(4)
fun()
console.log(5)

上面説了,回調是微任務,所以直接扔到微任務隊列等着,這題裏自然就是最後執行,是不是好理解一點了

再來

先別看下面答案,想一下這題,和上面有一點點區別

function bar () {
    console.log(2)
}
async function fun() {
    console.log(1)
    await bar()
    console.log(3)
}
console.log(4)
fun()
console.log(5)

輸出結果:4 1 2 5 3

為啥?上面例子中 2 都沒打印出來,為啥這個就出來了

因為await的意思就是等,等await後面的執行完。所以"await bar()",是從右向左執行,執行完bar(),然後遇到await,返回一個微任務(哪怕這任務裏沒東西),放到微任務隊列讓出主線程。

上面説了 async/await 就是把異步以同步的形式實現,同步就是一步一步一行一行來嘛,await在微任務隊列裏都沒回來,那在await下面的自然不能執行,導致 3 最後打印

下面還有一題,把上面幾題結合了,我就不寫答案了

你來搞定她

async function async1 ()  {
    console.log('async1 start');
    await  async2();
    console.log('async1 end')
}

async function  async2 ()  {
    console.log('async2')
}

console.log('script start');

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

async1();

new Promise(function (resolve)  {
    console.log('promise1');
    resolve()
}).then(function ()  {
    console.log('promise2')
});

console.log('script end')

結語

點贊支持、手留餘香、與有榮焉

user avatar laughingzhu 頭像 xiangjiaochihuanggua 頭像 esunr 頭像 chongdianqishi 頭像 _raymond 頭像 buxia97 頭像 weirdo_5f6c401c6cc86 頭像 frontoldman 頭像 musicfe 頭像 light_5cfbb652e97ce 頭像 fehaha 頭像 dashnowords 頭像
32 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.