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