Javascript事件循環機制以及渲染引擎什麼時候渲染UI

JavaScript的一大特色就是單線程,而這個線程中擁有惟一的一個事件循環。

事件循環基本概念

  • JavaScript代碼的執行過程當中,除了依靠函數調用棧來搞定函數的執行順序外,還依靠任務隊列(task queue)來搞定另一些代碼的執行。
  • 一個線程中,事件循環是惟一的,可是任務隊列能夠擁有多個。
  • 任務隊列又分爲macro-task(宏任務)與micro-task(微任務),在最新標準中,它們被分別稱爲task與jobs。
  • macro-task大概包括:script(總體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
  • setTimeout/Promise等咱們稱之爲任務源。而進入任務隊列的是他們指定的具體執行任務。
// setTimeout中的回調函數纔是進入任務隊列的任務
setTimeout(function() {
    console.log('xxxx');
})
// 很是多的同窗對於setTimeout的理解存在誤差。因此大概說一下誤解:
// setTimeout做爲一個任務分發器,這個函數會當即執行,而它所要分發的任務,也就是它的第一個參數,纔是延遲執行
  • 來自不一樣任務源的任務會進入到不一樣的任務隊列。其中setTimeout與setInterval是同源的。
  • 其中每個任務的執行,不管是macro-task仍是micro-task,都是藉助函數調用棧來完成。

事件循環執行循序

事件循環的順序,決定了JavaScript代碼的執行順序。它從script(總體代碼)開始第一次循環。以後全局上下文進入函數調用棧。直到調用棧清空(只剩全局),而後執行全部的micro-task。當全部可執行的micro-task執行完畢以後,本輪循環結束。下一輪循環再次從macro-task開始,找到其中一個任務隊列執行完畢,而後再執行全部的micro-task,這樣一直循環下去。html

當咱們在執行setTimeout任務中遇到setTimeout時,它仍然會將對應的任務分發到setTimeout隊列中去,可是該任務就得等到下一輪事件循環執行。html5

那麼整個事件循環中什麼時候進行ui render呢?

<div id="div">
    begin
</div>
setTimeout(function() {
    // 應該是這裏執行前開始渲染ui,試試用alert阻塞下。
    alert(' ui 已經渲染完畢了嗎? ');
    console.log('timeout1');
})

new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
}).then(function() {
    console.log('then1');
    alert(' ui 開始渲染 ');
})

console.log('global1');

div.innerHTML = 'end';

上述代碼中修改了div的內容,那麼在執行那句js代碼以後渲染引擎開始修改div的內容呢?chrome

根據HTML Standard,一輪事件循環執行結束以後,下輪事件循環執行以前開始進行UI render。即:macro-task任務執行完畢,接着執行完全部的micro-task任務後,此時本輪循環結束,開始執行UI render。UI render完畢以後接着下一輪循環。segmentfault

在chrome瀏覽器中執行以上代碼,控制檯先輸出promise1,promise2,global1,then1(micro-task任務輸出),彈出'ui 開始渲染'警告框,點擊肯定以後,頁面中的'begin'變爲'end',再彈出警告框'ui 已經渲染完畢了嗎?' ,點擊確認以後再輸入timeout1.promise

再來一個稍微複雜一點的例子
<div class="outer" style="width:200px;height:200px;background-color: #ccc">
    1
    <div class="inner" style="width:100px;height:100px;background-color: #ddd">begin</div>
</div>
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

var i = 0;

// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
    console.log('mutate');
}).observe(outer, {
    attributes: true
});

