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

上圖中,可以看到:
-
nextTick
等於一個立即執行函數。函數執行後,內部返回另一個匿名函數function (cb, ctx)
。從語義化命名可以分析,第一個參數cb
是個回調函數、ctx
這裡先猜測應該是個上下文。 -
在
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
的匿名函數里。

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