iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0
Modern Web

不只懂 Vue 語法:Vue.js 觀念篇系列 第 16

不只懂 Vue 語法:為什麼需要使用 $nextTick ?

問題回答

$nextTick 的作用是等待畫面更新後才執行程式,因為有些時候我們需要操作畫面上的 DOM,例如是取得某個 DOM 節點的文字、取得某元素的高度等等。事實上,當我們修改 Vue 裏的資料時,Vue 不會馬上更新畫面,而是採用非同步來更新畫面。因此,如果我們需要操作最新的 DOM,就需要等 Vue 更新好畫面後才執行,否則只會操作舊的 DOM。

以下會再作詳細解說。

Vue 採用非同步來更新畫面

在進入語法部分之前,先理解為什麼需要 $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 在什麼時候才會更新畫面?

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 的概念是非常重要,除了因為是面試常見題目,也有助於在非同步程式中除錯。

$nextTick 使用例子:scroll

最常見到的例子是當我們在可以捲動的列表中加入新資料時,捲軸會滾到最下方。

完整程式碼

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;
});

結果

$nextTick 使用例子:input 自動 focus

另一個例子是我曾經在開發時遇到的情況。使用者點擊按鈕修改文字時,會出現 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 更新後才執行程式。
  • 更新畫面 DOM 是非同步執行,修改資料是同步執行。
  • 當資料更動時,Vue 會把所有資料變化都暫存起來。等待同步程式被執行完,才會根據這暫存記錄,執行更新畫面 DOM 的程式。

參考資料

Vue中的$nextTick機制
Understanding $nextTick in Vue.js
重新認識 Vue.js - 1-7 元件的生命週期與更新機制


上一篇
不只懂 Vue 語法:請說明 keep-alive 以及 is 屬性的作用?
下一篇
不只懂 Vue 語法:什麼是 directive?請示範如何使用 directive?
系列文
不只懂 Vue 語法:Vue.js 觀念篇31

尚未有邦友留言

立即登入留言