Vue.nextTick純干貨使用方法詳解

用過(guò)Vue的朋友多多少少都知道$nextTick~ 在正式講解nextTick之前,我想你應(yīng)該清楚知道 Vue 在更新 DOM 時(shí)是異步執(zhí)行的,因?yàn)榻酉聛?lái)講解過(guò)程會(huì)結(jié)合組件更新一起講~ 事不宜遲,我們直進(jìn)主題吧(本文以v2.6.14版本的Vue源碼進(jìn)行講解)
一、nextTick小測(cè)試
你真的了解nextTick嗎?來(lái),直接上題~
<template>
<div id="app">
<p ref="name">{{ name }}</p>
<button @click="handleClick">修改name</button>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
name: '井柏然'
}
},
mounted() {
console.log('mounted', this.$refs.name.innerText)
},
methods: {
handleClick () {
this.$nextTick(() => console.log('nextTick1', this.$refs.name.innerText))
this.name = 'jngboran'
console.log('sync log', this.$refs.name.innerText)
this.$nextTick(() => console.log('nextTick2', this.$refs.name.innerText))
}
}
}
</script>
請(qǐng)問(wèn)上述代碼中,當(dāng)點(diǎn)擊按鈕“修改name”時(shí),'nextTick1','sync log','nextTick2'對(duì)應(yīng)的this.$refs.name.innerText分別會(huì)輸出什么?注意,這里打印的是DOM的innerText~(文章結(jié)尾處會(huì)貼出答案)
如果此時(shí)的你有非常堅(jiān)定的答案,那你可以不用繼續(xù)往下看了~但如果你對(duì)自己的答案有所顧慮,那不如跟著我,接著往下看。相信你看完,不需要看到答案都能有個(gè)肯定的答案了~!
二、nextTick源碼實(shí)現(xiàn)
源碼位于core/util/next-tick中??梢詫⑵浞譃?個(gè)部分來(lái)看,直接上代碼
1. 全局變量
callbacks隊(duì)列、pending狀態(tài)
const callbacks = [] // 存放cb的隊(duì)列 let pending = false // 是否馬上遍歷隊(duì)列,執(zhí)行cb的標(biāo)志
2. flushCallbacks
遍歷callbacks執(zhí)行每個(gè)cb
function flushCallbacks () {
pending = false // 注意這里,一旦執(zhí)行,pending馬上被重置為false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]() // 執(zhí)行每個(gè)cb
}
}
3. nextTick的異步實(shí)現(xiàn)
根據(jù)執(zhí)行環(huán)境的支持程度采用不同的異步實(shí)現(xiàn)策略
let timerFunc // nextTick異步實(shí)現(xiàn)fn
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// Promise方案
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks) // 將flushCallbacks包裝進(jìn)Promise.then中
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// MutationObserver方案
let counter = 1
const observer = new MutationObserver(flushCallbacks) // 將flushCallbacks作為觀測(cè)變化的cb
const textNode = document.createTextNode(String(counter)) // 創(chuàng)建文本節(jié)點(diǎn)
// 觀測(cè)文本節(jié)點(diǎn)變化
observer.observe(textNode, {
characterData: true
})
// timerFunc改變文本節(jié)點(diǎn)的data,以觸發(fā)觀測(cè)的回調(diào)flushCallbacks
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// setImmediate方案
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 最終降級(jí)方案setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
這里用個(gè)真實(shí)案例加深對(duì)MutationObserver的理解。畢竟比起其他三種異步方案,這個(gè)應(yīng)該是大家最陌生的
const observer = new MutationObserver(() => console.log('觀測(cè)到文本節(jié)點(diǎn)變化'))
const textNode = document.createTextNode(String(1))
observer.observe(textNode, {
characterData: true
})
console.log('script start')
setTimeout(() => console.log('timeout1'))
textNode.data = String(2) // 這里對(duì)文本節(jié)點(diǎn)進(jìn)行值的修改
console.log('script end')
知道對(duì)應(yīng)的輸出會(huì)是怎么樣的嗎?
script start、script end會(huì)在第一輪宏任務(wù)中執(zhí)行,這點(diǎn)沒(méi)問(wèn)題
setTimeout會(huì)被放入下一輪宏任務(wù)執(zhí)行
MutationObserver是微任務(wù),所以會(huì)在本輪宏任務(wù)后執(zhí)行,所以先于setTimeout
結(jié)果如下圖:

