在项目中也有用到Vue中的nextTick,但是知其然而不知其所以然,故参考了很多资料进行了了解。其中关于nextTick的解析涉及到不少方面,很多了解的还不透彻,暂且根据自己的一些理解介绍下nextTick。
一、术语解释
在深入nextTick之前先认识几个术语
异步执行
众所周知,Javascript是一个单线程运行的脚本语言,以防止dom更新时冲突。而单线程意味着任务按顺序执行,后一个任务需要等待前一个任务的执行,这就可能出现长时间的阻塞。所以为了避免这种阻塞,我们需要一种非阻塞机制。这种非阻塞机制即异步执行机制,即通过将异步任务如ajax请求、setTimeou、dom交互等交给相应的异步模块去处理,主线程(即单线程)的效率大大提升,可以并行的去处理其他的操作。由此出现了任务队列和事件循环来协调主线程和异步模块之间的工作。
任务队列
当主线程中遇到异步任务时,将其交由对应的异步模块去进行处理,处理完毕后推入一个异步队列,主线程任务执行完毕会查询该异步队列,取出一个任务推入主线程,进行处理,此时的处理是处理异步任务完成后的回调,这个过程中的异步队列称为任务队列。
任务队列存在多个,同一任务队列内,按队列顺序被主线程取走;不同任务队列之间,存在着优先级,优先级高的优先获取(如用户I/O)
事件循环
1.主线程执行完毕,查询任务队列取出一个任务推入主线程处理回调。2.重复该动作。该过程称为事件循环。
简单事件循环机制示例图(帮助理解)
二、初识nextTick
官方解释
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
官方文档中提供了一个简单的应用例子,但并未指出原因以及用与不用的区别,在此对其官方实例进行改造:
模板
Vue实例
点击前
点击后
可以看出msg2显示的内容是变换之后的,即在只有nextTick中的innerHtml更新完毕,由此可见在vue中,观测到数据变化了,dom不会立即变化,而是异步更新dom。此时再看nextTick官方解释是否可以这样理解(按照异步机制原理):vue实例数据变化,进入同步队列,而数据变化引发的dom更新作为一个异步任务进入异步队列进行更新,更新完成(称为:下次 DOM 更新循环结束)推入任务队列,同步队列任务执行完毕,去任务队列中取出异步任务执行回调(nextTick),在这个回调中才能保证dom更新完成,回调完成,继续上述步骤。而过程中:dom更新异步任务进入异步队列进行更新,更新完成推入任务队列,主线程去任务队列中取出异步任务执行称为一个tick。
三、深层次实例理解验证nextTick
验证设计
1.同一事件循环中数据更新与dom更新、nextTick的执行顺序
2.同一事件循环中不同nextTick的执行顺序、dom的更新顺序
3.利用setTimeout主动触发异步任务,异步任务与同步任务中数据更新、dom更新、nextTick的执行顺序
4.多个异步任务中数据更新、dom更新、nextTick的执行顺序
实例
<template>
<div>
<ul><li v-for="item in list1">{{item}}</li></ul>
<ul><li v-for="item in list2">{{item}}</li></ul>
<ul><li v-for="item in list3">{{item}}</li></ul>
<ul><li v-for="item in list4">{{item}}</li></ul>
<ul><li v-for="item in list5">{{item}}</li></ul>
<ul><li v-for="item in list6">{{item}}</li></ul>
<ul><li v-for="item in list7">{{item}}</li></ul>
</div>
</template>
<script>
export default {
data() {
return {
list1: [],
list2: [],
list3: [],
list4: [],
list5: [],
list6: [],
list7: [],
};
},
created() {
this.composeList12();
this.composeList34();
this.composeList5();
this.composeList6();
this.composeList7();
this.$nextTick(() => {
console.log(`finished test ${new Date().toString()}`);
console.log(document.querySelectorAll('li').length);
});
},
methods: {
composeList12() {
const count = 10000;
for (let i = 0; i < count; i += 1) {
this.list1.push(`testList1----${i}`);
}
console.log(`changed list1 ${new Date().toString()}`);
for (let i = 0; i < count; i += 1) {
this.list2.push(`testList2----${i}`);
}
console.log(`changed list2 ${new Date().toString()}`);
this.$nextTick(() => {
console.log(`domChanged tick1&2 ${new Date().toString()}`);
console.log(document.querySelectorAll('li').length);
});
},
composeList34() {
const count = 10000;
for (let i = 0; i < count; i += 1) {
this.list3.push(`testList3----${i}`);
}
console.log(`changed list3 ${new Date().toString()}`);
this.$nextTick(() => {
// DOM 更新了
console.log(`domChanged tick3 ${new Date().toString()}`);
console.log(document.querySelectorAll('li').length);
});
setTimeout(this.setTimeout1, 0);
},
setTimeout1() {
const count = 10000;
for (let i = 0; i < count; i += 1) {
this.list4.push(`testList4----${i}`);
}
console.log(`changed list4 ${new Date().toString()}`);
this.$nextTick(() => {
// DOM 更新了
console.log(`domChanged tick4 ${new Date().toString()}`);
console.log(document.querySelectorAll('li').length);
});
},
composeList5() {
const count = 10000;
for (let i = 0; i < count; i += 1) {
this.list5.push(`testList5----${i}`);
}
console.log(`changed list5 ${new Date().toString()}`);
this.$nextTick(() => {
// DOM 更新了
console.log(`domChanged tick5 ${new Date().toString()}`);
console.log(document.querySelectorAll('li').length);
});
},
composeList6() {
const count = 10000;
this.$nextTick(() => {
// DOM 更新了
console.log(`finished tick6-1 ${new Date().toString()}`);
console.log(document.querySelectorAll('li').length);
});
setTimeout(this.setTimeout2, 0);
},
setTimeout2() {
const count = 10000;
for (let i = 0; i < count; i += 1) {
this.list6.push(`testList6----${i}`);
}
console.log(`changed list6 ${new Date().toString()}`);
this.$nextTick(() => {
// DOM 更新了
console.log(`domChanged tick6 ${new Date().toString()}`);
console.log(document.querySelectorAll('li').length);
});
},
composeList7() {
const count = 10000;
for (let i = 0; i < count; i += 1) {
this.list7.push(`testList7----${i}`);
}
console.log(`changed list7 ${new Date().toString()}`);
this.$nextTick(() => {
// DOM 更新了
console.log(`domChanged tick7 ${new Date().toString()}`);
console.log(document.querySelectorAll('li').length);
});
},
},
};
</script>
运行结果
从结果中可以看出事件循环步骤如下:
同步环境:update list1 -> update list2 -> update list3->setTimeout1进入异步模块-> update list5->setTimeout2进入异步模块-> update list7 ->[事件循环1]异步dom更新 ->依次执行tick: tick‘1&2’ -> tick‘3’ -> tick‘5’ -> tick’6-1’ -> tick‘7’ -> tick’test'->[事件循环2]update list4 -> tick’4’ ->[事件循环3]update list6 -> tick’6’
结论
1.在同一事件循环中,只有所有的数据更新完毕,异步dom更新完成,才会调用nextTick--->官方解释:观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变,并在缓冲时去除重复数据变化,可以避免DOM的频繁变动,从而避免了因此带来的浏览器卡顿,大幅度提升性能。
2.从同步执行环境中的5个tick对应的‘li’数量均为50000可看出,同一事件循环中,nextTick所在的视图是相同的;
3.在同一事件循环中,如果存在多个nextTick,将会按最初的执行顺序进行调用。
4.每个异步callback最后都会处在一个独立的事件循环中,对应自己独立的nextTick;
四、应用场景
1.在Vue生命周期的created()钩子函数进行DOM操作时一定要将操作放在nextTick()的回调函数中,因为在created生命周期时初始dom结构还未挂载到实例上且未渲染到页面上。与之对应的mount()钩子函数,该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题 。
2.v-if/v-show变化后需要获取dom结构时
总体来说就是nextTick作为vue提供的一个钩子函数,在需要使用随数据改变而改变的DOM结构的时候,可以将操作放在Vue.nextTick()的回调函数中保证必然能够取到dom结构,并且在渲染、用户交互过程中,要巧用同步环境及异步环境,首页展现的内容,尽量保证在同步环境中完成,其他内容,拆分到异步中,从而保证性能和体验。
--- 一直在默默探索的前端小白好奇宝宝。