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的,應該叫 job)。

典型的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末尾,等待下一輪循環再執行。如果是microtask,則放到本次event loop中的microtask任務末尾繼續執行。直到microtask隊列清空。

4) 為什么宏任務先執行,反而處理時間還比微任務慢呢?因為script整體也是macrotask,就先把script里面的代碼放到主線程執行,如果再遇到macrotask,就把它放到macrotask任務隊列末尾,由於一次event loop只能取一個macrotask放到主線程執行,所以遇到的宏任務就需要等待其它輪次的事件循環了;如果遇到microtask,則放到本次循環的microtask隊列中去,等待主線程執行完再執行microtast隊列並清空。然后繼續執行下一個tick的宏任務。

 

到這里,上面那個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之后加入任務隊列,並且是macrotask隊列。

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里面,這次event loop不會執行。

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 中的 updatequeueWatcher 。暫時就先放到一邊。反正作用是搞懂了,原理還差一點。

 

如果有明白這個問題的,麻煩給我講解一下。先謝謝了。 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM