前言
什么是事件循环机制?
由于JavaScript是单线程的,一次只能执行一个任务,事件循环机制能够让js在不阻塞的情况下处理异步操作。
- 同步代码:不耗时执行的代码。
- 异步代码:需要耗时执行的代码。
- 微任务:promise.then(), process.nextTick(), MutationOverserver()
- 宏任务:script, setTimeout, setInterval, setImmediate, I/O, UI-rendering(页面渲染)
执行步骤:
- 执行同步代码(这属于宏任务)
- 同步代码执行完毕后,检查是否有异步代码执行
- 若有,执行所有微任务
- 微任务执行完毕后,如有需要就渲染页面
- 执行异步宏任务,既是这次事件循环的结束也是开启下一次事件循环
什么是进程和线程?
- 进程:CPU运行指令和保存上下文所需的时间
- 线程:执行一段指令需要的时间
例如:
一个浏览器的tab页面是一个进程,这个进程当中有多个线程。
- 渲染线程
- js引擎线程
- http线程
注: js的加载会阻塞页面的渲染,渲染线程和js引擎线程不能同时工作。浏览器会把js代码执行完毕再执行html代码。
js单线程:v8在执行js的过程中,只有一个线程会工作
- 节约性能
let a = 1
console.log(a);
setTimeout(function () {
let b = 2
console.log(b);
a++
}, 1000)
console.log(a);
function bar() {
console.log('bar');
}
bar()
打印结果
分析
代码从上往下执行,由于setTimeout()定时器是耗时代码,v8执行的过程会跳过该段代码,继续执行下面的代码,直到把不耗时的代码执行完成后再来执行耗时代码。所以会先打印 1 1 bar,然后过1s再打印b的值2。
- 节约上下文切换的时间
let a = 1
for(var i = 0; i < 1000; i++) { // 不耗时
a++
}
console.log(a);
setTimeout(function() {
a++
console.log(a);
}, 100)
打印结果
分析
由于js是单线程的并且for循环是不耗时代码,所以会先执行for循环语句,再执行定时器。
对于v8的事件循环机制的步骤
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
resolve()
})
.then(() => {
console.log(3);
setTimeout(() => {
console.log(4);
}, 0)
})
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 0)
}, 0)
console.log(7);
打印结果
分析
- 代码从上往下执行,先执行同步代码
console.log(1);,再执行Promise实例对象中的代码console.log(2);并且调用resolve()保证.then()能够触发,由于promise.then()是微任务,所以会存入到微任务队列中等待,setTimeout()(该定时器记为set1)是宏任务,会存到宏任务队列当中等待,再执行console.log(7); - 同步代码执行完成后,执行微任务
promise.then(),所以就会执行console.log(3);并且把.then()内部的setTimeout()(该定时器记为set2)存到宏任务队列当中等待。 - 微任务执行完成后,此时并没有页面需要渲染,所以执行宏任务set1,执行
console.log(5);,将set1内部的定时器(记为set3)存放到宏任务队列中等待,set1执行完毕。再执行set2,执行console.log(4);,最后执行set3,执行console.log(6);,所以打印的结果依次是:1 2 7 3 5 4 6。
async关键字
在函数前加上async关键字,默认让该函数返回一个Promise实例对象。
let data = null
function getData() {
setTimeout(() => {
data = [1, 2, 3]
}, 1000)
}
function another() {
console.log(data);
}
getData()
another()
打印结果
分析
虽然getData()先被调用,但是该数内部含有一个定时器,为耗时代码,所以会将该定时器放入宏任务队列中等待,继续执行下面的代码,调用another(),打印data,此时的data内的值并未改变,所以打印结果是null。
于是引入Promise实例对象
let data = null
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
data = [1, 2, 3]
resolve()
}, 1000)
})
}
function another() {
console.log(data);
}
getData().then(() => {
another()
})
打印结果
分析
因为定时器被执行后才会调用resolve(),才能执行.then()方法,在定时器内部已经执行了data = [1, 2, 3],于是再调用another()函数时打印结果就为 [1, 2, 3]。
引入async 和 await
let data = null
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
data = [1, 2, 3]
resolve()
}, 1000)
})
}
function another() {
return new Promise((resolve, reject) => {
setTimeout(() => {
data.push(4)
resolve()
}, 100)
})
}
function another2() {
console.log(data); // 1234
}
async function foo() {
// return new Promise((resolve, reject) => {})
await getData()
await another()
another2()
}
foo()
打印结果
分析
await getData()在getData()前面加上await,说明要等getData()执行完毕后才能继续执行下面的代码,getData()执行完毕后,data的值为[1, 2, 3],然后执行await another(),another()执行往data数组中插入一个数4,data的值为[ 1, 2, 3, 4 ],然后再调用another函数,打印出data的结果。
示例
console.log('script start');
async function async1() {
await async2() // await 会将后续的代码阻塞进微任务队列
console.log('async1 end');
}
async function async2() {
console.log('async2 end');
}
async1()
setTimeout(function() {
console.log('setTimeout');
}, 0)
new Promise(function(resolve, reject) {
console.log('promise');
resolve()
})
.then(() => {
console.log('then1');
})
.then(() => {
console.log('then2');
})
console.log('script end');
打印结果
分析 根据v8的事件循环机制,打印结果应该依次是
为什么与node打印结果不匹配呢?那是因为await 会将后续代码阻塞进微任务队列,
总结
自己手敲一敲,试着理清楚循环机制的思路吧。