由於vue的試圖渲染是非同步的,生命周期的created()鉤子函數進行的DOM操作一定要放在Vue.nextTick()的回調函數中,原因是在created()鉤子函數執行的時候DOM其實並未進行渲染,而此時進行DOM操作是徒勞的,所以一定要將DOM操作的js代碼放到Vue.nextTick()的回... ...
1.為什麼需要使用$nextTick?
首先我們來看看官方對於$nextTick的定義:
在下次 DOM 更新迴圈結束之後執行延遲回調。在修改數據之後立即使用這個方法,獲取更新後的 DOM。
由於vue的試圖渲染是非同步的,生命周期的created()鉤子函數進行的DOM操作一定要放在Vue.nextTick()的回調函數中,原因是在created()鉤子函數執行的時候DOM其實並未進行渲染,而此時進行DOM操作是徒勞的,所以一定要將DOM操作的js代碼放到Vue.nextTick()的回調函數中。除了在created()鉤子函數中使用之外咱們還會遇到很多種需要使用到Vue.nextTick()的場景,如下所示:
咱們日常生活中常常會遇上上述場景,當我們點擊按鈕更新數據時候,如下示例:
<template>
<div>
<input type="text" v-if = "isShow" ref="input"/>
<button @click="handleClick">點擊顯示輸入框,並且獲取輸入框焦點</button>
</div>
</template>
<script>
export default {
data() {
return {
isShow: false
}
},
methods : {
handleClick () {
this.isShow = true
this.$refs.input.focus() //控制欄會報錯,因為還沒有這個dom
}
}
}
</script>
點擊控制欄顯示效果:控制欄報錯,提示沒有獲取到dom元素;
所以現在Vue.nextTick()派上了用場,Vue.nextTick() 方法的作用正是等待上一次事件迴圈執行完畢,併在下一次事件迴圈開始時再執行回調函數。這樣可以保證回調函數中的 DOM 操作已經被 Vue.js 進行過更新,從而避免了一些潛在的問題,如下代碼所示:
<template>
<div>
<input type="text" v-if = "isShow" ref="input"/>
<button @click="handleClick">點擊顯示輸入框,並且獲取輸入框焦點</button>
</div>
</template>
<script>
export default {
data() {
return {
isShow: false
}
},
methods : {
handleClick () {
this.isShow = true
this.$nextTick(()=>{
this.$refs.input.focus()
})
}
}
}
</script>
加上this.$nextTick後就能夠使得輸入框獲取到焦點;
總而言之Vue.nextTick()就是下次 DOM 更新渲染後執行延遲回調函數。在日常開發中,我們在修改數據之後使用這個方法,就可以獲取更新後的 DOM的同時進行在對DOM進行相對應操作的 js代碼;
2.$nextTick如何實現的?
JS是單線程執行的,所有的同步任務都是在主線程上執行的,形成了一個執行棧,從上到下依次執行,非同步代碼會放在任務隊列裡面。
•同步任務
在主線程里執行,當瀏覽器第一遍過濾html文件的時候可以執行完;(在當前作用域直接執行的所有內容,包括執行的方法、new出來的對象)
•非同步任務
耗費時間較長或者性能較差的,瀏覽器執行到這些的時候會將其丟到非同步任務隊列中,不會立即執行
同時非同步任務分為巨集任務(如setTimeout、setInterval、postMessage、setImmediate等)和微任務(Promise、process.nextTick等),瀏覽器執行這兩種任務的優先順序不同;會優先執行微任務隊列的代碼,微任務隊列清空之後再執行巨集任務的隊列,這樣迴圈往複;
JS自上向下進行代碼的編譯執行,遇到同步代碼壓入JS執行棧執行後出棧,遇到非同步代碼放入任務隊列,當JS執行棧清空,去執行非同步隊列中的回調函數,先去執行微任務隊列,當微任務隊列清空後,去檢測執行巨集任務隊列中的回調函數,直至所有棧和隊列清空
整體流程如下圖所示:
接下來讓我們看看nextTick的源碼~
vue將nextTick的源碼放在了vue/core/util/next-tick.js中。如下圖所示:
我們把這個文件拆成三個部分來看:
1.nextTick定義函數
我們將nextTick函數單獨拿出來,callbacks是一個回調隊列,其實調用nextTick就是往這個數組裡面傳執行任務,callbacks新增回調函數之後執行timerFunc函數,pending是用來限制同一個事件迴圈內只能執行一次的pending鎖;
const callbacks = [] // 回調隊列
let pending = false //
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
// cb 回調函數會經統一處理壓入 callbacks 數組
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 執行非同步延遲函數 timerFunc
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
// 當 nextTick 沒有傳入函數參數的時候,返回一個 Promise 化的調用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
2.timerFunc函數 做了四個判斷,先後嘗試當前環境是否能夠使用原生的Promise.then、MutationObserver和setImmediate,不斷的降級處理,如果以上三個都不支持,則最後就會直接使用setTimeOut,主要操作就是將flushCallbacks中的函數放入微任務或者巨集任務,等待下一個事件迴圈開始執行;巨集任務耗費的時間是大於微任務的,所以在瀏覽器支持的情況下,優先使用微任務。如果瀏覽器不支持微任務,使用巨集任務;但是,各種巨集任務之間也有效率的不同,需要根據瀏覽器的支持情況,使用不同的巨集任務;
export let isUsingMicroTask = false
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//是否支持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]'
)) {
//是否支持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)) {
timerFunc = () => {
//是否支持setImmediate
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
//上面都不行,直接使用setTimeout
setTimeout(flushCallbacks, 0)
}
}
3.flushCallbacks函數
flushCallbacks函數只有幾行,也很好理解,將pending鎖置為false,同時將callbacks數組複製一份之後再將callbacks置為空,接下來將複製出來的callbacks數組的每個函數依次進行執行,簡單來說它的主要作用就是用來執行callbacks中的回調函數;
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
值得註意的是,$nextTick 並不是一個真正意義上的微任務microtask,而是利用了事件迴圈機制來實現非同步更新。因此,它的執行時機相對於微任務可能會有所延遲,但仍能保證在 DOM 更新後儘快執行回調函數。
總的來說,nextTick就是
1.將傳入的回調函數放入callbacks數組等待執行,定義pending判斷鎖保證一個事件迴圈中只能調用一次timerFunc函數;
2.根據環境判斷使用非同步方式,調用timerFunc函數調用flushCallbacks函數依次執行callbacks中的回調函數;
3.個人小結
nextTick可避免數據更新後導致DOM的數據不一致的問題,提供了更穩定的非同步更新機制,解決了created鉤子函數DOM未渲染會造成的非同步數據渲染問題,但如果過多的使用nextTick會導致事件迴圈中任務數量和回調函數增多,有可能出現可怕的回調地獄,導致性能下降,同時過度依賴nextTick也會降低代碼的可讀性,所以大家還是"按需載入"的好~
作者:京東保險 卓雅倩
來源:京東雲開發者社區 轉載請註明來源