事件循环
- JS的运行机制是单线程的,主线程的执行过程就是一个
tick。如果有一个线程是增加元素,另外一个线程是删除元素,这样的操作是很蠢的。 - 单线程的是串行的,一个任务完成后,另外一个任务接着继续执行,如此循环。但是在Ajax网页请求,setTimout等事件发生时,这些事件如果还是同步执行,一个接着一个执行,那么网页很有可能会出现长时间卡顿,出现效率低下的问题
- 因此出现了异步事件,意思是这个函数隔一段时间才会拿到结果。但是如何处理和处理异步和同步任务的处理顺序呢,这里,由此出现了任务队列和事件循环。
- 简单来说,事件循环(Event loop)的顺序为:同步任务(代码)执行-> 查找异步队列,推入执行栈,执行可能存在的回调(callback). 这里的过程就是一个事件循环,如果查找到异步队列还有任务,就会继续推入执行栈,执行可能存在的callback, 这个过程就是另外一个事件循环。
- 执行栈(Stack)是先进后出的规则,意思是最后添加的函数会被最先执行,而最开始添加的函数会在最后被执行,函数执行后从栈中移除的过程,称为弹栈
- 任务队列(Queue)是先进先出的数据结构。同步的任务在调用栈中执行,异步任务例如ajax的请求获得的结果后,会将ajax的回调函数推入任务队列,当执行栈为空的时间,就会去查询任务队列,如果有就将任务队列第一个任务推入执行栈,执行,如此循环。
- 此外,这里其实还有(microTask)微任务和(macroTask)宏任务, 以上的内容我只是做简单的介绍,更多的信息可以查询文章尾部的链接
nextTick
- 定义: 由于Vue的数据更新驱动视图是异步的,修改数据后,并不会立即进行视图更新,而是等同一事件循环的所有数据变化完成之后,再统一进行视图更新
- 触发时机: 同一事件循环的数据变化完成->DOM完成更新->执行callback(nextTick里的回调函数)
- 应用场景: 在新的视图更新之后,基于新的视图进行更新
- 简单总结: 同步代码执行->查找异步队列,推入执行栈->执行callback【事件循环1】-> 查找异步队列,推入执行栈->执行callback【事件循环2】
- 这小节均指macroTask宏任务,更多的信息可以查询文章尾部的链接
代码实例
<template>
<div>
<ul>
<li v-for="item in list1" :key="item">{{ item }}</li>
</ul>
<ul>
<li v-for="item in list2" :key="item">{{ item }}</li>
</ul>
<ul>
<li v-for="item in list3" :key="item">{{ item }}</li>
</ul>
<ul>
<li v-for="item in list4" :key="item">{{ item }}</li>
</ul>
<ul>
<li v-for="item in list5" :key="item">{{ item }}</li>
</ul>
</div>
</template>
<script>
import Vue from "vue";
export default {
data() {
return {
list1: [],
list2: [],
list3: [],
list4: [],
list5: [],
};
},
created() {
this.composeList12();
this.composeList34();
this.composeList5();
this.$nextTick(function () {
// DOM 更新了
console.log("猜猜我在哪里");
console.log("li元素的数量是:" + document.querySelectorAll("li").length);
});
},
methods: {
composeList12() {
let me = this;
let count = 3;
for (let i = 0; i < count; i++) {
Vue.set(me.list1, i, "测试" + i);
}
console.log("完成同步代码,列表1的循环");
for (let i = 0; i < count; i++) {
Vue.set(me.list2, i, "测试信息" + i);
}
console.log("完成同步代码,列表2的循环");
this.$nextTick(function () {
// DOM 更新了
console.log("Dom更新后的tick1和tick2");
console.log("li元素的数量是:" + document.querySelectorAll("li").length);
});
},
composeList34() {
let me = this;
let count = 4;
for (let i = 0; i < count; i++) {
Vue.set(me.list3, i, "测试信息啦啦啦" + i);
}
console.log("完成同步代码,列表3的循环");
this.$nextTick(function () {
// DOM更新了
console.log("Dom更新后的tick3");
console.log("li元素的数量是:" + document.querySelectorAll("li").length);
});
setTimeout(me.setTimeout1, 0);
},
setTimeout1() {
let me = this;
let count = 5;
for (let i = 0; i < count; i++) {
Vue.set(me.list4, i, "测试信息啦啦啦啦" + i);
}
console.log("完成同步代码,列表4的循环");
me.$nextTick(function () {
// DOM 更新了
console.log("Dom更新后的tick4");
console.log("li元素的数量是:" + document.querySelectorAll("li").length);
});
},
composeList5() {
let me = this;
this.$nextTick(function () {
// DOM 更新了
console.log("Dom更新后的tick3.5");
console.log("li元素的数量是:" + document.querySelectorAll("li").length);
});
setTimeout(me.setTimeout2, 0);
},
setTimeout2() {
let me = this;
let count = 6;
for (let i = 0; i < count; i++) {
Vue.set(me.list5, i, "测试信息~~啦啦啦" + i);
}
console.log("完成同步代码,列表5的循环");
me.$nextTick(function () {
// DOM 更新了
console.log("Dom更新后的tick5-1");
console.log("li元素的数量是:" + document.querySelectorAll("li").length);
});
},
},
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
函数执行顺序:
- [事件循环1]:执行同步代码,更新DOM, 执行nextTick的回调函数
- [事件循环2]:查询异步函数任务队列,推入执行栈,执行异步函数,执行nextTick的回调函数
- [事件循环3]:查询异步函数任务队列,推入执行栈,执行异步函数,执行nextTick的回调函数
推断执行顺序:
- 【事件循环1】:updatelist1 -> updatelist2 -> updatelist3 -> DOM更新 -> nextTick触发 -> 执行回调函数 -> Dom更新后的tick1和tick2 -> Dom更新后的tick3 -> DOM更新后的tick3.5 -> 猜猜我在哪里
- 【事件循环2】:updatelist4 -> DOM更新 -> nextTick触发 -> 执行回调函数 -> DOM更新后的tick4
- 【事件循环3】: updatelist5 -> DOM更新 -> nextTick触发 -> 执行回调函数 -> DOM更新后的tick5
实际执行顺序:
执行顺序总结:
- 在同一事件循环中,只有所有数据更新完毕,才会更新DOM
- 在同一事件循环中,如果存在多个nextTick,将会按顺序执行
- 在同一事件循环中,nextTick的视图是相同的
- 每个异步的callback回调都存在于一个独立的时间循环,有自己的nextTick