Vue 的异步更新队列

807 阅读2分钟

Vue 的异步更新

我们知道,Vue 在更新 DOM 时是 异步 执行的。只要侦听到数据变化,Vue 将开启一个异步队列,并缓冲在同一事件循环中发生的所有数据变更。

如果同一个 watcher 被多次触发,只会被推送到队列中一次,这种在缓冲时去除重复操作的机制非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (去重后的) 操作。

Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

Vue 的异步更新有一个关键点就在于去重机制,这种去重机制减少了频繁处理数据、操作 DOM 和渲染的开销。例如,使用一个 for 循环来动态改变数据 100 次,其实它只会应用最后一次改变,如果没有这种缓存队列的去重机制,DOM就要重绘 100 次。去重后实际上 DOM 只更新了一次。

<div id="app">{{value}}</div>
var vm = new Vue({
  el: 'app',
  data: {
    value: 1
  }
})
for(let i=0; i<100;i++) {
  vm.value ++
}
// value 只显示一次最终结果

更新后立即执行回调

当你设置 this.value = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环中更新。如果需要在数据变化之后等待 Vue 完成更新 DOM 后立即执行某一操作,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:

<div id="app">{{value}}</div>
var vm = new Vue({
  el: 'app',
  data: {
    value: 1
  }
})
vm.value = vm.value + 1
console.log(vm.$el.textContent)
Vue.nextTick(function () {
  console.log(vm.$el.textContent)
})

分别会输出 0 和 1。一个是输出 DOM 更新之前的数据,使用nextTick才会得到更新后的数据。

在组件中没必要使用 vm.$nextTick() 这样的方法,使用 this 将自动绑定到当前的 Vue 实例上:

Vue.component('NameList', {
  template: '<span>{{ name }}</span>',
  data: function () {
    return {
      name: 'blank'
    }
  },
  methods: {
    updateName() {
      this.name = 'Tom'
      console.log(this.$el.textContent) // 'blank'
      this.$nextTick(function () {
        console.log(this.$el.textContent) //  'Tom'
      })
    }
  }
})

因为 $nextTick() 可以返回一个 Promise 对象,使用新的 ES2017 async/await 语法也可这样实现:

methods: {
  updateName: async function () {
    this.name = 'Tom'
    console.log(this.$el.textContent) // => 'blank'
    await this.$nextTick()
    console.log(this.$el.textContent) // => 'Tom'
  }
}

nextTick() 方法

Vue 的 nextTick 方法在上文里已经演示了,这里再补充一些信息。

vm.$nextTick( [callback] )

  • 参数:

    • Function [callback]
  • 用法:
    将回调函数延迟到下次 DOM 更新循环之后执行。在修改数据之后使用它,然后等待 DOM 更新后执行。它的回调的 this 自动绑定到调用它的实例上。

此外,如果没有提供回调且在支持 Promise 的环境中,此方法会返回一个 Promise。

nextTick 的常见应用场景

下面了解下nextTick的主要的应用场景及使用原因。

created() 生命周期钩子

在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。

created()钩子函数执行的时候 DOM 其实并未进行任何渲染,而此时进行 DOM 操作是徒劳,所以此处一定要将关于 DOM 操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted()钩子函数,因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题 。

  • 在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中。

此外,vm.$nextTick 方法还可以跟一些生命周期钩子结合使用,以下是它的一些应用场景:

mounted() 生命周期钩子

实例被渲染和挂载后调用,这时 el 被新创建的 vm.$el 替换了。
注意 mounted 不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,可以在 mounted 内部使用 vm.$nextTick

    mounted() {
      this.$nextTick(function () {
        // 仅在整个视图都被渲染之后才会运行的代码
      })
    }

updated() 生命周期钩子

在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。

当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。但是也需要注意,updated 不会保证所有的子组件也都被重新渲染完毕。如果你希望等到整个视图都渲染完毕再进行回调,可以在 updated 里使用 vm.$nextTick

```
updated: function () {
  this.$nextTick(function () {
    //  仅在整个视图都被重新渲染之后才会运行的代码     
  })
}
```

如果需要在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的 DOM 结构的时候,这个操作都应该放进Vm.$nextTick()的回调函数中。

总之,vm.$nextTick是一个 Vue 的异步 API ,用来在当前的队列里代码执行完毕后,再去执行异步回。是 Vue 的异步操作中非常常用的一个 API。

图解 Vue 生命周期

既然说起生命周期钩子,这里可以再复习一下 Vue 的生命周期:

image.png

参考文档:https://segmentfault.com/a/1190000008010666