nextTick()是什么?
定义:在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,获取更新后的DOM。
Vue就会开启一个任务队列,然后把在同一个事件循环 (Event loop) 中观察到数据变化的 Watcher(Vue源码中的Wacher类是用来更新Dep类收集到的依赖的)推送进这个队列。
如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。
nextTick的作用是为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback),JS是单线程的,拥有事件循环机制,nextTick的实现就是利用了事件循环的宏任务和微任务。
先解读nextTick源码
(1)创建了一个callbacks数组和 一个flushCallbacks函数,将nextTick传入得到回调push到数组里,然后flushCallbacks循环执行
export let isUsingMicroTask = false //是否启用微任务开个
// 1.创建了一个callbacks数组和 一个flushCallbacks函数,将nextTick传入得到回调push到数组里,然后flushCallbacks循环执行
//callbacks是定义一个回调队列
const callbacks = []
//pending是执行异步的开关,标记是否正在执行回调函数
let pending = false
// flushCallbacks函数是负责执行队列中的全部回调
function flushCallbacks() {
// 重置异步开关
pending = false
// 防止nextTick里面有nextTick出现的问题
// 在执行之前先进行存储然后清空回调队列
const copies = callbacks.slice(0)
callbacks.length = 0
//循环执行队列任务
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
(2)判断采用了哪种异步回调方法
因为微任务优先级高,首先尝试微任务
《1》尝试使用promise.then(微任务)
《2》使用MutationObserver(微任务)回调
《3》使用setImmediate(宏任务)回调
《4》使用setTimeout(宏任务)回调
let timerFunc //定义一个执行异步任务的方法
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
// 判断当前的队列中是否支持原生的promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 保存执行一个异步任务
const p = Promise.resolve()
timerFunc = () => {
// 执行回调函数flushCallbacks
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
// ios 中可能会出现一个回调被推入微任务队列,但是队列没有刷新的情况
// 所以用一个空的计时器来强制刷新任务队列
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// 不支持promise的话,在支持MutationObserver的非IE浏览器下
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
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
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
// 使用setImmediate,虽然也是宏任务,但是比setTimeout更好
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
// 上面都不支持的情况下,使用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 抛出nextTick方法
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve
// callbacks是维护微任务的数组
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 将维护的队推送到微任务队列中维护
timerFunc()
}
// $flow-disable-line
// 判断nextTick 没有参数,浏览器支持promise,就返回一个promise对象
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
nextTick的调用方法
1.回调函数 vue.nextTick(callback)
2.promise方法:vue.nextTick().then(callback)
3.实例方式:vm.$nextTick(callback)
vue.nextTick的应用
1.Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载已完成。
created(){
let that=this;
that.$nextTick(function(){ //不使用this.$nextTick()方法会报错
that.$refs.aa.innerHTML="created中更改了按钮内容"; //写入到DOM元素
});
},
2.2、当项目中你想在改变DOM元素的数据后基于新的dom做点什么,对新DOM一系列的js操作都需要放进Vue.nextTick()的回调函数中; 通俗的理解是:更改数据后当你想立即使用js操作新的视图的时候需要使用它
<template>
<div class="hello">
<h3 id="h">{{testMsg}}</h3>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
testMsg:"原始值",
}
},
methods:{
changeTxt:function(){
let that=this;
that.testMsg="修改后的文本值"; //vue数据改变,改变dom结构
let domTxt=document.getElementById('h').innerText; //后续js对dom的操作
console.log(domTxt); //输出可以看到vue数据修改后DOM并没有立即更新,后续的dom都不是最新的
if(domTxt==="原始值"){
console.log("文本data被修改后dom内容没立即更新");
}else {
console.log("文本data被修改后dom内容被马上更新了");
}
},
}
}
</script>
总结
nextTick是Vue提供的一个全局API,由于Vue的异步更新策略导致对数据的修改不会立即体现在DOM变化上,此时如果我们需要立即获取到变化后的DOM状态,就需要使用API(定义+场景)
Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的DOM操作完成之后在调用(必要性)
callbacks里面加入了我们传入的函数,就是nextTick的 ()=>{} 这部分,然后用timeFunc的异步方式调用他们,首选是promise方式。