【vue】nextTick源碼解析

1、整體入手

閱讀代碼和畫畫是一樣的,忌諱一開始就從細節下手(比如一行一行讀),我們先將細節代碼摺疊起來,整體觀察nextTick源碼的幾大塊。

摺疊後代碼如下圖

整體觀察代碼結構
整體觀察代碼結構

上圖中,可以看到:

  1. nextTick等於一個立即執行函數。函數執行後,內部返回另一個匿名函數function (cb, ctx)。從語義化命名可以分析,第一個參數cb是個回調函數、ctx這裡先猜測應該是個上下文。

  2. return返回之前,立即執行函數被調用後,函數內部先用var定義了三個參數、用function聲明一個函數。

先不管這些變量是幹啥用的。光從語義化命名上瞎分析一下:

  • callbacks可能是一個裝callback回調的數組,可能是將來有多個回調的時候模擬隊列執行效果用的。

  • pending是一個布爾值。pending這個單詞在接口請求中會看到,可能是用來標識某個狀態是否正在進行中的。

  • timeFunc目前看來就不知道具體幹啥的了。

  • nextTickHandler函數先不管。用到的時候再來看。

以上,就是初始化對代碼的分析。

2、逐行解析

看完大的代碼塊結構後,可以按照js引擎解析代碼的順序來分析源碼了。前邊的變量和函數聲明看完後,就到解析if語句了。

在if條件中,有一個判斷:typeof MutationObserver !== 'undefined' && !hasMutationObserverBug

MutationObserver這玩意兒是幹啥的?

A、MutationObserver

度娘說他「提供了監視對DOM樹所做更改的能力」。大白話粗糙理解就是他能監聽dom修改。

是HTML5中的一個新特性。

MutationObserver()

該屬性提供一個構造函數MutationObserver()

通過new MutationObserver()可以得到一個新的觀察器,它會在觸髮指定 DOM 事件時,調用指定的回調函數。

MutationObserver 對 DOM 的觀察不會立即啟動;而必須先調用 observe() 方法來確定,要監聽哪一部分的 DOM 以及要響應哪些更改。

observe(target[, options])

啟用觀察者,開始根據配置監聽指定DOM。無返回值。

接收兩個參數:

  • target是Node/Element節點,表示要監聽的DOM對象。

  • options是監聽配置,配置了target的哪些變動需要出發callback回調。配置項相關參數參照MutationObserverInit配置字典

    • attributes : true|false, 觀察受監視DOM元素的任意一個屬性值變更
    • attributeFilter: 監聽多個特定屬性,放到數組裡。如:['class', 'id', 'src']
    • characterData : true|false, 為true,則在更改指定要 監聽的文本節點的內容時,將調用callback回調。
    • childList :true|false, 為 true 就監視指定DOM對象添加或刪除新的子節點的情況
    • 還有其他好幾個擴展情況。參考MutationObserverInit配置字典

    當調用 observe() 方法時,childList,attributes 或者 characterData 三個屬性之中,至少有一個必須為 true,否則會拋出 TypeError 異常。

語法

// 得到要觀察的元素
var elementToObserve = document.querySelector("#targetElementId");

// 構造MutationObserver對象,傳遞一個函數當做參數
var observer = new MutationObserver(callback);

// 啟用觀察者observe(), 監聽的DOM對象是elementToObserve
observer.observe(elementToObserve, { // 監聽規則,當子節點或目標節點整個節點樹中的所有節點被添加/刪除的時候,觸發上邊的callback回調函數
  subtree: true,
  childListtrue
});

當MutationObserver監聽到我們註冊的DOM被改變(無論是DOM節點改變、還是DOM的屬性被改變,主要監聽DOM的哪部分改變啥還是看你的配置項)時,回調函數callback就會被調用。
(有點像我們派到云云DOM對象中的一個間諜,監視我們指定的dom,當發生改變時就告知我們)

