当数据发生变化后,Vue 不会立即更新 DOM,而是加入一个更新队列,并在下一次事件循环中更新视图。而 Vue.nextTick
可以在下次 DOM 更新循环结束之后执行回调。这使得我们可以在 DOM 变化后立即操作更新后的 DOM,例如获取更新后的元素宽高等。
一、Vue.nextTick 的调用方式
Vue 提供了两种调用方式:
1.1 全局调用方式
Vue.nextTick(() => {
// DOM 更新完毕后的回调
})
1.2 Vue 实例上的调用方式
new Vue({
// ...
methods: {
doSomething: function () {
// ...
this.$nextTick(function () {
// DOM 更新完毕后的回调
})
}
}
})
二、Vue.nextTick 的实现方法
Vue 通过将回调函数处理成微任务或宏任务来实现 Vue.nextTick
,具体的实现方法如下:
2.1 将回调函数封装成一个 Watcher
使用 Vue.$watch
方法来实现将回调函数封装成一个 Watcher
对象:
Watcher.prototype.run = function () {
this.getAndInvoke(this.cb) // 执行回调函数
}
function Watcher () {
// 将回调函数绑定到该 Watcher 上
this.cb = cb
this.vm = vm
this.cb()
}
2.2 将 Watcher 放入异步队列中
Vue 通过异步队列来统一管理所有需要监听的 Watcher。在同一个事件循环中,Vue 将多次更新的 Watcher 排队执行,从而减少重复更新。
const queue = []
let has = {}
let waiting = false
function queueWatcher(watcher) {
const id = watcher.id
// 如果队列中不存在 ID 匹配的 Watcher
// 则加入队列并标记已存在
if (!has[id]) {
has[id] = true
queue.push(watcher)
// 判断异步队列是否处于等待状态
// 如果没有在等待,则调用 nextTickFlush 执行异步任务
if (!waiting) {
waiting = true
nextTick(nextTickFlush)
}
}
}
function nextTickFlush() {
waiting = false
const copiedQueue = queue.slice(0)
queue.length = 0
has = {}
// 遍历队列中的 Watcher,逐一执行回调函数
copiedQueue.forEach(watcher => {
watcher.run()
})
}
2.3 将 Watcher 加入异步队列中的方式:微任务 / 宏任务
在异步执行回调函数的过程中,Vue.nextTick
的实现方法可能采用微任务或宏任务的方式,Vue 会依次尝试以下方式,根据支持情况来决定最终的方式:
// 尝试使用 Promise 微任务(常规方法)
if (typeof Promise !== 'undefined') {
const p = Promise.resolve()
timerFunc = () => {
p.then(nextTickFlush)
}
}
// 尝试使用 MutationObserver 微任务
else if (typeof MutationObserver !== 'undefined') {
let counter = 1
const observer = new MutationObserver(nextTickFlush)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, { characterData: true })
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
}
// 尝试使用 setImmediate 宏任务
else if (typeof setImmediate !== 'undefined') {
timerFunc = () => {
setImmediate(nextTickFlush)
}
}
// 尝试使用 setTimeout 宏任务
else {
timerFunc = () => {
setTimeout(nextTickFlush, 0)
}
}
三、示例说明
3.1 示例一:组件中使用 Vue.nextTick
在下面的示例中,一个组件内部的 data
属性改变后,使用 Vue.nextTick
来获取更新后的 DOM 节点信息,并动态设置一个元素的样式。
<template>
<div>
<p>当前计数:{{ count }}</p>
<button @click="increment">增加计数</button>
<div ref="box" :style="boxStyle">待更新元素</div>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
this.$nextTick(() => {
const box = this.$refs.box
const { width, height } = box.getBoundingClientRect()
const size = Math.min(width, height)
box.style.width = size + 'px'
box.style.height = size + 'px'
})
}
},
computed: {
boxStyle() {
return {
border: '1px solid #ddd',
textAlign: 'center',
transition: 'all .5s',
cursor: 'pointer'
}
}
}
}
</script>
3.2 示例二:在方法中使用 Vue.nextTick
在下面的示例中,一个方法内部先改变了某个元素的样式,再使用 Vue.nextTick
来获取该元素更新后的宽度和高度。
<template>
<div>
<p>当前宽度:{{ width }},高度:{{ height }}</p>
<button @click="updateBox">更新元素</button>
<div ref="box" class="box"></div>
</div>
</template>
<script>
export default {
data() {
return {
width: 0,
height: 0
}
},
methods: {
updateBox() {
const box = this.$refs.box
box.style.width = '200px'
box.style.height = '100px'
this.$nextTick(() => {
const { width, height } = box.getBoundingClientRect()
this.width = width
this.height = height
})
}
}
}
</script>
<style>
.box {
background-color: #eee;
width: 100px;
height: 50px;
}
</style>
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:浅谈Vue.nextTick 的实现方法 - Python技术站