不只懂 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 元件的生命周期与更新机制


<<:  Day11:11 - 商品服务(2) - 前端 - 总商品资料显示

>>:  Day 10: 面试中成长

[Day8]什麽是乙太坊?

乙太坊也是一个区块链平台!和比特币区块链一样,都是开放、没有人能掌控的。虽然都是区块链平台,但乙太...

Rstudio

Shift+Ctrl+R 分段 可缩 Shift+Ctrl+C 多行注解 Shift+Ctrl+M ...

【程序】说是说不 转生成恶役菜鸟工程师避免 Bad End 的 30 件事 - 25

说是说不 坚守原则,安全第一 一诺千金 最大公约数 ...

[Day 19] SQL select & where

select 使用*号可取得table内所有资料 select * from schema名称.ta...

[Day16] CSS Text Shadow Mouse Move Effec

[Day16] CSS Text Shadow Mouse Move Effect 滑鼠移动 物件更...