在作項目的時候,咱們常常會用到nextTick,簡單的理解就是它就是一個setTimeout函數,將函數放到異步後去處理;將它替換成setTimeout好像也能跑起來,但它僅僅這麼簡單嗎?那爲何咱們不直接用setTimeout呢?讓咱們深刻剖析一下。javascript
記得以前有一個需求,就是根據文字的行數來顯示展開更多的一個按鈕,所以咱們在Vue中給數據賦值以後須要獲取文字高度。html
<div id="app">
<div class="msg">
{{msg}}
</div>
</div>
new Vue({
el: '#app',
data: function(){
return {
msg: ''
}
},
mounted(){
this.msg = '我是測試文字'
console.log(document.querySelector('.msg').offsetHeight) //0
}
})
複製代碼
這時無論怎麼獲取,文字的Div高度都是0;可是直接獲取倒是有值:前端
一樣的狀況也發生在給子組件傳參上;咱們給子組件傳參數後,在子組件中調用函數查看參數。java
<div id="app">
<div class="msg"> <form-report ref="child" :name="childName"></form-report> </div> </div>
Vue.component('form-report', {
props: ['name'],
methods: {
showName(){
console.log('子組件name:'+this.name)
}
},
template: '<div>{{name}}</div>'
})
new Vue({
el: '#app',
data: function(){
return {
childName: '',
}
},
mounted(){
this.childName = '我是子組件名字'
this.$refs.child.showName()
}
})
複製代碼
雖然頁面上展現了子組件的name,可是打印出來倒是空值:面試
咱們發現上述兩個問題的發生,無論子組件仍是父組件,都是在給data
中賦值後立馬去查看數據致使的。因爲「查看數據」這個動做是同步操做的,並且都是在賦值以後;所以咱們猜想一下,給數據賦值操做是一個異步操做,並無立刻執行,Vue官網對數據操做是這麼描述的:數組
可能你尚未注意到,Vue 在更新 DOM 時是異步執行的。只要偵聽到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動。若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做是很是重要的。而後,在下一個的事件循環「tick」中,Vue 刷新隊列並執行實際 (已去重的) 工做。Vue 在內部對異步隊列嘗試使用原生的 Promise.then、MutationObserver 和 setImmediate,若是執行環境不支持,則會採用 setTimeout(fn, 0) 代替。瀏覽器
也就是說咱們在設置this.msg = 'some thing'
的時候,Vue並無立刻去更新DOM數據,而是將這個操做放進一個隊列中;若是咱們重複執行的話,隊列還會進行去重操做;等待同一事件循環中的全部數據變化完成以後,會將隊列中的事件拿出來處理。閉包
這樣作主要是爲了提高性能,由於若是在主線程中更新DOM,循環100次就要更新100次DOM;可是若是等事件循環完成以後更新DOM,只須要更新1次。還不瞭解事件循環的童鞋,能夠看個人另外一篇文章從一道面試題來理解JS事件循環app
爲了在數據更新操做以後操做DOM,咱們能夠在數據變化以後當即使用Vue.nextTick(callback)
;這樣回調函數會在DOM更新完成後被調用,就能夠拿到最新的DOM元素了。異步
//第一個demo
this.msg = '我是測試文字'
this.$nextTick(()=>{
//20
console.log(document.querySelector('.msg').offsetHeight)
})
//第二個demo
this.childName = '我是子組件名字'
this.$nextTick(()=>{
//子組件name:我是子組件名字
this.$refs.child.showName()
})
複製代碼
瞭解了nextTick的用法和原理以後,咱們就來看一下Vue是怎麼來實現這波「操做」的。
Vue把nextTick的源碼單獨抽到一個文件中,/src/core/util/next-tick.js
,刪掉註釋也就大概六七十行的樣子,讓咱們逐段來分析。
const callbacks = []
let pending = false
let timerFunc
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
})
}
}
複製代碼
咱們首先找到nextTick
這個函數定義的地方,看看它具體作了什麼操做;看到它在外層定義了三個變量,有一個變量看名字就很熟悉:callbacks,就是咱們上面說的隊列;在nextTick的外層定義變量就造成了一個閉包,因此咱們每次調用$nextTick的過程其實就是在向callbacks新增回調函數的過程。
callbacks新增回調函數後又執行了timerFunc函數,pending
用來標識同一個時間只能執行一次。那麼這個timerFunc函數是作什麼用的呢,咱們繼續來看代碼:
export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//判斷1:是否原生支持Promise
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
//判斷2:是否原生支持MutationObserver
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
//判斷3:是否原生支持setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
//判斷4:上面都不行,直接用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
複製代碼
這裏出現了好幾個isNative
函數,這是用來判斷所傳參數是否在當前環境原生就支持;例如某些瀏覽器不支持Promise,雖然咱們使用了墊片(polify),可是isNative(Promise)仍是會返回false。
能夠看出這邊代碼實際上是作了四個判斷,對當前環境進行不斷的降級處理,嘗試使用原生的Promise.then
、MutationObserver
和setImmediate
,上述三個都不支持最後使用setTimeout;降級處理的目的都是將flushCallbacks
函數放入微任務(判斷1和判斷2)或者宏任務(判斷3和判斷4),等待下一次事件循環時來執行。MutationObserver
是Html5的一個新特性,用來監聽目標DOM結構是否改變,也就是代碼中新建的textNode;若是改變了就執行MutationObserver構造函數中的回調函數,不過是它是在微任務中執行的。
那麼最終咱們順藤摸瓜找到了最終的大boss:flushCallbacks;nextTick不顧一切的要把它放入微任務或者宏任務中去執行,它到底是何方神聖呢?讓咱們來一睹它的真容:
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
複製代碼
原本覺得有多複雜的flushCallbacks,竟然不太短短的8行。它所作的事情也很是的簡單,把callbacks數組複製一份,而後把callbacks置爲空,最後把複製出來的數組中的每一個函數依次執行一遍;因此它的做用僅僅是用來執行callbacks中的回調函數。
到這裏,總體nextTick的代碼都分析完畢了,總結一下它的流程就是:
再回到咱們開頭說的setTimeout,能夠看出來nextTick是對setTimeout進行了多種兼容性的處理,寬泛的也能夠理解爲將回調函數放入setTimeout中執行;不過nextTick優先放入微任務執行,而setTimeout是宏任務,所以nextTick通常狀況下老是先於setTimeout執行,咱們能夠在瀏覽器中嘗試一下:
setTimeout(()=>{
console.log(1)
}, 0)
this.$nextTick(()=>{
console.log(2)
})
this.$nextTick(()=>{
console.log(3)
})
//運行結果 2 3 1
複製代碼
最後驗證猜測,當前宏任務執行完成後,優先執行兩個微任務,最後再執行宏任務。
更多前端資料請關注公衆號【前端壹讀】
。