對于 Vue.nextTick 方法,自己有些疑惑。在查詢了各種資料後,總結了一下其原理和用途,如有錯誤,請不吝賜教。
概覽
官方文檔說明:
- 用法:
在下次 DOM 更新循環結束之後執行延遲回調。在修改資料之後立即使用這個方法,擷取更新後的 DOM。
疑問:
- DOM 更新循環是指什麼?
- 下次更新循環是什麼時候?
- 修改資料之後使用,是加快了資料更新進度嗎?
- 在什麼情況下要用到?
原理
異步說明
Vue 實作響應式并不是資料發生變化之後 DOM 立即變化,而是按一定的政策進行 DOM 的更新。
在 Vue 的文檔中,說明 Vue 是異步執行 DOM 更新的。關于異步的解析,可以檢視阮一峰老師的這篇文章。截取關鍵部分如下:
具體來說,異步執行的運作機制如下。
(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
(2)主線程之外,還存在一個"任務隊列"(task queue)。隻要異步任務有了運作結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看裡面有哪些事件。那些對應的異步任務,于是結束等待狀态,進入執行棧,開始執行。
(4)主線程不斷重複上面的第三步。
下圖就是主線程和任務隊列的示意圖。
事件循環說明
簡單來說,Vue 在修改資料後,視圖不會立刻更新,而是等同一事件循環中的所有資料變化完成之後,再統一進行視圖更新。
知乎上的例子:
//改變資料
vm.message = 'changed'
//想要立即使用更新後的DOM。這樣不行,因為設定message後DOM還沒有更新
console.log(vm.$el.textContent) // 并不會得到'changed'
//這樣可以,nextTick裡面的代碼會在DOM更新後執行
Vue.nextTick(function(){
console.log(vm.$el.textContent) //可以得到'changed'
})
圖解:
事件循環:
第一個 tick(圖例中第一個步驟,即'本次更新循環'):
- 首先修改資料,這是同步任務。同一事件循環的所有的同步任務都在主線程上執行,形成一個執行棧,此時還未涉及 DOM 。
- Vue 開啟一個異步隊列,并緩沖在此事件循環中發生的所有資料改變。如果同一個 watcher 被多次觸發,隻會被推入到隊列中一次。
第二個 tick(圖例中第二個步驟,即'下次更新循環'):
同步任務執行完畢,開始執行異步 watcher 隊列的任務,更新 DOM 。Vue 在内部嘗試對異步隊列使用原生的 Promise.then 和 MessageChannel 方法,如果執行環境不支援,會采用 setTimeout(fn, 0) 代替。
第三個 tick(圖例中第三個步驟):
此時就是文檔所說的
下次 DOM 更新循環結束之後
此時通過 Vue.nextTick 擷取到改變後的 DOM 。通過 setTimeout(fn, 0) 也可以同樣擷取到。
簡單總結事件循環:
同步代碼執行 -> 查找異步隊列,推入執行棧,執行Vue.nextTick[事件循環1] ->查找異步隊列,推入執行棧,執行Vue.nextTick[事件循環2]...
總之,異步是單獨的一個tick,不會和同步在一個 tick 裡發生,也是 DOM 不會馬上改變的原因。
對于事件循環,可以在這裡檢視更詳細的内容: https://segmentfault.com/a/11...
用途
應用場景:需要在視圖更新之後,基于新的視圖進行操作。
created、mounted
需要注意的是,在 created 和 mounted 階段,如果需要操作渲染後的試圖,也要使用 nextTick 方法。
注意 mounted 不會承諾所有的子元件也都一起被挂載。如果你希望等到整個視圖都渲染完畢,可以用 vm.$nextTick 替換掉 mounted
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the
// entire view has been rendered
})
}
其他應用場景
其他應用場景如下三例:
例子1:
點選按鈕顯示原本以 v-show = false 隐藏起來的輸入框,并擷取焦點。
showsou(){
this.showit = true //修改 v-show
document.getElementById("keywords").focus() //在第一個 tick 裡,擷取不到輸入框,自然也擷取不到焦點
}
修改為:
showsou(){
this.showit = true
this.$nextTick(function () {
// DOM 更新了
document.getElementById("keywords").focus()
})
}
例子2:
點選擷取元素寬度。
<div id="app">
<p ref="myWidth" v-if="showMe">{{ message }}</p>
<button @click="getMyWidth">擷取p元素寬度</button>
</div>
getMyWidth() {
this.showMe = true;
//this.message = this.$refs.myWidth.offsetWidth;
//報錯 TypeError: this.$refs.myWidth is undefined
this.$nextTick(()=>{
//dom元素更新後執行,此時能拿到p元素的屬性
this.message = this.$refs.myWidth.offsetWidth;
})
}
例子3:
使用 swiper 插件通過 ajax 請求圖檔後的滑動問題。
執行個體了解 nextTick 應用
下面的例子來自 https://www.cnblogs.com/hity-..., 稍有改動。各位可以複制運作一遍,加深了解。
<template>
<div>
<ul>
<li class="example" v-for="item in list1">{{item}}</li>
</ul>
<ul>
<li class="example" v-for="item in list2">{{item}}</li>
</ul>
<ol>
<li class="example" v-for="item in list3">{{item}}</li>
</ol>
<ol>
<li class="example" v-for="item in list4">{{item}}</li>
</ol>
<ol>
<li class="example" v-for="item in list5">{{item}}</li>
</ol>
</div>
</template>
<script type="text/javascript">
export default {
data() {
return {
list1: [],
list2: [],
list3: [],
list4: [],
list5: []
}
},
created() {
this.composeList12()
this.composeList34()
this.composeList5()
this.$nextTick(function() {
// DOM 更新了
console.log('finished test ' + new Date().toString(),document.querySelectorAll('.example').length)
})
},
methods: {
composeList12() {
let me = this
let count = 10000
for (let i = 0; i < count; i++) {
this.$set(me.list1, i, 'I am a 測試資訊~~啦啦啦' + i)
}
console.log('finished list1 ' + new Date().toString(),document.querySelectorAll('.example').length)
for (let i = 0; i < count; i++) {
this.$set(me.list2, i, 'I am a 測試資訊~~啦啦啦' + i)
}
console.log('finished list2 ' + new Date().toString(),document.querySelectorAll('.example').length)
this.$nextTick(function() {
// DOM 更新了
console.log('finished tick1&2 ' + new Date().toString(),document.querySelectorAll('.example').length)
})
},
composeList34() {
let me = this
let count = 10000
for (let i = 0; i < count; i++) {
this.$set(me.list3, i, 'I am a 測試資訊~~啦啦啦' + i)
}
console.log('finished list3 ' + new Date().toString(),document.querySelectorAll('.example').length)
this.$nextTick(function() {
// DOM 更新了
console.log('finished tick3 ' + new Date().toString(),document.querySelectorAll('.example').length)
})
setTimeout(me.setTimeout1, 0)
},
setTimeout1() {
let me = this
let count = 10000
for (let i = 0; i < count; i++) {
this.$set(me.list4, i, 'I am a 測試資訊~~啦啦啦' + i)
}
console.log('finished list4 ' + new Date().toString(),document.querySelectorAll('.example').length)
me.$nextTick(function() {
// DOM 更新了
console.log('finished tick4 ' + new Date().toString(),document.querySelectorAll('.example').length)
})
},
composeList5() {
let me = this
let count = 10000
this.$nextTick(function() {
// DOM 更新了
console.log('finished tick5-1 ' + new Date().toString(),document.querySelectorAll('.example').length)
})
setTimeout(me.setTimeout2, 0)
},
setTimeout2() {
let me = this
let count = 10000
for (let i = 0; i < count; i++) {
this.$set(me.list5, i, 'I am a 測試資訊~~啦啦啦' + i)
}
console.log('finished list5 ' + new Date().toString(),document.querySelectorAll('.example').length)
me.$nextTick(function() {
// DOM 更新了
console.log('finished tick5 ' + new Date().toString(),document.querySelectorAll('.example').length)
})
}
}
}
</script>
結果:
參考文章
vue nextTick深入了解-vue性能優化、DOM更新時機、事件循環機制;
JavaScript 運作機制詳解:再談Event Loop
知乎:vue.js$nextTick的一個問題
JS 事件循環機制 - 任務隊列、web API、JS主線程的互相協同