找了一個(gè)實(shí)習(xí),去公司做數(shù)據(jù)的可視化,就是用iview-admin,Echarts做一下展示。中間遇到了一個(gè)問題數(shù)據(jù)第一次死活渲染不出來,后來把那段代碼放到this.nextTick(()=>{})里面,就渲染出來了,很神奇,再早之前也是一個(gè)數(shù)據(jù)顯示項(xiàng)目,this.$refs死活選不到動(dòng)態(tài)生成的dom,后來放到this.nextTick(()=>{})里面也行了。所以必須得查找一下nextTick的相關(guān)資料,在此記錄,供自己以后查閱。
官網(wǎng)解釋
先來看看官網(wǎng)如何解釋的
可能你還沒有注意到,Vue 異步執(zhí)行 DOM 更新。只要觀察到數(shù)據(jù)變化,Vue 將開啟一個(gè)隊(duì)列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)改變。如果同一個(gè) watcher 被多次觸發(fā),只會(huì)被推入到隊(duì)列中一次。這種在緩沖時(shí)去除重復(fù)數(shù)據(jù)對于避免不必要的計(jì)算和 DOM 操作上非常重要。然后,在下一個(gè)的事件循環(huán)“tick”中,Vue 刷新隊(duì)列并執(zhí)行實(shí)際 (已去重的) 工作。Vue 在內(nèi)部嘗試對異步隊(duì)列使用原生的 Promise.then 和 MessageChannel,如果執(zhí)行環(huán)境不支持,會(huì)采用 setTimeout(fn, 0) 代替。
例如,當(dāng)你設(shè)置 vm.someData = 'new value' ,該組件不會(huì)立即重新渲染。當(dāng)刷新隊(duì)列時(shí),組件會(huì)在事件循環(huán)隊(duì)列清空時(shí)的下一個(gè)“tick”更新。多數(shù)情況我們不需要關(guān)心這個(gè)過程,但是如果你想在 DOM 狀態(tài)更新后做點(diǎn)什么,這就可能會(huì)有些棘手。雖然 Vue.js 通常鼓勵(lì)開發(fā)人員沿著“數(shù)據(jù)驅(qū)動(dòng)”的方式思考,避免直接接觸 DOM,但是有時(shí)我們確實(shí)要這么做。為了在數(shù)據(jù)變化之后等待 Vue 完成更新 DOM ,可以在數(shù)據(jù)變化之后立即使用 Vue.nextTick(callback) 。這樣回調(diào)函數(shù)在 DOM 更新完成后就會(huì)調(diào)用。
如果同一個(gè) watcher 被多次觸發(fā),只會(huì)被推入到隊(duì)列中一次這句話什么意思呢?跑個(gè)例子就知道了
<div id="example">
{{ msg }}
</div>
<script type="text/javascript">
var vm = new Vue({
el: '#example',
data: {
msg: 1
},
created(){
this.msg = 1
this.msg = 2
this.msg = 3
},
watch: {
msg(){
console.log(this.msg)
}
}
})
</script>
瀏覽器控制臺會(huì)輸出什么呢?答案是3,而不是1、2、3。這就是官網(wǎng)說的多次觸發(fā),只會(huì)被推入隊(duì)列一次。
再看個(gè)例子
<div id="example">
{{ msg }}
</div>
var vm = new Vue({
el: '#example',
data: {
msg: '123'
}
})
vm.msg = 'new message'
console.log(1)
console.log(vm.$el.innerText)
console.log(2)
Vue.nextTick(()=>{
console.log(vm.$el.innerText)
})
console.log(3)
</script>
在谷歌瀏覽器控制臺中的輸出是
1
123
2
3
new message
是不是和想象中不太一樣,為什么最后打出的是'new message'而不是代碼的最后3,為什么第一次打印vm.$el.innerText是123,而不是賦值后的'new message'這個(gè)就要了解一下JavaScript的EventLoop,這個(gè)稍后會(huì)說,再來看一個(gè)例子,這個(gè)例子也是之前我為什么取不到div實(shí)例的簡化版
<div id="example">
<div v-for="i in number" :ref="'div'+i" v-if="number > 0">{{i}}</div>
<button @click="addNumber">點(diǎn)擊</button>
</div>
<script type="text/javascript">
var vm = new Vue({
el: '#example',
data: {
number: 0
},
methods:{
addNumber(){
this.number = 3
console.log(1)
console.log(this.$refs['div1'])
console.log(2)
this.$nextTick(()=>{
console.log(this.$refs['div1'])
})
console.log(3)
}
}
})
</script>
打印結(jié)果為
1
undefined
2
3
[div]
可以看到第一次并沒有取到id為div1的div元素,在nextTick里面就取到了。這個(gè)就對應(yīng)官網(wǎng)里面的為了在數(shù)據(jù)變化之后等待 Vue 完成更新 DOM ,可以在數(shù)據(jù)變化之后立即使用 Vue.nextTick(callback)
Javascript EventLoop
上面那么多奇怪的行為其實(shí)和JavaScript的EventLoop有很大的關(guān)系,下面我就嘗試著解釋一下這個(gè)EventLoop。
有一個(gè)比較有名的視頻是解釋這個(gè)的,Philip Roberts的演講《Help, I'm stuck in an event-loop》,雖然是純英文,但是配合PPT上面的動(dòng)畫還是能看懂的。我把阮一峰老師的解釋直接復(fù)制粘貼過來吧,因?yàn)閷懙拇_實(shí)很好。
為什么JavaScript是單線程?
JavaScript語言的一大特點(diǎn)就是單線程,也就是說,同一個(gè)時(shí)間只能做一件事。那么,為什么JavaScript不能有多個(gè)線程呢?這樣能提高效率啊。
JavaScript的單線程,與它的用途有關(guān)。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動(dòng),以及操作DOM。這決定了它只能是單線程,否則會(huì)帶來很復(fù)雜的同步問題。比如,假定JavaScript同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?
所以,為了避免復(fù)雜性,從一誕生,JavaScript就是單線程,這已經(jīng)成了這門語言的核心特征,將來也不會(huì)改變。
為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒有改變JavaScript單線程的本質(zhì)。
任務(wù)隊(duì)列
單線程就意味著,所有任務(wù)需要排隊(duì),前一個(gè)任務(wù)結(jié)束,才會(huì)執(zhí)行后一個(gè)任務(wù)。如果前一個(gè)任務(wù)耗時(shí)很長,后一個(gè)任務(wù)就不得不一直等著。
如果排隊(duì)是因?yàn)橛?jì)算量大,CPU忙不過來,倒也算了,但是很多時(shí)候CPU是閑著的,因?yàn)镮O設(shè)備(輸入輸出設(shè)備)很慢(比如Ajax操作從網(wǎng)絡(luò)讀取數(shù)據(jù)),不得不等著結(jié)果出來,再往下執(zhí)行。
JavaScript語言的設(shè)計(jì)者意識到,這時(shí)主線程完全可以不管IO設(shè)備,掛起處于等待中的任務(wù),先運(yùn)行排在后面的任務(wù)。等到IO設(shè)備返回了結(jié)果,再回過頭,把掛起的任務(wù)繼續(xù)執(zhí)行下去。
于是,所有任務(wù)可以分成兩種,一種是同步任務(wù)(synchronous),另一種是異步任務(wù)(asynchronous)。同步任務(wù)指的是,在主線程上排隊(duì)執(zhí)行的任務(wù),只有前一個(gè)任務(wù)執(zhí)行完畢,才能執(zhí)行后一個(gè)任務(wù);異步任務(wù)指的是,不進(jìn)入主線程、而進(jìn)入"任務(wù)隊(duì)列"(task queue)的任務(wù),只有"任務(wù)隊(duì)列"通知主線程,某個(gè)異步任務(wù)可以執(zhí)行了,該任務(wù)才會(huì)進(jìn)入主線程執(zhí)行。
具體來說,異步執(zhí)行的運(yùn)行機(jī)制如下。(同步執(zhí)行也是如此,因?yàn)樗梢员灰暈闆]有異步任務(wù)的異步執(zhí)行。)
(1)所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧(execution context stack)。
(2)主線程之外,還存在一個(gè)"任務(wù)隊(duì)列"(task queue)。只要異步任務(wù)有了運(yùn)行結(jié)果,就在"任務(wù)隊(duì)列"之中放置一個(gè)事件。
(3)一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取"任務(wù)隊(duì)列",看看里面有哪些事件。那些對應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進(jìn)入執(zhí)行棧,開始執(zhí)行。
(4)主線程不斷重復(fù)上面的第三步。

