經過一道題進入瀏覽器事件循環原理: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
macroTask
和microTask
宏隊列,macroTask
也叫tasks
。包含同步任務,和一些異步任務的回調會依次進入macro task queue
中,macroTask
包含:數組
微隊列, microtask
,也叫jobs
。另一些異步任務的回調會依次進入micro task queue
,等待後續被調用,這些異步任務包含:promise
下面是Event Loop
的示意圖
一段javascript
執行的具體流程就是以下:瀏覽器
script
就是至關於一個macrotask
,因此他先會執行同步代碼,當遇到例如setTimeout
的時候,就會把這個異步任務推送到宏隊列隊尾中。macrotask
執行完成之後,就會從微隊列中取出位於頭部的異步任務進行執行,而後微隊列中任務的長度減一。microtask
,那麼會加入整個隊列的隊尾,也會在當前的週期中執行macrotask
,執行完成之後再執行微隊列,以此反覆。從1
到3
的過程就是一個循環,也就是我們下面講到的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) } }
優先使用Promise
、MutationObserver
由於這兩個方法的回調函數都會在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
從而實現回調。
若是上述兩種方法都不支持的環境則會使用setTimeout
。setTimeout
會在下一個tick
中執行。爲何使用這種方式,根據HTML Standard
,每一個task
運行完之後,UI
都會從新渲染,那麼在microtask
中完成數據更新,當前task
結束後就能夠獲得最新的UI
了,不然就須要等到下一個tick
進行數據更新,可是此時已經渲染了兩次
注意:這個部分須要對Vue
源碼有必定的瞭解
下面有一個示例,點擊按鈕,會讓count
從0
增長到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
對象的update
,run
方法用來更新頁面。
當某個數據發生改變時,就會往queue
中加入屬於這個數據的watcher
,每一個watcher
都有專屬的id
,這樣就避免重複添加同一個watcher
。waiting
是一個標誌位,在下一個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
中只會存在一個watcher
。run
方法的調用會在nextTick
中調用,也就是先前提到的microtask
中進行調用。從而輸出了上面的結果
本文講了js
的事件輪詢機制,是否是對同步異步瞭解的更加清晰。而且在尤大也是巧妙的運行了這種思路,對這個知識點進行了落地。學一個知識點最重要的對其進行落地,能夠本身多嘗試一下,更加深刻了解事件輪詢機制。github
求關注,感謝。