小试牛刀
<script>
document.addEventListener('click', function () {
Promise.resolve().then(() => console.log(1));
console.log(2);
})
document.addEventListener('click', function () {
Promise.resolve().then(() => console.log(3));
console.log(4);
})
</script>
打印顺序是 2 1 4 3
问:为什么打印顺序不是 1 2 3 4?
1.事件循环是什么?为什么有事件循环?
一句话来说就是指浏览器或Node的一种解决JS单线程运行时不会阻塞的一种机制。
我们都知道 JavaScript 引擎是单线程,也就是说每次只能执行一项任务,其他任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务,但是一些高耗时操作就带来了进程阻塞问题。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,用户引擎必须使用 event loops。
1.1浏览器线程:
(1)GUI 渲染线程:
负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该线程。和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。
(2)JS 引擎线程:
单线程工作,负责解析运行 JavaScript 脚本。和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
(3)事件触发线程:
当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
(4)定时器触发线程:
浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理。
(5)http 请求线程:
http 请求的时候会开启一条请求线程,请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。
1.2事件循环(Event Loop):
具体来说,就是执行栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环。
(1)执行栈:JS执行栈( call-stack )是一种后进先出的数据结构。所有的任务都会被放到执行栈等待主线程执行。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。
(2)同步任务与异步任务:JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行,异步任务则会在异步有了结果后将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
2.任务队列
任务队列:一块内存空间,用于存放执行时机到达的异步函数。当JS引擎空闲(执行栈没有可执行的上下文),它会从队列里拿出第一个函数执行。分为微队列和宏队列。
微队列:存放微任务对应的函数;
宏队列:存放宏任务对应的队列;
3.微任务(microTask)是什么?宏任务(macroTask)又是什么?
微任务:Process.nextTick(Node独有)、Object.observe(废弃)、Promise(then/catch/finally)、queueMicrotask、MutationObserver(具体使用方式查看MDN)(JS自身发起);
宏任务:script全部代码、setTimeout、setInterval、setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)、I/O、UI Rendering。(宿主发起)
4.script 整体代码是一个宏任务如何理解?
#### 实际上如果同时存在两个 script 代码块,会首先在执行第一个 script 代码块中的同步代码,如果这个过程中创建了微任务并进入了微任务队列,第一个 script 同步代码执行完之后,会首先去清空微任务队列,再去开启第二个 script 代码块的执行。所以这里应该就可以理解 script(整体代码块)为什么会是宏任务。
(1)代码通过script脚本引入
<script src="./script1.js"></script>
<script src="./script2.js"></script>
script1.js
console.log('代码块1', '同步代码1');
setTimeout(() => {
console.log('代码块1的setTimeout');
}, 0);
Promise.resolve('代码块1的promise').then((data)=>{
console.log(data);
})
console.log('代码块1','同步代码2');
script2.js
console.log('代码块2','同步代码1' );
setTimeout(() => {
console.log('代码块2的setTimeout');
}, 0);
Promise.resolve('代码块2的promise').then((data)=>{
console.log(data);
})
console.log('代码块2', '同步代码2');
执行顺序
小试牛刀
<script>
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function () {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function () {
console.log('promise1')
})
.then(function () {
console.log('promise2')
})
console.log('script end')
</script>
执行顺序
(2)代码整体作为setTimeout回调函数内容放入
<script>
setTimeout(() => {
console.log('代码块1', '同步代码1');
setTimeout(() => {
console.log('代码块1的setTimeout');
}, 0);
Promise.resolve('代码块1的promise').then((data) => {
console.log(data);
})
console.log('代码块1', '同步代码2');
}, 0);
setTimeout(() => {
console.log('代码块2', '同步代码1');
setTimeout(() => {
console.log('代码块2的setTimeout');
}, 0);
Promise.resolve('代码块2的promise').then((data) => {
console.log(data);
})
console.log('代码块2', '同步代码2');
}, 0);
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
setTimeout(() => {
}, 0);
Promise.resolve(3).then((data) => {
console.log(data);
})
console.log(4);
}, 0);
</script>
执行顺序
5.微队列与宏队列如何执行(事件循环机制)
在事件循环中,每进行一次循环操作称为tick,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤可以总结如下:
(1)在此次 tick 中选择最先进入队列的任务( oldest task ),如果有则执行(一次);
(2)检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtask Queue;
##### 总的结论就是,执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。
(3)更新 render;
(4)主线程重复执行上述步骤;
小试牛刀
<script>
console.log(1);
Promise.resolve(2).then(data => {
console.log(data);
})
const p = new Promise((resolve, reject) => {
console.log(3);
resolve(4);
})
setTimeout(() => {
console.log(5);
p.then(data => {
console.log(data)
});
}, 0);
function one(data) {
return new Promise((resolve, reject) => {
console.log(6);
console.log(data);
resolve(); // 不推向已决,then方法不会执行
});
}
async function two(data) {
console.log(data);
one(7).then((data1) => {
const result = data1;
console.log(8);
})
}
setTimeout(() => {
console.log(9);
two(10);
console.log(11);
}, 0);
</script>