主線程從"任務(wù)隊(duì)列"中讀取事件,這個(gè)過程是循環(huán)不斷的,所以整個(gè)的這種運(yùn)行機(jī)制又稱為Event Loop(事件循環(huán))。

主線程在運(yùn)行的時(shí)候會(huì)產(chǎn)生堆棧,堆就是存儲變量,棧記錄執(zhí)行的順序,如果碰到回調(diào)函數(shù)、DOM操作比如點(diǎn)擊、鼠標(biāo)移上去等、setTimeout操作會(huì)放到任務(wù)隊(duì)列,只有棧中的代碼執(zhí)行完畢才會(huì)從任務(wù)隊(duì)列取出代碼,進(jìn)行執(zhí)行。所以這也是為什么上面代碼例子中雖然console.log(3)在代碼最后,但是比nextTick里面的代碼先輸出。
你可以這么理解,一堆代碼,該放到stack里面的方法,放到stack里面,然后這堆代碼里面的異步操作放到任務(wù)隊(duì)列里面,然后執(zhí)行棧里面的代碼,棧里面的代碼執(zhí)行完畢,執(zhí)行任務(wù)隊(duì)列里面的代碼,所以代碼的執(zhí)行順序和寫的順序并不是一直的
任務(wù)隊(duì)列
上面的任務(wù)隊(duì)列分為兩種,執(zhí)行順序也是有一點(diǎn)差別的,Macrotasks 和 Microtasks
- Macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
- Microtasks: process.nextTick, Promises, Object.observe(廢棄), MutationObserver
Macrotasks 和 Microtasks有什么區(qū)別呢?我們以setTimeout和Promises來舉例。
console.log('1');
setTimeout(function() {
console.log('2');
}, 0);
Promise.resolve().then(function() {
console.log('3');
}).then(function() {
console.log('4');
});
console.log('5');
//輸出結(jié)果:
//1
//5
//3
//4
//2
原因是Promise中的then方法的函數(shù)會(huì)被推入 microtasks 隊(duì)列,而setTimeout的任務(wù)會(huì)被推入 macrotasks 隊(duì)列。在每一次事件循環(huán)中,macrotask 只會(huì)提取一個(gè)執(zhí)行,而 microtask 會(huì)一直提取,直到 microtasks 隊(duì)列清空。結(jié)論如下:
- microtask會(huì)優(yōu)先macrotask執(zhí)行
- microtasks會(huì)被循環(huán)提取到執(zhí)行引擎主線程的執(zhí)行棧,直到microtasks任務(wù)隊(duì)列清空,才會(huì)執(zhí)行macrotask
【注:一般情況下,macrotask queues 我們會(huì)直接稱為 task queues,只有 microtask queues 才會(huì)特別指明?!?/p>
解釋vue nextTick
查看一下vue nextTick的代碼實(shí)現(xiàn)
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false
// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
從上面這一段代碼知道,vue nextTick默認(rèn)使用microTask,然后生成兩個(gè)函數(shù),首先是macroTimerFunc,順序是setImmediate->MessageChannnel->setTimeout,microTimerFunc生成順序是 Promise
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
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
nextTick函數(shù)其實(shí)做了兩件事情,一是生成兩個(gè)timerFunc,把回調(diào)作為microTask或macroTask參與到事件循環(huán)中來。二是把回調(diào)函數(shù)放入一個(gè)callbacks隊(duì)列,等待適當(dāng)?shù)臅r(shí)機(jī)執(zhí)行。(這個(gè)時(shí)機(jī)和timerFunc不同的實(shí)現(xiàn)有關(guān)),說白了就是改變代碼的執(zhí)行順序,在dom節(jié)點(diǎn)更新完畢再去執(zhí)行,因?yàn)橛行┎僮餍枰猟om節(jié)點(diǎn)更新完畢才行,所以不能立刻執(zhí)行,需要放到nextTick里面去執(zhí)行,下面圖片可以參考一下
原始代碼
A();
B();
C();
執(zhí)行順序
正常順序
原始代碼(這個(gè)nextTick可不是vue里面的nextTick,而是原生的函數(shù),不過可以借鑒一下,有助于理解vue里面的nextTick執(zhí)行順序)
A();
process.nextTick(B);
C();
nextTick
原始代碼
A();
setImmediate(B);//或者setTimeout(B,0);
C();
setImmediate|setTimeout
應(yīng)用場景
在操作DOM節(jié)點(diǎn)無效的時(shí)候,就要考慮操作的實(shí)際DOM節(jié)點(diǎn)是否存在,或者相應(yīng)的DOM是否被更新完畢。
比如說,在created鉤子中涉及DOM節(jié)點(diǎn)的操作肯定是無效的,因?yàn)榇藭r(shí)還沒有完成相關(guān)DOM的掛載。解決的方法就是在nextTick函數(shù)中去處理DOM,這樣才能保證DOM被成功掛載而有效操作。
還有就是在數(shù)據(jù)變化之后要執(zhí)行某個(gè)操作,而這個(gè)操作需要使用隨數(shù)據(jù)改變而改變的DOM時(shí),這個(gè)操作應(yīng)該放進(jìn)Vue.nextTick。
參考資料