瀏覽器事件循環原理

經過一道題進入瀏覽器事件循環原理:javascript

console.log('script start')
setTimeout(function () {
  console.log('setTimeout')
}, 0);
Promise.resolve().then(function () {
  console.log('promise1')
}).then(function () {
  console.log('promise2')
})
console.log('script end')

能夠先試一下,手寫出執行結果,而後看完這篇文章之後,在運行一下這段代碼,看結果和預期是否同樣html

單線程

定義

單線程意味着全部的任務須要排隊,前一個任務結束,纔可以執行後一個任務。若是前一個任務耗時很長,後面一個任務不得不一直等着。java

緣由

javascript的單線程,與它的用途有關。做爲瀏覽器腳本語言,javascript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定javascript同時有兩個線程,一個在添加DOM節點,另一個是刪除DOM節點,那瀏覽器應該應該以哪一個爲準,若是在增長一個線程進行管理多個線程,雖然解決了問題,可是增長了複雜度,爲何不使用單線程呢,執行有個前後順序,某個時間只執行單個事件。
爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,運行javascript建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個標準並無改變javascript單線程的本質git

瀏覽器中的Event Loop

js的執行環境是一個單線程,會按照順序執行代碼,可是javaScript又能夠是異步,這二者感受有衝突。若是理解瀏覽器的事件循環機制,就會以爲不衝突。github

macroTaskmicroTask

宏隊列,macroTask也叫tasks。包含同步任務,和一些異步任務的回調會依次進入macro task queue中,macroTask包含:數組

  • script代碼塊
  • setTimeout
  • requestAnimationFrame
  • I/O
  • UI rendering

微隊列, microtask,也叫jobs。另一些異步任務的回調會依次進入micro task queue,等待後續被調用,這些異步任務包含:promise

  • Promise.then
  • MutationObserver

下面是Event Loop的示意圖

一段javascript執行的具體流程就是以下:瀏覽器

  1. 首先執行宏隊列中取出第一個,一段script就是至關於一個macrotask,因此他先會執行同步代碼,當遇到例如setTimeout的時候,就會把這個異步任務推送到宏隊列隊尾中。
  2. 當前macrotask執行完成之後,就會從微隊列中取出位於頭部的異步任務進行執行,而後微隊列中任務的長度減一。
  3. 而後繼續從微隊列中取出任務,直到整個隊列中沒有任務。若是在執行微隊列任務的過程當中,又產生了microtask,那麼會加入整個隊列的隊尾,也會在當前的週期中執行
  4. 當微隊列的任務爲空了,那麼就須要執行下一個macrotask,執行完成之後再執行微隊列,以此反覆。

13的過程就是一個循環,也就是我們下面講到的tick,所謂的事件循環就是重複一個一個的tick異步

示例分析

在前面給出了一道題,如今來對這道題進行分析。下面是這段代碼的流程分析圖:

首先整個代碼塊是一個task因此,先運行同步代碼,當執行到setTimeout的時候,會向宏隊列隊尾中推入整個異步任務,這時候宏隊列就有兩個任務,當同步任務執行完成之後,也就是第一個task執行完成之後,會執行微隊列中的任務。Promise是屬於microtask,因此會推入微隊列中。因此輸出結果以下:函數

script start
script end
promise1
promise2
setTimeout

Vue nextTick原理

Vue內部實現了nextTick函數,傳入一個cb函數,這個cb會存儲到一個隊列中,在下一個tick中觸發隊列中全部的cb事件。
首先定義一個數組callbacks來存儲下一個tick須要執行的任務,pending是一個標誌位,保證在下一個tick以前只執行一次。timeFunc是一個函數指針,針對瀏覽器支持狀況,使用不一樣的方法

