Vue nextTick延遲回調(diào)獲取更新后DOM機(jī)制詳解
簡(jiǎn)述
相信大家在寫vue項(xiàng)目的時(shí)候,一定會(huì)發(fā)現(xiàn)一個(gè)神奇的api,Vue.nextTick。為什么說它神奇呢,那是因?yàn)樵谀阕瞿承┎僮鞑簧r(shí),將操作寫在Vue.nextTick內(nèi),就神奇的生效了。那這是什么原因呢?
讓我們一起來研究一下。
- vue 實(shí)現(xiàn)響應(yīng)式并不是數(shù)據(jù)發(fā)生變化后 DOM 立即變化,而是按照一定策略異步執(zhí)行 DOM 更新的
- vue 在修改數(shù)據(jù)后,視圖不會(huì)立刻進(jìn)行更新,而是要等同一事件循環(huán)機(jī)制內(nèi)所有數(shù)據(jù)變化完成后,再統(tǒng)一進(jìn)行DOM更新
nextTick可以讓我們?cè)谙麓?DOM 更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào),用于獲得更新后的 DOM。
事件循環(huán)機(jī)制
在討論Vue.nextTick之前,需要先搞清楚事件循環(huán)機(jī)制,算是實(shí)現(xiàn)的基石了,那我們來看一下。
在瀏覽器環(huán)境中,我們可以將我們的執(zhí)行任務(wù)分為宏任務(wù)和微任務(wù),
- 宏任務(wù): 包括整體代碼script,
setTimeout,setInterval、setImmediate、 I/O 操作、UI 渲染 - 微任務(wù):
Promise.then、MuationObserver

事件循環(huán)的順序,決定js代碼的執(zhí)行順序。事件循環(huán)如下:

