使用場景
1.當改變了DOM中的數據後,立馬去獲取DOM中的數值
2.當我們在watch偵聽器中改變了數據,立馬用這一些數據去做一些操作,這裏爲什麼不可以呢?因爲watch其實應該也算是會被推到隊列中去執行,而nextTick就是等當前DOM更新,也就是隊列全部完成後執行
在vue官網深入響應式原理異步跟新隊列中有寫
3.Vue生命週期的created()鉤子函數進行的DOM操作一定要放在Vue.nextTick()的回調函數中,原因是在created()鉤子函數執行的時候DOM 其實並未進行任何渲染,而此時進行DOM操作無異於徒勞,所以此處一定要將DOM操作的js代碼放進Vue.nextTick()的回調函數中。與之對應的就是mounted鉤子函數,因爲該鉤子函數執行時所有的DOM掛載已完成。
4.當項目中你想在改變DOM元素的數據後基於新的DOM做點什麼,對新DOM一系列的js操作都需要放進Vue.nextTick()的回調函數中;通俗的理解是:更改數據後當你想立即使用js操作新的DOM的時候
5.在使用某個第三方插件時 ,希望在vue生成的某些dom動態發生變化時重新應用該插件,也會用到該方法,這時候就需要在 $nextTick 的回調函數中執行重新應用插件的方法
源碼解析
函數定義
一
將nextTick定義到Vue原型鏈上代碼位於src/core/instance/render.js,代碼如下
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this) // 默認this等於Vue.prototype
}
上述代碼中return的nextTick就是我們本文主角,他的定義如下
二
export function nextTick (cb?: Function, ctx?: Object) { // next-tick.js line87
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()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
- 函數參數cb?: Function,意味參數cb(callback)的類型爲函數或undefined,ctx(context)同理,這種類型判斷是TypeScript的寫法。
- 同事可以看出在綁定$nextTick的時候this默認爲Vue.prototype,
- 函數體內callbacks是在next-tick.js line10定義的一個數組,判斷如果參數cb不爲undefined,就把cb外加(this) push到callbacks中,如果cb爲undefined,則把_resolve(ctx)push到callbacks中。
注意
1.當我們一般在單文件中使用this.$nextTick的時候這個時候this不是Vue.propType,而是單文件中的對象,但是一般那個對象是繼承於Vue的
2.因爲平時使用都是傳回調的,所以很好奇cb什麼情況下會爲undefined,去翻看Vue官方文檔發現:
2.1.0 起新增:如果沒有提供回調且在支持 Promise 的環境中,則返回一個 Promise。
3. 爲什麼使用nextTick的時候可以使用箭頭函數,即nextTick函數定義中的cb.call(ctx)
測試
created() {
Vue.nextTick(undefined, { a: 'in nextTick' }).then(ctx => {
console.log(ctx.a) // ctx是對象{ a: 'in nextTick'}
})
console.log('out nextTick')
}
輸出:out nextTick in nextTick
三
函數體內只剩下中間if (!pending),這段代碼很好懂,pending明顯是一個狀態位,而timerFunc()就應該是nextTick實現異步的核心了
timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
if (isIOS) setTimeout(noop)
}
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else {
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
可以看到nextTick優先使用microTask(Promise和MutationObserver)然後使用macroTask(setImmediate和setTimeout)
四
flushCallbacks
function flushCallbacks () { // next-tick.js line13
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
因爲callbacks裏都是函數,所以一層淺拷貝的方式就可以滿足複製需求,定義一個copies數組等於callbacks,然後清空callbacks,然後遍歷copies數組調用其中的函數
如何保證只在接收第一個回調函數時執行異步方法?
nextTick源碼中使用了一個異步鎖的概念,即接收第一個回調函數時,先關上鎖,執行異步方法。此時,瀏覽器處於等待執行完同步代碼就執行異步代碼的情況。
打個比喻:相當於一羣旅客準備上車,當第一個旅客上車的時候,車開始發動,準備出發,等到所有旅客都上車後,就可以正式開車了。
當然執行flushCallbacks函數時有個難以理解的點,即:爲什麼需要備份回調函數隊列?執行的也是備份的回調函數隊列?
因爲,會出現這麼一種情況:nextTick套用nextTick。如果flushCallbacks不做特殊處理,直接循環執行回調函數,會導致裏面nextTick中的回調函數會進入回調隊列。這就相當於,下一個班車的旅客上了上一個班車。
這裏還有一個問題是我比較困擾的?第一次使用nextTick的時候就是回立馬執行flushCallbacks這裏其實數組就是一項,但是爲什麼還是要數組呢?其實就是爲了放在第一個nextTick執行的過程中後面添加了新的多個nextTick,所以使用數組