$nextTick
的作用是等待畫面更新後才執行程式,因為有些時候我們需要操作畫面上的 DOM,例如是取得某個 DOM 節點的文字、取得某元素的高度等等。事實上,當我們修改 Vue 裏的資料時,Vue 不會馬上更新畫面,而是採用非同步來更新畫面。因此,如果我們需要操作最新的 DOM,就需要等 Vue 更新好畫面後才執行,否則只會操作舊的 DOM。
以下會再作詳細解說。
在進入語法部分之前,先理解為什麼需要 $nextTick
這個方法。原因是 Vue 會以非同步方式更新畫面的 DOM。
為了優化效能,當你修改資料後,Vue 並不會立即渲染畫面。例如以下這寫法,如果 Vue 是同步更新 DOM ,就要渲染 1000 次畫面:
<div> {{ n }} </div>
for(let i = 0; i < 1000; i++) {
this.n = i
}
一個很重要的概念:修改資料和更新畫面的 DOM 是兩回事。前者是同步執行,後者是非同步執行。
借用這篇文章提到的例子,明顯看到如果不等待更新畫面的非同步程式執行完,貿然操作畫面的 DOM 時,你所操作的只是還未被更新的 DOM:
<template>
<div id="app">
<p ref="pDOM"> {{ p }} </p>
<p> {{ p1 }} </p>
<p> {{ p2 }} </p>
<p> {{ p3 }} </p>
</div>
</template>
data() {
return {
p: 'Before nextTick',
p1: '',
p2: '',
p3: ''
}
},
mounted() {
this.p = 'After nextTick'
// 只取到舊的 DOM
this.p1 = this.$refs.pDOM.innerHTML
// 取到已更新的 DOM
this.$nextTick( () =>
this.p2 = this.$refs.pDOM.innerHTML
)
// 只取到舊的 DOM
this.p3 = this.$refs.pDOM.innerHTML
}
https://codesandbox.io/s/vue-yong-nexttick-li-jie-fei-tong-bu-geng-xin-dom-qu7zd
以上例子可見,雖然 this.p3 = this.$refs.pDOM.innerHTML
是在 this.p = 'After nextTick'
之後,但因為畫面的 DOM 還沒更新,所以 this.p3
所取得到的文字會是 Before nextTick
。
Vue 2 的官方文件有提到:
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
建議大家也看看英文版本,中英一起理解文件的原意。然而,這部分我也花了點時間參考其他文章來理解,這裏我用白話去解說我所理解的意思:
每次 Vue 偵測到資料變化時,都會開一個陣列去暫存所有資料變化。等到當所有同步程式都被執行掉後,就會開始按這個暫存陣列的記錄,更新畫面的 DOM。
所謂同一事件循環(event loop)。 以下程式碼就是在同一個事件循環裏,也就是第一個 tick:
this.p = 'After nextTick' // After nextTick
this.p1 = this.$refs.pDOM.innerHTML // Before nextTick
this.p3 = this.$refs.pDOM.innerHTML // Before nextTick
當以上三行程式碼被執行掉後,就會開始執行更新 DOM 這非同步的程式,而根據這次記錄,this.$refs.pDOM.innerHTML
依舊是 'Before nextTick',所以 Vue 就按此記錄渲染 DOM,也就是你目前看到的畫面結果。
之後,接下來的事件循環就只有這行:
this.p2 = this.$refs.pDOM.innerHTML // After nextTick
目前是第二個 tick,剛剛 DOM 已經更新了一次,所以目前 Vue 知道現在 this.$refs.pDOM.innerHTML
是 "After nextTick"。於是 Vue 再次把這紀錄暫存起來,如果目前已經執行掉所有同步程式,就會開始執行非同步程式,即是在事件佇列(event queue) 把更新 DOM 的任務拿回來執行,按暫存記錄更新 DOM。把 p2
渲染為 "After nextTick"。
用流程圖來理解:
以上提到事件迴圈(event loop) 和事件佇列(event queue),建議大家使用 loupe來理解這裏的概念。這裏也附上去年我所寫的非同步與事件佇列文章。
小提醒:理解非同步和 Event loop 的概念是非常重要,除了因為是面試常見題目,也有助於在非同步程式中除錯。
最常見到的例子是當我們在可以捲動的列表中加入新資料時,捲軸會滾到最下方。
https://codesandbox.io/s/nexttick-scrollheight-li-zi-4xgd5?file=/src/App.vue
這裏的重點是:
當加進列表裏的畫面被更新時,才執行「滾動列表最下方」程式碼:
this.$nextTick(() => {
const list = this.$refs.list;
list.scroll({
top: list.scrollHeight,
behavior: "smooth",
});
// 另一種寫法
// const list = this.$refs.list;
// list.scrollTop=list.scrollHeight;
});
另一個例子是我曾經在開發時遇到的情況。使用者點擊按鈕修改文字時,會出現 input 輸入欄,並且要自動 focus。如果不使用 $nextTick
的話,就會報錯。因為畫面還沒有我想要抓取的 DOM。
edit() {
this.editing = true;
this.$nextTick(() => this.$refs.autoFocusInput.focus());
// 會報錯
// this.$refs.autoFocusInput.focus();
}
https://codesandbox.io/s/nexttick-input-focus-li-zi-13s7l?file=/src/App.vue
$nextTick
的作用是等畫面的 DOM 更新後才執行程式。Vue中的$nextTick機制
Understanding $nextTick in Vue.js
重新認識 Vue.js - 1-7 元件的生命週期與更新機制