function nextTick() {
  const callbacks = [];
  let pending = false;
  let timeFunc
}
function nextTickHandler() {
  pending = false;
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

nextTickHandler的做用就是將callbacks存儲的函數都調用一遍。下面再來看timeFunc的實現:

if (typeof Promise !== 'undefined') {
  timeFunc = () => {
    Promise.resolve()
      .then(nextTickHandler)
  }
} else if (typeof MutationObserver !== 'undefined') {
  // ...
} else {
  timeFunc = () => {
    setTimeout(nextTickHandler, 0)
  }
}

優先使用PromiseMutationObserver由於這兩個方法的回調函數都會在microtask中執行,他們會比setTimeout更早執行,因此優先使用。下面是MutationObserver的實現:

const counter = 1;
const observer = new MutationObserver(nextTickHandler)
const textNode = document.createTextNode(counter)
observer.observe(textNode, {
    characterData: true,
})
timeFunc = () => {
    couter = (counter + 1) % 2;
    textNode.data = String(counter)
}

每次調用timeFunc,都會更改counter的值,改變DOM的值後,觸發observer從而實現回調。
若是上述兩種方法都不支持的環境則會使用setTimeoutsetTimeout會在下一個tick中執行。爲何使用這種方式,根據HTML Standard,每一個task運行完之後,UI都會從新渲染,那麼在microtask中完成數據更新,當前task結束後就能夠獲得最新的UI了,不然就須要等到下一個tick進行數據更新,可是此時已經渲染了兩次

Vue的批量異步更新策略

注意:這個部分須要對Vue源碼有必定的瞭解
下面有一個示例,點擊按鈕,會讓count0增長到1000。若是每次count的修改都會觸發DOM的更新,那麼DOM都會更新1000次,那手機就卡死了。

<div>{{count}}</div>
<button @click="addCount">click</button>
data () {
    return {
        count: 0,
    }
},
methods: {
    addCount() {
        for (let i = 0; i < 1000; i++ ){
            this.count += 1;
        }
    }
}

那麼Vue是如何避免這種事情的,每次觸發某個數據的setter方法後,對應的Watcher對象就會被push進一個隊列queue中,Watcher對象用來觸發真實DOM的更新。

let id = 0;
class Watcher {
    constructor() {
        this.id = id++;
    }
    update() {
        console.log('update:' + id);
        queueWatcher(this);
    }
    run() {
        console.log('run:' + id);
    }
}

當觸發setter會觸發Watcher對象的updaterun方法用來更新頁面。

當某個數據發生改變時,就會往queue中加入屬於這個數據的watcher,每一個watcher都有專屬的id,這樣就避免重複添加同一個watcherwaiting是一個標誌位,在下一個tick的時候執行flushSchedulerQueue來執行隊列queue中全部的watcher對象的run方法

const has = {};
const queue = [];
let waiting = false;
function queueWatcher(watcher) {
    const id = watcher.id;
    if (has[id] == null) {
        queue.push(watcher)
        has[id] = true;
    }
    if (!waiting) {
        waiting = true;
        nextTick(flushScheulerQueue)
    }
}
function flushScheulerQueue() {
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        id = watcher.id;
        has[id] = null;
        watcher.run();
    }
    wating = false;
}

這樣當一個值屢次發生改變時,實際上只會往這個queue隊列中加入一個,而後在nextTick中進行回調,遍歷queue對頁面進行更新,這樣也就實現了屢次更改data的時候只會更新一次DOM,可是在項目中也須要儘可能避免這種屢次更改的狀況。
例如如下代碼:

const watcher1 = new Watcher();
const wather2 = new Watcher();

watcher1.update();
watcher2.update();
watcher2.update();

一個watcher觸發了兩次update,可是輸出結果以下:

update: 1
update: 2
update: 2
run: 1
run: 2

雖然watcher2觸發了兩次update,可是由於Vue對相同的Watcher進行了過濾,因此在queue中只會存在一個watcherrun方法的調用會在nextTick中調用,也就是先前提到的microtask中進行調用。從而輸出了上面的結果

本文講了js的事件輪詢機制,是否是對同步異步瞭解的更加清晰。而且在尤大也是巧妙的運行了這種思路,對這個知識點進行了落地。學一個知識點最重要的對其進行落地,能夠本身多嘗試一下,更加深刻了解事件輪詢機制。github求關注,感謝。

相關文章
相關標籤/搜索