公司做之前項目的時候,遇到了一些比較困惑的問題,后來研究明白了nextTick的用法。
我們先看兩種情況:
第一種:
export default { data () { return { msg: 0 } }, mounted () { this.msg = 1 this.msg = 2 this.msg = 3 }, watch: { msg () { console.log(this.msg) } } }
這段腳本執行我們猜測會依次打印:1、2、3。但是實際效果中,只會輸出一次:3。為什么會出現這樣的情況?
原因:
當觸發update
更新的時候,會去執行queueWatcher
方法,也就是說,下一個循環開始時調用,此時msg已經變成3了。
保證更新視圖操作DOM的動作是在當前棧執行完以后下一個Tick(或者是當前Tick的微任務階段)的時候調用,大大優化了性能。
第二種情況:
<body> <div id="main"> <ul class="list"> <li class="item" v-for="item in list">{{ item }}</li> </ul> </div> <script> new Vue({ el: '#main', data: { list: [ 'AAAAAAAAAA', 'BBBBBBBBBB', 'CCCCCCCCCC' ] }, mounted: function () { this.list.push('DDDDD') } }) </script> </body>
隨便給了點樣式之后,頁面是這樣的:
看起來似乎一切正常,我們在給數組添加了一條數據之后,頁面也確實對應的更新了。可是,當我們在打印這個 ul 元素里 li 的 length 時,問題出現了:
mounted: function () { this.list.push('DDDDD') console.log(this.$el.querySelectorAll('.item').length) // 3 }
這時候如果我們有需求需要通過 li 的個數來計算出 ul 容器的高度來進行布局,顯然就有問題了。
而這時候 Vue 的 nextTick 就可以幫助我們解決這個問題:
mounted: function () { this.list.push('DDDDD') Vue.nextTick(function() { console.log(this.$el.querySelectorAll('.item').length) // 4 // ... 計算 })
當你設置 vm.someData = 'new value' ,該組件不會立即重新渲染。當刷新隊列時,組件會在事件循環隊列清空時的下一個“tick”更新。多數情況我們不需要關心這個過程,但是如果你想在 DOM 狀態更新后做點什么,這就可能會有些棘手。雖然 Vue.js 通常鼓勵開發人員沿着“數據驅動”的方式思考,避免直接接觸 DOM,但是有時我們確實要這么做。為了在數據變化之后等待 Vue 完成更新 DOM ,可以在數據變化之后立即使用 Vue.nextTick(callback) 。這樣回調函數在 DOM 更新完成后就會調用。
js 是單線程語言
我們都知道,js 執行的所有任務都需要排隊,一個任務必須要等它前面的一個任務執行完之后才能執行。如果前一個任務需要花費大量的時間來計算,那么后一個任務就必須一直等它執行完才會輪到它執行,這就是單線程的特性。 而 js 的任務分為兩種,同步任務和異步任務:
- 同步任務就是按照順序一個一個的執行任務,后一個任務要執行必須等它前一個任務完成
- 異步任務(比如回調)不會占用主線程,會被塞到一個任務隊列,等主線程的任務執行完畢,就會把這個異步任務隊列里的任務放回主線程依次執行
用一個丑但易懂的圖來表示:
所以結果輸出是這樣就很好理解了:
Event Loop(事件循環)
被稱作事件循環的原因在於,同步的任務可能會生成新的任務,因此它一直在不停的查找新的事件並執行。一次循環的執行稱之為 tick,在這個循環里執行的代碼被稱作 task,而整個過程是不斷重復的。
console.log(1); setTimeout(()=>{ console.log(2); },1000); while (true){}
上面代碼在輸出 1 之后(謹慎使用!我的瀏覽器就被卡死了~),定時器被塞到任務隊列里,然后主線程繼續往下執行,碰到一個死循環,導致任務隊列里的任務永遠不會被執行,因此不會輸出 2
事件隊列
除了我們的主線程之外,任務隊列分為 microtask 和 macrotask,通常我們會稱之為微任務和宏任務。 microtask 這一名詞在js中是個比較新的概念,我們通常是在學習 ES6 的 Promise 時才初次接觸到。
- 執行優先級上,主線程任務 > microtask > macrotask。
- 典型的 macrotask 有 setTimeout 和 setInterval,以及只有 IE 支持的 setImmediate,還有 MessageChannel等,ES6的 Promise 則是屬於 microtask
console.log(1) setTimeout(function(){ console.log(2) }) Promise.resolve().then(function(){ console.log('promise1') }).then(function(){ console.log('promise2') }) console.log(4)
根據執行順序,上面代碼的輸出結果很容易就能得出了:
nextTick
讓我們回到上面的主題,Vue 的 nextTick方法,
從 源碼 不難發現,Vue 在內部嘗試對異步隊列使用原生的setImmediate
和MessageChannel和
,Promise.then
如果當前執行環境不支持,就采用setTimeout(fn, 0)
代替。
什么時候需要用Vue.nextTick()
:
- 你在
Vue
生命周期的created()鈎子函數進行的DOM
操作一定要放在Vue.nextTick()
的回調函數中。原因是什么呢,原因是在created()
鈎子函數執行的時候DOM
其實並未進行任何渲染,而此時進行DOM
操作無異於徒勞,所以此處一定要將DOM
操作的js
代碼放進Vue.nextTick()
的回調函數中。與之對應的就是mounted鈎子函數,因為該鈎子函數執行時所有的DOM
掛載和渲染都已完成,此時在該鈎子函數中進行任何DOM
操作都不會有問題 。 - 在數據變化后要執行的某個操作,當你設置
vm.someData = 'new value'
,DOM
並不會馬上更新,而是在異步隊列被清除,也就是下一個事件循環開始時執行更新時才會進行必要的DOM
更新。如果此時你想要根據更新的DOM
狀態去做某些事情,就會出現問題。。為了在數據變化之后等待Vue
完成更新DOM
,可以在數據變化之后立即使用Vue.nextTick(callback)
。這樣回調函數在DOM
更新完成后就會調用。 mounted
不會承諾所有的子組件也都一起被掛載。如果你希望等到整個視圖都渲染完畢,可以用vm.$nextTick
替換掉mounted
:
mounted: function () { this.$nextTick(function () { // Code that will run only after the // entire view has been rendered }) }