1. 程式人生 > >【vue】nextTick原始碼解析

【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,
  childList: true
});

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

callback回撥函式擁有兩個引數:一個是描述所有被觸發改動的 MutationRecord 物件陣列,另一個是呼叫該函式的MutationObserver 物件。 不過這都是該屬性的用法了,VUE關於nextTick的原始碼裡關於這個屬性沒用到callback的這倆引數。這裡不做展開講解,詳情可以看這裡 MDN MutationObserver()

B、if條件成立

好了,掌握了MutationObserver和他的用法後,再來回歸原始碼,if裡邊的程式碼就很好理解了:

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

所以,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, {
  characterData: true
})

再接下來就是把程式碼頂部定義的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全域性搜尋

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 排版