vue---由nextTick原理引出的js執行機制

最開始查看nextTick這個方法的時候,眼瞎當作了nextClick。。。我還在疑問難道是下一次click以後處理事件。。。vue

而後用這個方法的時候,就只知道是用在DOM更新以後調用回調方法。node

這時就產生了一堆疑問:git

1)DOM更新後?難道修改數據以後,DOM沒有及時更新,還有延遲?可是頁面上看到的就是實時更新呀,難道還有什麼貓膩?github

2)它是怎麼監聽到DOM被更新了數組

3)它和異步的setTimeout、setInterval有沒有關係?promise

深刻了解後才發現裏面有大學問。。。瀏覽器

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

 

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

相關文章
相關標籤/搜索