4. nextTick方法實(shí)現(xiàn)
cb、Promise方式
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 往全局的callbacks隊(duì)列中添加cb
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
// 這里是支持Promise的寫(xiě)法
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 執(zhí)行timerFunc,在下一個(gè)Tick中執(zhí)行callbacks中的所有cb
timerFunc()
}
// 對(duì)Promise的實(shí)現(xiàn),這也是我們使用時(shí)可以寫(xiě)成nextTick.then的原因
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
- 深入細(xì)節(jié),理解
pending有什么用,如何運(yùn)作?
- 案例1,同一輪Tick中執(zhí)行2次
$nextTick,timerFunc只會(huì)被執(zhí)行一次
this.$nextTick(() => console.log('nextTick1'))
this.$nextTick(() => console.log('nextTick2'))
- 用圖看看更直觀?

三、Vue組件的異步更新
這里如果有對(duì)Vue組件化、派發(fā)更新不是十分了解的朋友,可以先戳這里,看圖解Vue響應(yīng)式原理了解下Vue組件化和派發(fā)更新的相關(guān)內(nèi)容再回來(lái)看噢~
Vue的異步更新DOM其實(shí)也是使用nextTick來(lái)實(shí)現(xiàn)的,跟我們平時(shí)使用的$nextTick其實(shí)是同一個(gè)~
這里我們回顧一下,當(dāng)我們改變一個(gè)屬性值的時(shí)候會(huì)發(fā)生什么?

根據(jù)上圖派發(fā)更新過(guò)程,我們從watcher.update開(kāi)時(shí)講起,以渲染W(wǎng)atcher為例,進(jìn)入到queueWatcher里
1. queueWatcher做了什么?
// 用來(lái)存放Wathcer的隊(duì)列。注意,不要跟nextTick的callbacks搞混了,都是隊(duì)列,但用處不同~
const queue: Array<Watcher> = []
function queueWatcher (watcher: Watcher) {
const id = watcher.id // 拿到Wathcer的id,這個(gè)id每個(gè)watcher都有且全局唯一
if (has[id] == null) {
// 避免添加重復(fù)wathcer,這也是異步渲染的優(yōu)化做法
has[id] = true
if (!flushing) {
queue.push(watcher)
}
if (!waiting) {
waiting = true
// 這里把flushSchedulerQueue推進(jìn)nextTick的callbacks隊(duì)列中
nextTick(flushSchedulerQueue)
}
}
}
2. flushSchedulerQueue做了什么?
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// 排序保證先父后子執(zhí)行更新,保證userWatcher在渲染W(wǎng)atcher前
queue.sort((a, b) => a.id - b.id)
// 遍歷所有的需要派發(fā)更新的Watcher執(zhí)行更新
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
// 真正執(zhí)行派發(fā)更新,render -> update -> patch
watcher.run()
}
}
- 最后,一張圖搞懂組件的異步更新過(guò)程

四、回歸題目本身
相信經(jīng)過(guò)上文對(duì)nextTick源碼的剖析,我們已經(jīng)揭開(kāi)它神秘的面紗了。這時(shí)的你一定可以堅(jiān)定地把答案說(shuō)出來(lái)了~話不多說(shuō),我們一起核實(shí)下,看看是不是如你所想!
如圖所示,mounted時(shí)候的innerText是“井柏然”的中文

接下來(lái)是點(diǎn)擊按鈕后,打印結(jié)果如圖所示