用代碼解釋,瀏覽器中事件循環(huán)的順序同如下代碼:
for (macroTask of macroTaskQueue) {
// 1. 執(zhí)行一個(gè)宏任務(wù)
handleMacroTask();
// 2. 執(zhí)行所有的微任務(wù)
for (microTask of microTaskQueue) {
handleMicroTask(microTask);
}
}
vue數(shù)據(jù)驅(qū)動(dòng)視圖的處理(異步變化DOM)
<template>
<div>
<div>{{count}}</div>
<div @click="handleClick">click</div>
</div>
</template>
export default {
data () {
return {
number: 0
};
},
methods: {
handleClick () {
for(let i = 0; i < 10000; i++) {
this.count++;
}
}
}
}
分析上述代碼:
- 當(dāng)點(diǎn)擊按鈕時(shí),count會(huì)被循環(huán)改變10000次。那么每次count+1,都會(huì)觸發(fā)count的
setter方法,然后修改真實(shí)DOM。按此邏輯,這整個(gè)過程,DOM會(huì)被更新10000次,我們都知道DOM的操作是非常昂貴的,而且這樣的操作完全沒有必要。所以vue內(nèi)部在派發(fā)更新時(shí)做了優(yōu)化 - 也就是,并不會(huì)每次數(shù)據(jù)改變都觸發(fā) watcher 的回調(diào),而是把這些 watcher 先添加到一個(gè)隊(duì)列queueWatcher里,然后在 nextTick 后執(zhí)行 flushSchedulerQueue處理
- 當(dāng) count 增加 10000 次時(shí),vue內(nèi)部會(huì)先將對(duì)應(yīng)的 Watcher 對(duì)象給 push 進(jìn)一個(gè)隊(duì)列 queue 中去,等下一個(gè) tick 的時(shí)候再去執(zhí)行。并不需要在下一個(gè) tick 的時(shí)候執(zhí)行 10000 個(gè)同樣的 Watcher 對(duì)象去修改界面,而是只需要執(zhí)行一個(gè) Watcher 對(duì)象,使其將界面上的 0 變成 10000 即可
Vue.nextTick原理
由上一節(jié)我們知道,Vue中 數(shù)據(jù)變化 => DOM變化 是異步過程,一旦觀察到數(shù)據(jù)變化,Vue就會(huì)開啟一個(gè)任務(wù)隊(duì)列,然后把在同一個(gè)事件循環(huán) (Event loop) 中觀察到數(shù)據(jù)變化的 Watcher(Vue源碼中的Wacher類是用來更新Dep類收集到的依賴的)推送進(jìn)這個(gè)隊(duì)列。
如果這個(gè)watcher被觸發(fā)多次,只會(huì)被推送到隊(duì)列一次。這種緩沖行為可以有效的去掉重復(fù)數(shù)據(jù)造成的不必要的計(jì)算和DOM操作。而在下一個(gè)事件循環(huán)時(shí),Vue會(huì)清空隊(duì)列,并進(jìn)行必要的DOM更新。
nextTick的作用是為了在數(shù)據(jù)變化之后等待 Vue 完成更新 DOM ,可以在數(shù)據(jù)變化之后立即使用 Vue.nextTick(callback),JS是單線程的,擁有事件循環(huán)機(jī)制,nextTick的實(shí)現(xiàn)就是利用了事件循環(huán)的宏任務(wù)和微任務(wù)。
vue中next-tick.js的源碼如下
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
// 首先定義一個(gè) callbacks 數(shù)組用來存儲(chǔ) nextTick,在下一個(gè) tick 處理這些回調(diào)函數(shù)之前,
// 所有的 cb 都會(huì)被存在這個(gè) callbacks 數(shù)組中
const callbacks = []
// pending 是一個(gè)標(biāo)記位,代表一個(gè)等待的狀態(tài)
let pending = false
// 最后執(zhí)行 flushCallbacks() 方法,遍歷callbacks數(shù)組,依次執(zhí)行里邊的每個(gè)函數(shù)
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
/*
判斷采用哪種異步回調(diào)方式
由于微任務(wù)優(yōu)先級(jí)高,首先嘗試微任務(wù)模擬
1.首先嘗試使用Promise.then(微任務(wù))
2.嘗試使用MuationObserver(微任務(wù))回調(diào)
3.嘗試使用 setImmediate(宏任務(wù))回調(diào)
4.最后嘗試使用setTimeout(宏任務(wù))回調(diào)
*/
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
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(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
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
})
}
}
目前瀏覽器平臺(tái)并沒有實(shí)現(xiàn) nextTick 方法,所以 Vue.js 源碼中分別用 Promise、setTimeout、setImmediate 等方式在 microtask(或是task)中創(chuàng)建一個(gè)事件,目的是在當(dāng)前調(diào)用棧執(zhí)行完畢以后(不一定立即)才會(huì)去執(zhí)行這個(gè)事件。
nextTick的調(diào)用方式
回調(diào)函數(shù)方式:Vue.nextTick(callback)
Promise方式:Vue.nextTick().then(callback)
實(shí)例方式:vm.$nextTick(callback)
Vue.nextTick的應(yīng)用
created生命周期中操作DOM
created鉤子函數(shù)執(zhí)行的時(shí)候DOM 其實(shí)并未進(jìn)行掛載和渲染,此時(shí)就是無法操作DOM的,我們將操作DOM的代碼中放到nextTick中,等待下一輪事件循環(huán)開始,DOM就已經(jīng)進(jìn)行掛載好了,而與這個(gè)操作對(duì)應(yīng)的就是mounted鉤子函數(shù),因?yàn)樵趍ounted執(zhí)行的時(shí)候所有的DOM掛載已完成。
created(){
vm.$nextTick(() => {
//不使用this.$nextTick()方法操作DOM會(huì)報(bào)錯(cuò)
this.$refs.test.innerHTML="created中操作了DOM"
});
}
修改數(shù)據(jù),獲取DOM值
當(dāng)我們修改了data里的數(shù)據(jù)時(shí),并不能立刻通過操作DOM去獲取到里面的值
<template>
<div class="test">
<p ref='msg' id="msg">{{msg}}</p>
</div>
</template>
<script>
export default {
name: 'Test',
data () {
return {
msg:"hello world",
}
},
methods: {
changeMsg() {
this.msg = "hello Vue" // vue數(shù)據(jù)改變,改變了DOM里的innerText
let msgEle = this.$refs.msg.innerText //后續(xù)js對(duì)dom的操作
console.log(msgEle) // hello world
// 輸出可以看到data里的數(shù)據(jù)修改后DOM并沒有立即更新,后續(xù)的DOM不是最新的
this.$nextTick(() => {
console.log(this.$refs.msg.innerText) // hello Vue
})
this.$nextTick().then(() => {
console.log(this.$refs.msg.innerText) // hello Vue
})
},
changeMsg2() {
this.$nextTick(() => {
console.log(this.$refs.msg.innerText) // 1.hello world
})
this.msg = "hello Vue" // 2.
console.log(this.$refs.msg.innerText) // hello world
this.$nextTick().then(() => {
console.log(this.$refs.msg.innerText) // hello Vue
})
// nextTick中先添加的先執(zhí)行,執(zhí)行1后,才會(huì)執(zhí)行2(Vue操作Dom的異步)
}
}
}
</script>
v-show/v-if由隱藏變?yōu)轱@示
點(diǎn)擊按鈕顯示原本以 v-show=false或v-if 隱藏起來的輸入框,并獲取焦點(diǎn)或者獲得寬高等的場(chǎng)景
以上就是Vue nextTick延遲回調(diào)獲取更新后DOM機(jī)制詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue nextTick延遲回調(diào)DOM獲取的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue proxyTable 接口跨域請(qǐng)求調(diào)試的示例
本篇文章主要介紹了vue proxyTable 接口跨域請(qǐng)求調(diào)試的示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09
基于Vue.js的文件選擇與多選對(duì)話框組件Dccfile的使用教程
在現(xiàn)代前端開發(fā)中,Vue.js 提供了強(qiáng)大的組件化開發(fā)能力,使得我們可以輕松構(gòu)建復(fù)雜且可復(fù)用的用戶界面,本文將介紹一個(gè)基于 Vue.js 的文件選擇與多選對(duì)話框組件——Dccfile,并詳細(xì)解析其功能和實(shí)現(xiàn)細(xì)節(jié)2025-04-04
基于Vue-Cli 打包自動(dòng)生成/抽離相關(guān)配置文件的實(shí)現(xiàn)方法
基于Vue-cli 項(xiàng)目產(chǎn)品部署,涉及到的交互的地址等配置信息,每次都要重新打包才能生效,極大的降低了效率。這篇文章主要介紹了基于Vue-Cli 打包自動(dòng)生成/抽離相關(guān)配置文件 ,需要的朋友可以參考下2018-12-12
vue input標(biāo)簽通用指令校驗(yàn)的實(shí)現(xiàn)
這篇文章主要介紹了vue input標(biāo)簽通用指令校驗(yàn)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11