callback回調函數擁有兩個參數:一個是描述所有被觸發改動的 MutationRecord 對象數組,另一個是調用該函數的MutationObserver 對象。
不過這都是該屬性的用法了,VUE關於nextTick的源碼里關於這個屬性沒用到callback的這倆參數。這裡不做展開講解,詳情可以看這裡 MDN MutationObserver()

B、if條件成立

好了,掌握了MutationObserver和他的用法後,再來回歸源碼,if裡邊的代碼就很好理解了:
MutationObserver判斷成功

首先,作為H5新特性,其兼容性就是不太好(IE爸爸:看我幹嘛!)
MutationObserver兼容性

所以,vue這裡做了容錯,先判斷MutationObserver的類型是否為「undefined」,來檢查瀏覽器是否支持該特性。如果支持這個屬性且無bug,那麼就走if語句的內容

if語句內部三個var:

var counter = 1
var textNode = document.createTextNode(counter)
var observer = new MutationObserver(nextTickHandler)
  • 定義了一個counter數字
  • textNode變量用於存放document.createTextNode創建的一個文本節點,文本內容是counter的值
  • new MutationObserver() 這一行,相信有了上邊知識點的鋪墊,你就很容易理解了。構造並返回一個新的observer,用於在指定的DOM(就是上邊的textNode)發生變化時,調用回調函數nextTickHandler

接下來觀察者observer,根據MutationObserverInit配置字段的設置,監聽textNode元素。當textNode文本節點的文本內容發生一丟丟變化時,就會立即觸發nextTickHandler回調函數。

var observer = new MutationObserver(nextTickHandler)
observer.observe(textNode, {
  characterDatatrue
})

再接下來就是把代碼頂部定義的timerFunc變量賦值為一個函數。

