熟悉 Vue 的同學們都知道,Vue 有個 nextTick 方法,用來非同步更新資料。
來看看這個栗子:
<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
// ... 計算
})
複製程式碼
關於 Vue 的非同步更新佇列,官網是這麼說的:
當你設定 vm.someData = 'new value' ,該元件不會立即重新渲染。當重新整理佇列時,元件會在事件迴圈佇列清空時的下一個“tick”更新。多數情況我們不需要關心這個過程,但是如果你想在 DOM 狀態更新後做點什麼,這就可能會有些棘手。雖然 Vue.js 通常鼓勵開發人員沿著“資料驅動”的方式思考,避免直接接觸 DOM,但是有時我們確實要這麼做。為了在資料變化之後等待 Vue 完成更新 DOM ,可以在資料變化之後立即使用 Vue.nextTick(callback) 。這樣回撥函式在 DOM 更新完成後就會呼叫。
簡單說,因為 DOM 至少會在當前執行緒裡面的程式碼全部執行完畢再更新。所以不可能做到在修改資料後並且 DOM 更新後再執行,要保證在 DOM 更新以後再執行某一塊程式碼,就必須把這塊程式碼放到下一次事件迴圈裡面,比如 setTimeout(fn, 0),這樣 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
Promise.then
和MessageChannel
,如果當前執行環境不支援,就採用setTimeout(fn, 0)
代替。
Nodejs
node原生就支援 process.nextTick(fn)
和setImmediate(fn)
方法,並且process.nextTick(fn)
會被當做microtask
順序執行。