// Here's a click listener…
function onClick() {
    i++;

    if(i === 1) {
        inner.innerHTML = 'end';
    }

    console.log('click');

    setTimeout(function() {
        alert('錨點');
        console.log('timeout');
    }, 0);

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


    outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

當咱們點擊 inner div 時程序依次的執行順序是:瀏覽器

  1. onclick 入 JS stack
  2. 打印出 click
  3. 將 timeout 壓入到 macrotask
  4. 將 promise 壓入到 microtask
  5. 修改 outer 屬性 data-random
  6. 將 mutate 壓入到 microtask,
  7. onclick 出 JS stack

此時,因爲用戶點擊事件onclick產生的macrotask執行完畢,JS stack 清空,開始執行microtask.dom

  1. promise 入 JS stack
  2. 打印出 promise
  3. promise 出 JS stack
  4. mutate 入 JS stack
  5. 打印出 mutate
  6. mutate 出 JS stack

此時,microtask 執行完畢,JS stack 清空,可是因爲事件冒泡,接着執行outer上的onclick事件.函數

  1. onclick 入 JS stack
  2. 打印出 click
  3. 將 timeout 壓入到 macrotask
  4. 將 promise 壓入到 microtask
  5. 修改 outer 屬性 data-random
  6. 將 mutate 壓入到 microtask,
  7. onclick 出 JS stack

此時,因爲outer上的onclick事件產生的macrotask執行完畢,JS stack 清空,開始執行microtask.ui

  1. promise 入 JS stack
  2. 打印出 promise
  3. promise 出 JS stack
  4. mutate 入 JS stack
  5. 打印出 mutate
  6. mutate 出 JS stack

此時,本輪事件循環結束,UI 開始 render.線程

  1. 頁面中inner的innerHTML變爲end

此時,UI render 完畢,開始下一輪事件循環.

  1. timeout 入 JS stack
  2. 彈出警告 錨點.
  3. 打印出 timeout
  4. timeout 出 JS stack
  5. timeout 入 JS stack
  6. 彈出警告 錨點.
  7. 打印出 timeout
  8. timeout 出 JS stack

到此爲止,整個事件執行完畢,咱們能夠看到在彈出警告框以前inner的內容已經改變

那若是不是用戶點擊事件觸發onclick,而是js觸發呢?
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
inner.click();

此時的執行順序是:

  1. 首先是script(總體代碼)入 JS stack
  2. onclick 入 JS stack
  3. 打印出 click
  4. 將 timeout 壓入到 macrotask
  5. 將 promise 壓入到 microtask
  6. 修改 outer 屬性 data-random
  7. 將 mutate 壓入到 microtask,
  8. onclick 出 JS stack

此時,inner 的 onclick 已經出 JS stack,可是script(總體代碼)尚未出 JS stack,還不能執行microtask,因爲冒泡,接着執行 outer 的 onclick.

  1. onclick 入 JS stack
  2. 打印出 click
  3. 將 timeout 壓入到 macrotask
  4. 將 promise 壓入到 microtask
  5. 修改 outer 屬性 data-random

接着執行的outer.setAttribute('data-random', Math.random());,可是因爲上一個mutation microtask還處於等待狀態,不能再添加mutation microtask,因此這裏不會將 mutate 壓入到 microtask。接着執行:

  1. onclick 出 JS stack
  2. script(總體代碼)出 JS stack

此時,inner.click()執行完畢,script(總體代碼)已出 JS stack,JS stack 清空,開始執行mircotask.

  1. promise 入 JS stack
  2. 打印出 promise
  3. promise 出 JS stack
  4. mutate 入 JS stack
  5. 打印出 mutate
  6. mutate 出 JS stack
  7. promise 入 JS stack
  8. 打印出 promise
  9. promise 出 JS stack

此時,全部的mircotask執行完畢,本輪事件循環結束,UI 開始 render.

  1. 頁面中inner的innerHTML變爲end

此時,UI render 完畢,開始下一輪事件循環.

  1. timeout 入 JS stack
  2. 彈出警告 錨點.
  3. 打印出 timeout
  4. timeout 出 JS stack
  5. timeout 入 JS stack
  6. 彈出警告 錨點.
  7. 打印出 timeout
  8. timeout 出 JS stack

到此爲止,整個事件執行完畢,咱們能夠看到在彈出警告框以前inner的內容已經改變

總結:首先執行macrotask,當js stack爲空時執行microtask,接着開始UI render,接着再開始下一輪循環

參考文獻:
深刻核心,詳解事件循環機制
Tasks, microtasks, queues and schedules

相關文章
相關標籤/搜索