vue---由nextTick原理引出的js執行機制
最開始檢視nextTick這個方法的時候,眼瞎看成了nextClick。。。我還在疑問難道是下一次click之後處理事件。。。
然後用這個方法的時候,就只知道是用在DOM更新之後呼叫回撥方法。
這時就產生了一堆疑問:
1)DOM更新後?難道修改資料之後,DOM沒有及時更新,還有延遲?但是頁面上看到的就是實時更新呀,難道還有什麼貓膩?
2)它是怎麼監聽到DOM被更新了
3)它和非同步的setTimeout、setInterval有沒有關係?
深入瞭解後才發現裡面有大學問。。。
在理解nextTick之前,先來一段程式碼
setTimeout(function(){ console.log(11) },300)
這段程式碼很簡單,一般人都會說,300ms之後控制檯打印出11。
但是,一定是精確的300ms之後馬上打印出11嗎。答案是不一定。為什麼?這就涉及到下面的知識點
1. js為什麼是單執行緒
深究原因我不是很清楚,但是我是這樣理解的:假如js是多執行緒,意思是如果我對同一個DOM進行操作,那麼都會同時處理。那這時一個執行緒我對一個按鈕修改顏色為red,同時另外一個執行緒對這個按鈕修改顏色為blue。那瀏覽器到底是執行哪一個呢,這樣就矛盾了。所以這就能很好理解為什麼要設計成單執行緒了。
2. Event loop
既然是單執行緒,那麼事件任務就一定會在主執行緒上排隊執行。同一時間就只能按佇列執行一個方法。要是某個方法要花費很長時間,那後面的方法就只能等待了,這是極其不能忍受的。所以js設計者把任務分成了同步任務和非同步任務。同步任務即主執行緒(執行棧)上執行的任務,而非同步任務則是掛載到一個任務佇列裡面。等待主執行緒的所有任務執行完成後(棧空),通知任務佇列可以把 可執行的任務 放到主執行緒裡面執行。非同步任務放到主執行緒中執行完後,棧又空了,又通知任務佇列把非同步任務放到主執行緒中執行。這個過程 一直持續,直到非同步任務執行完成 ,這個持續重複的過程就叫Event loop。而 一次迴圈就是一次tick 。
注意:
1) 這裡非同步任務例如setTimeout這種,實際上是先由瀏覽器其它模組(應該是IO裝置)處理之後,它的回撥函式才再加入到任務佇列裡面。注意是回撥函式。
2) onclick,onmouseover等都屬於非同步任務。回撥都會掛載到任務佇列。
3. microtast(微任務)和macrotask(巨集任務)
任務佇列裡面非同步任務也分macrotast(標準說法是task)和microtast(標準說法中它是不屬於task的)。
典型的microtast包含:Promises(瀏覽器原生Promise)、MutationObserver、Object.observe(已廢棄)、以及nodejs中的process.nextTick,UI rendering(UI渲染)
典型的macrotast包含: script整體程式碼(這個很重要)、 setTimeout(最短4ms) 、 setInterval(最短10ms)、MessageChannel、以及只有 IE 支援的 setImmediate。
執行優先順序上, 先執行巨集任務macrotask,再執行微任務mincrotask
process.nextTick > Promise.then > MutationObserver > setImmediate > setTimeout。
注意:
1) 對於microtast和macrotask,這兩個在一次event loop中,microtask在這一次迴圈中是一直取一直取,直到清空microtask佇列,而macrotask則是一次迴圈取一次。
2) 相當於事件迴圈的過程是:主執行緒(棧空)--->取一個macrotask執行---->檢視有沒有microtask,如果有就執行該任務直到清空microtask佇列,然後執行下一個macrotask任務--->又取macrotask執行--->清空microtask裡面的任務 。重複第二和第三的步驟直到macrotask任務佇列也執行完畢
3) 如果執行事件迴圈的過程中又加入了非同步任務,如果是macrotask,則放到macrotask末尾,等待下一輪迴圈再執行。如果是macrotask,則放到本次event loop中的microtask任務末尾繼續執行。直到microtask佇列清空。
4) 為什麼巨集任務先執行,反而處理時間還比微任務慢呢? 因為 script整體也是macrotask ,就先把script裡面的程式碼放到主執行緒執行,如果再 遇到macrotask ,就把它 放到macrotask任務佇列末尾 ,由於 一次event loop只能取一個macrotask ,所以遇到的巨集任務就需要等待其它輪次的事件迴圈了;如果 遇到microtask ,則放到 本次迴圈的microtask佇列 中去。這樣就能明白為什麼microtask會比macrotask先處理了。
到這裡,上面那個300ms的定時器為什麼不一定是精確的300ms之後列印就能理解了:
因為300ms的setTimeout並不是說300ms之後立馬執行, 而是300ms之後被放入任務列表裡面 。等待事件迴圈,等待它執行的時候才能執行程式碼。如果非同步任務列表裡面只有它這個macrotask任務,那麼就是精確的300ms。但是如果 還有microtast等其它的任務,就不止300ms了。
所以,下面的程式碼也能很好理解了
for(var i = 0; i < 3; i++) { console.log("for:"+i); var time=setTimeout(function() { console.log("setTime:"+i); }, 300); console.log(time) }
這個執行的結果是:
1) 當執行for迴圈的時候,定義了3個定時器,由於setTimeout是非同步任務,所以這三個定時器,每個都會在300ms之後加入任務佇列。
2) 此時執行程式碼,輸出for:xx,並列印對應定時器的標識。
3) 300ms之後,每個setTimeout的回撥函式加入到任務佇列,這時候for迴圈早就執行完畢了。
4) 執行完迴圈之後,此時相當於主執行緒棧空了,通知任務佇列,把非同步任務放到主執行緒執行,這時候就開始執行setTimeout的回撥函式。由於這時setTimeout匿名回撥函式保持對外部變數 i 的引用,而此時的 i 由於主執行緒執行完之後變成了3,所以最終再打印出3個setTime:3。
再來分析一下下面的程式碼:
console.log(1); setTimeout(function(){ console.log(2) },0); new Promise(function(resolve){ console.log(3) for( var i=100 ; i>0 ; i-- ){ i==1 && resolve() } console.log(4) }).then(function(){ console.log(5) }).then(function(){ console.log(6) }); console.log(7);
1) 由於script也屬於macrotask,所以整個script裡面的內容都放到了主執行緒(任務棧)中,按順序執行程式碼。然後遇到console.log(1),直接列印1。
2) 遇到setTimeout, 表示在0秒後才加入任務佇列 ,根據第3大點的 第3點注意事項,這個setTimeout會被放到下一個事件迴圈的macrotask裡面,這次不會執行。
3) 執行遇到new Promise, new Promise
在例項化的過程中所執行的程式碼都是同步進行的,只有回撥 .then()才是microtask。所以先直接列印3,執行完迴圈,然後再列印4。然後遇到第一個 .then(),屬於microtask,加入到本次迴圈的microtask佇列裡面。接著向下執行又遇到一個 .then() ,又加入到本次迴圈的microtask佇列裡面。然後繼續向下執行。
4) 遇到console.log(7),直接列印7。直到此時,一個事件迴圈的macrotask執行完成,然後去檢視此次迴圈是否還有microtask,發現還有剛才的 .then() ,立即放到主執行緒執行,打印出5。然後發現還有第二個 .then(),立即放到主執行緒執行,打印出6 。此時microtask任務列表清空完了。到此第一次迴圈完成。
5) 第二次事件迴圈,從macrotask任務列表裡面找到了第一次放進的setTimeout,放到主執行緒執行,打印出2。
6) 所以最終的結果就是 1 3 4 7 5 6 2
上面說了這麼多,就是為了下面做鋪墊
vue的nextTick使用方法:
接收兩個引數:
第一個是回撥函式,即DOM更新之後需要做的操作。
第二個是回撥函式中,this指標的指向。
vue.nextTick(cb,obj)
vm.$nextTick(cb)。 注意例項中使用nextTick的時候,cb回撥函式的this指向已經繫結為當前例項了。
這裡附上vue 2.6 版本 nextTick原始碼的連結 nextTick ,2.5版本與2.6有些不一樣。
export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => {//第一步 if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) {//第二步 pending = true timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') {//第三步 return new Promise(resolve => { _resolve = resolve }) } }
每次呼叫 Vue.nextTick(cb) :
1)cb 函式經處理壓入 callbacks 陣列,並且指定了cb的this指向。
2)pending表示是否正在執行回撥即是否已經有非同步任務在主執行緒執行,由於pending這個標識最初為false,所以把它設定為true,然後呼叫 timerFunc()。這個是用來觸發非同步回撥函式的。
3)如果沒有傳入回撥函式,並且支援promise的時候,則返回一個promise的呼叫
4)timerFunc()最初就看Promise(延遲呼叫) 、MutationObserver(監聽變化)、setImmediate 、setTimeout這四個中誰的相容當前瀏覽器,誰就優先用來做非同步API來處理回撥函式。
對於為什麼是下一個tick,我有問題:
1)在下次 DOM 更新迴圈結束之後執行延遲迴調。在修改資料之後立即使用這個方法,獲取更新後的 DOM。這是官方對於nextTick的說法。
2)在設定了vm.xxx='xxx'的時候,如果立即去DOM的內容,獲取到的並不是最新的值,說明DOM的更新一定是非同步的,因為同步的話就能獲取到修改後的內容了。但是nextTick的回撥函式,在呼叫後要麼屬於microtask,要麼就是macrotask,
3)如果是macrotask則好理解一點,因為執行程式碼遇到這個macrotask則會被新增到macrotask的末尾,等待event loop 取到它的時候才執行,而執行一次macrotask之後,如果microtask列表為空了,就會執行UI rendering,頁面就渲染成最新的內容。這時候是能獲取到更新後的內容的。
4)那如果是microtask, 就是在當前event loop中需要執行完畢,是屬於當前的tick,而這個時候是怎麼獲取到DOM更新的內容的???
對於上面的這個問題,好像要涉及到 watcher 中的 update 和 queueWatcher 。暫時就先放到一邊。反正作用是搞懂了,原理還差一點。
如果有明白這個問題的,麻煩給我講解一下。先謝謝了。