沒(méi)錯(cuò),輸出結(jié)果如下(意不意外?驚不驚喜?)
sync log 井柏然
nextTick1 井柏然
nextTick2 jngboran
下面簡(jiǎn)單分析一下每個(gè)輸出:
this.$nextTick(() => console.log('nextTick1', this.$refs.name.innerText))
this.name = 'jngboran'
console.log('sync log', this.$refs.name.innerText)
this.$nextTick(() => console.log('nextTick2', this.$refs.name.innerText))
sync log:這個(gè)同步打印沒(méi)什么好說(shuō)了,相信大部分童鞋的疑問(wèn)點(diǎn)都不在這里。如果不清楚的童鞋可以先回顧一下EventLoop,這里不多贅述了~
nextTick1:注意其雖然是放在$nextTick的回調(diào)中,在下一個(gè)tick執(zhí)行,但是他的位置是在this.name = 'jngboran'的前。也就是說(shuō),他的cb會(huì)比App組件的派發(fā)更新(flushSchedulerQueue)更先進(jìn)入隊(duì)列,當(dāng)nextTick1打印時(shí),App組件還未派發(fā)更新,所以拿到的還是舊的DOM值。
nextTick2就不展開(kāi)了,大家可以自行分析一下。相信大家對(duì)它應(yīng)該是最肯定的,我們平時(shí)不就是這樣拿到更新后的DOM嗎?
最后來(lái)一張圖加深理解

寫(xiě)在最后,nextTick其實(shí)在Vue中也算是比較核心的一個(gè)東西了。因?yàn)樨灤┱麄€(gè)Vue應(yīng)用的組件化、響應(yīng)式的派發(fā)更新與其息息相關(guān)~深入理解nextTick的背后實(shí)現(xiàn)原理,不僅能讓你在面試的時(shí)候一展風(fēng)采,更能讓你在日常開(kāi)發(fā)工作中,少走彎路少踩坑!
以上就是Vue.nextTick純干貨使用方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue.nextTick使用方法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
實(shí)現(xiàn)vuex與組件data之間的數(shù)據(jù)同步更新方式
今天小編就為大家分享一篇實(shí)現(xiàn)vuex與組件data之間的數(shù)據(jù)同步更新方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11
Element+Vue實(shí)現(xiàn)動(dòng)態(tài)表單多個(gè)下拉框組件功能
這篇文章主要介紹了Element+Vue實(shí)現(xiàn)動(dòng)態(tài)表單多個(gè)下拉框組件功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07
在vue2.0中引用element-ui組件庫(kù)的方法
這篇文章主要介紹了在vue2.0中引用element-ui組件庫(kù),需要的朋友可以參考下2018-06-06
vue實(shí)現(xiàn)折疊展開(kāi)收縮動(dòng)畫(huà)效果
這篇文章主要介紹了vue實(shí)現(xiàn)折疊展開(kāi)收縮動(dòng)畫(huà),通過(guò)scrollHeight實(shí)現(xiàn),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2023-11-11
Vue實(shí)現(xiàn)無(wú)限級(jí)樹(shù)形選擇器
這篇文章主要介紹了Vue實(shí)現(xiàn)無(wú)限級(jí)樹(shù)形選擇器,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-09-09
基于vue.js仿淘寶收貨地址并設(shè)置默認(rèn)地址的案例分析
這篇文章主要介紹了基于vue.js仿淘寶收貨地址并設(shè)置默認(rèn)地址的案例分析,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08
vue3.0+element Plus實(shí)現(xiàn)頁(yè)面布局側(cè)邊欄菜單路由跳轉(zhuǎn)功能
這篇文章主要介紹了vue3.0+element Plus實(shí)現(xiàn)頁(yè)面布局,側(cè)邊欄菜單路由跳轉(zhuǎn),需要的朋友可以參考下2023-07-07
關(guān)于vite proxy跨域問(wèn)題的解決
這篇文章主要介紹了關(guān)于vite proxy跨域問(wèn)題的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
詳解使用webpack打包編寫(xiě)一個(gè)vue-toast插件
本篇文章主要介紹了詳解使用webpack打包編寫(xiě)一個(gè)vue插件,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11