timerFunc = function ({
  counter = (counter + 1) % 2
  textNode.data = counter
}

函數內部通過(counter + 1) % 2的表達式思想,讓counter的值因為每次timeFunc函數的調用都會變成0/1。
並通過將counter變化後的值賦值給textNode節點,實現改變textNode文本節點的內容,達到觸發observer監聽、進而調取nextTickHandler回調函數的目的。

至此,if語句內部流程就走完了。我們趁熱打鐵,先不看else里的內容(腳指頭掰也能想到裡邊應該是不兼容MutationObserver後的降級方案了。

根據if裡邊的思路,我們該看nextTickHandler里都是啥了,監聽了DOM變化後,每次回調都幹了撒?

C、nextTickHandler()

逐句閱讀代碼:

// 1
pending = false

每次nextTickHandler調用,pending先置為false,之前猜測pending是一個鎖的想法,進一步得到了驗證。

// 2
var copies = callbacks.slice(0)

利用數組的slice()方法,傳入起始下標0,不傳終點下標,得到一個淺拷貝callbacks的新數組,並複製給copies

// 3
callbacks = []

重新賦值callback為一個空數組

// 4
for (var i = 0; i < copies.length; i++) {
    copies[i]()
}

最後遍歷copies數組,順序調取copies隊列里的函數。
鬱悶了,這個copies里的(確切的說是callbacks里的)每一項函數都是個啥?哪來的?

這得看看callbacks這個變量在哪裡賦值了、賦值的都是啥。於是我們
全局搜索callbacks,發現除了目前看到的三個,還有一個在return 的匿名函數里。

callbacks全局搜索
callbacks全局搜索

D、return

本着哪裡不會點哪裡的原則,說明到了我們觀察返回的這個匿名函數內部代碼的時候了。

源碼里,nextTick等於一個立即執行函數,函數執行完畢return一個匿名函數如下,也就是說,下邊的代碼就是我們調用nextTick的時候調用的函數。

function (cb, ctx{
  var func = ctx
      ?
      function ({
          cb.call(ctx)
      }
      :
      cb
  callbacks.push(func)
  if (pending) return
  pending = true
  timerFunc(nextTickHandler, 0)
}

nextTick用法

我們先回憶一下nextTick的用法:

// modify data
vm.msg = 'Hello'
// DOM not updated yet
Vue.nextTick(function ({
  // DOM updated
})

可以看到,nextTick的第一個參數傳入一個匿名函數。函數裡邊代碼就是我們開發者執行nextTick後要運行的內容。

於是我們知道了,我們調用nextTick時傳入的function () { // DOM updated }對應的就是return 後邊匿名函數的cb參數。

執行上下文

在匿名函數裡邊,先判斷nextTick調用時第二個參數是否填,如果沒填就直接將cb函數賦值給func變量。

var func = ctx
  ?
  function ({
      cb.call(ctx)
  }
  :
  cb

如果填了第二個參數,func就等於一個匿名函數,函數內部利用call調用cb回調,改變cb內部this指向。由call調用時的傳參為ctx可以推導出,nextTick的第二個參數ctx是一個上下文參數,用於改變第一個參數內部的this指向。

callbacks隊列

緊接着將func函數推送到callbacks隊列中:callbacks.push(func)。說明callbacks(也就是nextTickHandler函數里的copies)里存的就是nextTick的第一個回調函數參數。for循環執行的也就是他們。

pending加鎖

if (pending) return

利用閉包,判斷如果上一個nextTick未執行完畢,則本次的nextTick不能完整執行、會運行到了if這裡被中斷。

如果pending為false,說明上次的nextTick回調函數已經完了,可以進行本次執行。並緊接着pending = true將本次的nextTick調用狀態改為pending中。

這pending就好像收費站的柵欄,上一輛車過去後立馬落下杆子,上一輛車未繳費完畢、開走之前,不收起杆子。每次起杆子前,都看下是否有上一輛車正在堵着通道在繳費,如果沒有,則可以開啟杆子,讓一輛車過去,放過一輛車後立馬又落下杆子阻止後邊的車。

timerFunc

最後調用timerFunc(nextTickHandler, 0)

先來看看timerFunc是啥:

立即執行函數里聲明後未被初始化

var timerFunc

緊接着判斷MutationObserver可用的話,在if代碼塊里被賦值為函數:

timerFunc = function ({
  counter = (counter + 1) % 2
  textNode.data = counter
}

函數里修改counter的值並賦值給textNode.data:

這個我們上邊分析過,當指定的DOM「textNode」文本節點的文本內容發生變化時,MutationObserver對象的ovserve監聽方法就會立即調用回調函數nextTickHandler

於是我們知道了整個流程:timerFunc調用,也就等於nextTickHandler調用,nextTickHandler調用後,內部遍歷調用copies的每一項,即遍歷調用多個nextTick的第一個函數參數(這是因為pending把下一個nextTick攔住了,不過每次調用nextTick時的第一個回調參數都被push到callbacks里了,當有幾個被阻塞的nextTick回調還沒被執行的情況下,callbacks數組裡就可能不止一個回調函數,因此就需要用for循環依次調用)。

至此,我們的整個流程終於疏通完了。

等等,人家調用timerFunc時有傳參啊。MutationObserver里給timerFunc賦值時,匿名函數沒接收參數啊。

優雅降級

這時我們全局搜索timerFunc,發現我們漏了一個else代碼塊還沒看:

else {
  const context = inBrowser ?
      window :
      typeof global !== 'undefined' ? global : {}
  timerFunc = context.setImmediate || setTimeout
}

這裡,用「inBrowser」判斷是否為瀏覽器環境,然後給context賦值為window/global/{},

給timerFunc賦值為context.setImmediate(ie或者node環境)或者window.setTimeout(其他環境),主要看當前運行的環境。

這裡是vue的降級處理方式,如果瀏覽器不支持MutationObserver的話,就用setImmediate,如果不支持setImmediate的話,就用setTimeout來模擬異步方式。

當流程走到else代碼塊里的話,timerFunc調用就需要傳遞一個匿名函數(這裡為nextTickHandler)和一個interval的值(這裡為0)了

本文使用 mdnice 排版