【vue】nextTick原始碼解析
1、整體入手
閱讀程式碼和畫畫是一樣的,忌諱一開始就從細節下手(比如一行一行讀),我們先將細節程式碼摺疊起來,整體觀察nextTick原始碼的幾大塊。
摺疊後代碼如下圖
上圖中,可以看到:
nextTick
等於一個立即執行函式。函式執行後,內部返回另一個匿名函式function (cb, ctx)
。從語義化命名可以分析,第一個引數cb
是個回撥函式、ctx
這裡先猜測應該是個上下文。在
return
返回之前,立即執行函式被呼叫後,函式內部先用var定義了三個引數、用function宣告一個函式。
先不管這些變數是幹啥用的。光從語義化命名上瞎分析一下:
callbacks
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 排版