还记得第一次接触nextTick是在公司做项目时遇到了问题,最后通过nextTick解决的,之后就去学习了它的相关用法和实现,分享学习路上的小知识,希望大家都能有所收获!
Vue.nextTick()用法
用法:Vue.nextTick([callback,context]);
参数说明:
{Function} [callback]
:回调函数,不传时提供promise调用;{Object} [context]
:回调函数执行的上下文环境,不传默认是自动绑定到调用它的实例上;
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
... //DOM操作
})
// 作为一个 Promise 使用
Vue.nextTick()
.then(function () {
// DOM 更新了
})
Vue.nextTick()是Vue的全局核心API之一,官网描述如下:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
也就是说它会等待DOM更新完成后再去触发回调函数执行,下面结合使用场景来进一步理解。
nextTick使用场景
场景一:如果要在created钩子函数中操作DOM需要放在Vue.nextTick()回调函数中执行;
<template>
<div>
<h3 ref="title">Hello World!</h3>
</div>
</template>
<script>
export default {
created() {
console.log('created');
console.log(this.$refs.title)
this.$nextTick(() => {
console.log('created-nextTick');
console.log(this.$refs.title)
})
},
mounted() {
console.log('mounted');
console.log(this.$refs.title)
this.$nextTick(() => {
console.log('mounted-nextTick');
console.log(this.$refs.title)
})
}
}
</script>
输出结果:
根据输出值的顺序,可以发现在created()钩子函数执行时DOM并没有进行渲染,此时要是操作DOM并没有任何作用,而如果created里使用了nextTick后是可以获取到DOM对象的。由于mounted()钩子函数执行时已经完成了DOM的挂载和渲染,所以此时去操作DOM是没有任何问题的。
场景二:当数据变化后,我们希望拿到修改数据后的DOM结构去做某些操作时也需要放到Vue.nextTick()回调函数中。
<template>
<div>
<h3 ref="title">{{msg}}</h3>
<el-button @click="changeMsg">Click</el-button>
</div>
</template>
<script>
export default {
data() {
return {
msg: 'hello'
}
},
methods: {
changeMsg() {
this.msg = 'changed';
console.log(this.msg); //顺序1:changed
console.log(this.$refs.title.innerHTML) //顺序2:hello
this.$nextTick(() => {
console.log(this.$refs.title.innerHTML) //顺序4:changed
})
console.log(this.$refs.title.innerHTML) //顺序3:hello
}
}
}
</script>
通过执行结果我们发现,修改数据后数据值立马修改了,但是所对应的DOM并没有及时更新,nextTick通过一定的策略去更新DOM,接下来我们看看它的实现原理。
实现原理
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的
Promise.then
、MutationObserver
和setImmediate
,如果执行环境不支持,则会采用setTimeout(fn, 0)
代替。例如,当你设置
vm.someData = 'new value'
,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用Vue.nextTick(callback)
。这样回调函数将在 DOM 更新完成后被调用。
以上是官网关于异步更新队列的描述,完美解答了nextTick的实现机制。为什么DOM更新是一个异步操作呢?它有什么好处?答:提升了渲染效率!!
复习同步异步编程和事件循环,请看这里:https://juejin.im/post/6850418109632200712
源码浅析
流程大概是这个样子的:nextTick执行传递了一个回调函数,把传进来的函数放到callbacks数组中,这个数组其实就是一个事件池,添加操作完成之后,根据pending变量执行timerfunc函数,并且把pending的值修改为true,这样在多次同步使用nextTick时就不会再去执行timerfunc,timerfunc是一个函数,函数里做了异步操作【就是让flushCallbacks异步执行】,异步执行是为了在多次同步执行nextTick时,先把回调函数放到事件池中,等同步执行完成之后,再去执行异步的flushCallbacks,flushCallbacks这个函数就是把事件池中的所有回调函数挨个执行。
那先来了解下上面提到的3个很重要的变量:
callbacks:相当于一个事件池,存储所有回调函数;
pending:标记当前是否正在执行回调函数;
timerFunc:用来触发执行回调函数;
下面贴出核心代码:
nextTick
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// push进callbacks数组
callbacks.push(() => {
cb.call(ctx)
})
if (!pending) {
pending = true
// 执行timerFunc方法
timerFunc()
}
}
timerFunc
let timerFunc
// 判断是否原生支持Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
// 如果原生支持Promise 用Promise执行flushCallbacks
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
// 判断是否原生支持MutationObserver
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
// 如果原生支持MutationObserver 用MutationObserver执行flushCallbacks
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
// 判断是否原生支持setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
// 如果原生支持setImmediate 用setImmediate执行flushCallbacks
setImmediate(flushCallbacks)
}
// 都不支持的情况下使用setTimeout 0
} else {
timerFunc = () => {
// 使用setTimeout执行flushCallbacks
setTimeout(flushCallbacks, 0)
}
}
// flushCallbacks 最终执行nextTick 方法传进来的回调函数
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
实际上,nextTick(callback)类似于Promise().resolve().then(callback)或者setTimeout(callback,0);所以如果要想在DOM更新后获取DOM信息,就需要在本次异步任务创建之后创建一个异步任务。下面通过使用setTimeout来做个验证:
<template>
<h3 class="title">{{msg}}</h3>
</template>
<script>
export default {
data() {
return {
msg: 'before'
}
},
mounted() {
this.msg = 'changed';
let box = document.getElementsByClassName('title')[0];
setTimeout(() => {
console.log(box.innerHTML)
})
}
}
</script>
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 是蹄蹄吖 原文链接:https://juejin.im/post/6865978424633229325