Event loop即事件循环,指浏览器或者Node(JS运行的环境)用来解决JS单线程运行阻塞的问题的一种机制。
JS是单线程
执行,执行顺序:
-
先执行同步任务
-
再执行异步任务
- 在异步任务中分(宏任务和微任务)
先执行微任务,再执行宏任务
同步任务
for循环、while、new Promise、 闭包、console.log()
异步任务中的宏任务(macrotasks)和微任务(microtasks)
异步的操作,会有一个优先级的执行顺序,分别为宏任务和微任务。
- 宏任务(macrotasks)包含哪些
setTimeout
, setInterval
, setImmediate
, I/O(Ajax操作)
, UI rendering
- 微任务(macrotasks)包含哪些
process.nextTick
,Promise
,promise.then
,MutationObserver
,Promise.resolve().then
异步任务中的执行顺序优先级
:
process.nextTick > promise.then > setTimeout > setImmediate
- 如何区分宏任务和微任务呢
宏任务本质:参与了事件循环的任务
。回到 Chromium 中,需要处理的消息主要分成了三类:
- Chromium 自定义消息
- Socket 或者文件等 IO 消息
- UI 相关的消息
1.与平台无关的消息,例如 setTimeout 的定时器就是属于这个
2.Chromium 的 IO 操作是基于 libevent 实现,它本身也是一个事件驱动的库
3.UI 相关的其实属于 blink 渲染引擎过来的消息,例如各种 DOM 的事件 其实与 JavaScript 的引擎无关,都是在 Chromium 实现的。
微任务本质:直接在 Javascript 引擎中执行的,没有参与事件循环的任务。
- 是个内存回收的清理任务,使用过 Java 的童鞋应该都很熟悉,只是在 JavaScript 这是V8内部调用的
- 就是普通的回调,MutationObserver 也是这一类
- Callable
- 包括 Fullfiled 和 Rejected 也就是 Promise 的完成和失败
- Thenable 对象的处理任务
哪些情况下会发生异步
-
回调函数,这个很常见,很多内置函数都支持接收回调函数来异步代码
-
事件监听,DOM操作, click事件等都是异步的
-
订阅与发布,这个常见是在 angular和 vue中,用emit来发布事件,经常用于父子组件交互
-
promise是es6新增的特性,能通过resolve和reject来执行异步操作,经常与 async 、await配合使用。
JS的执行机制
- 开始,任务先进入 Call Stack(调用栈)
- 同步任务直接在栈中等待被执行,异步任务从 Call Stack 移入到 Event Table(事件表格) 注册
- 当对应的事件触发(或延迟到指定时间),Event Table 会将事件回调函数移入 Event Queue (事件队列)等待
- 当 Call Stack 中没有任务,就从 Event Queue 中拿出一个任务放入 Call Stack
以上三步循环执行,这就是event loop(事件循环)。
event loop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。
经典代码执行顺序
- for、while问题
console.log('A')
while(true){}
console.log('B')
答案:A
解析:代码从上往下执行
,先打印 A,然后 while 循环,因为条件一直是 true,所以会进入死循环。while 不执行完就不会执行到第三行。
console.log('A');
setTimeout(function(){
console.log('B')
}, 0);
while(1){}
答案:A
因为异步任务需要等同步任务执行完之后才执行,while 进入了死循环,所以不会打印 B。
- 经典闭包问题
for(var i=0; i<4; i++){
setTimeout(function(){
console.log(i)
}, 0)
}
答案:4个4
解析:这题主要考察异步任务放入任务队列的时机。当执行到 setTimeout即定时器时,并不会马上把这个异步任务放入任务队列,而是等时间到了之后才放入。然后等执行栈中的同步任务执行完毕后,再从任务队列中依次取出任务执行。
for 循环是同步任务,会先执行完循环,此时 i 的值是 4。4ms后 console.log(i)
被依次放入任务队列,此时如果执行栈中没有同步任务了,就从任务队列中依次取出任务,所以打印出 4 个 4。
如何改为输出的结果为1,2,3,4??
// 方法1:把 var 换成 let
//let是ES6新增的一个变量声明方式,拥有块级作用域;
for(let i= 0; i< 4; i++){
setTimeout(function(){
console.log(i)
}, 0)
}
// 方法2:加闭包
//函数内部可以访问外部的变量,外部却访问不了里边的;
//i 以函数参数形式传递给内层函数
for(var i= 0; i< 4; i++){
(function(i){
setTimeout(function(){
console.log(i)
}, 0)
})(i)
}
// 方法3:使用立即执行函数
for(let i= 0; i< 4; i++){
var a = function(){
var j = i;
setTimeout(function(){
console.log(j)
}, 0)
}
a();
}
- new promise(同步任务)
setTimeout(function(){
console.log(1) // 4
});
new Promise(function(resolve){
console.log(2); // 1
for(var i = 0; i < 10000; i++){
i == 9999 && resolve();
}
}).then(function(){
console.log(3) // 3
});
console.log(4) //2
答案:2,4,3,1
解析:
1.setTimeout是异步,且是宏函数,放到宏函数队列中;
2.new Promise是同步任务,直接执行,打印2,并执行for循环
3.promise.then是微任务,放到微任务队列中;
4.console.log(4)同步任务,直接执行,打印4;
5.此时主线程任务执行完毕,检查微任务队列中,有promise.then,执行微任务,打印3
6.微任务执行完毕,第一次循环结束;从宏任务队列中取出第一个宏任务到主线程执行,打印1;
7.结果:2,4,3,1
- 函数调用、new Promise 综合题
function add(x, y) {
console.log(1)
setTimeout1(function() { // timer1
console.log(2)
}, 1000)
}
add(); // 调用函数
setTimeout2(function() { // timer2
console.log(3)
})
new Promise(function(resolve) {
console.log(4)
setTimeout3(function() { // timer3
console.log(5)
}, 100)
for(var i = 0; i < 100; i++) {
i == 99 && resolve()
}
}).then(function() {
setTimeout4(function() { // timer4
console.log(6)
}, 0)
console.log(7)
})
console.log(8)
答案:1,4,8,7,3,6,5,2
解析:
1.add()是同步任务,直接执行,打印1;
2.setTimeout1是异步任务且宏函数,记做timer1放到宏函数队列;
3.setTimeout2是异步任务且宏函数,记做timer2放到宏函数队列;
4.new Promise是同步任务,直接执行,打印4;
5.setTimeout3是异步任务且宏函数,记做timer3放到宏函数队列;
6.Promise里面的for循环,同步任务,执行代码;
7.Promise.then是微任务,放到微任务队列;
8.console.log(8)是同步任务,直接执行,打印8;
9.此时主线程任务执行完毕,检查微任务队列中,有Promise.then,执行微任务,发现有setTimeout4是异步任务且宏函数,记做timer4放到宏函数队列;
10.微任务队列中的console.log(7)是同步任务,直接执行,打印7;
11.微任务执行完毕,第一次循环结束;
12.检查宏任务Event Table,里面有timer1、timer2、timer3、timer4,四个定时器宏任务,按照定时器延迟时间得到可以执行的顺序,即Event Queue:timer2、timer4、timer3、timer1,取出排在第一个的timer2;
13.取出timer2执行,console.log(3)同步任务,直接执行,打印3;
14.没有微任务,第二次Event Loop结束;
15.取出timer4执行,console.log(6)同步任务,直接执行,打印6;
16.没有微任务,第三次Event Loop结束;
17.取出timer3执行,console.log(5)同步任务,直接执行,打印5;
18.没有微任务,第四次Event Loop结束;
19.取出timer1执行,console.log(2)同步任务,直接执行,打印2;
20.没有微任务,也没有宏任务,第五次Event Loop结束;
21.结果:1,4,8,7,3,6,5,2
Promise 状态一旦改变,无法再发生变更。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
reject('error')
}, 1000)
})
promise.then((res)=>{
console.log(res)
},(err)=>{
console.log(err)
})
输出结果:success
Promise的then方法的参数期望是函数,传入非函数则会发生值穿透。
// 第一种情况
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log) // 1
// 第二种情况
Promise方法链通过return传值,没有return就只是相互独立的任务而已。
Promise.resolve(1)
.then(function(){ return 2})
.then(function(){return Promise.resolve(3)})
.then(console.log) // 3
promise.then.then的情况
注意:then方法会返回一个新的Promise,也就是return new Promise.
new Promise((resolve,reject)=>{
console.log(1)
resolve()
}).then(()=>{
console.log(2)
}).then(()=>{
console.log(3)
})
// 可修改为
const p1 = new Promise((resolve,reject)=>{
console.log(1)
resolve()
}).then(()=>{
console.log(2)
})
p1.then(()=>{
consoel.log(3)
})
// 1 2 3
promise中带有return
上一个then方法会自动返回一个新的Promise,相当于return new Promise,但是如果手动写了return Promise,那return的就是手动写的这个Promise
new Promise((resolve, reject) => {
console.log(1)
resolve()
}).then(() => {
console.log(2)
// 多了个return
return new Promise((resolve, reject) => {
console.log(3)
resolve()
}).then(() => {
console.log(4)
}).then(() => { // 相当于return了这个then的执行返回Promise
console.log(5)
})
}).then(() => {
console.log(6)
})
上面的可以解析为:
new Promise((resolve, reject) => {
console.log(1) // 同步
resolve()
}).then(() => { // 异步:微任务 then1
console.log(2) // then1 中的 同步
new Promise((resolve, reject) => {
console.log(3) // then1 中的 同步
resolve()
}).then(() => { // 异步:微任务 then2
console.log(4)
}).then(() => { // 异步:微任务 then3
console.log(5)
}).then(() => { // 异步:微任务 then4
console.log(6)
})
})
// 1 2 3 4 5 6
全局变量、局部变量
var a = 100
function b = {
console.log(a)
a = 10
console.log(a)
}
b()
console.log(a)
答案:100,10,10
解析:因为在b函数中 定义变量,去掉var,就是全局变量。
当 Event Loop 遇到 async/await
async/await 仅仅是生成器的语法糖,只要把它转换成 Promise 的形式即可。下面这段代码是 async/await 函数的经典形式。
async function foo() {
// await 前面的代码 同步
await bar();
// await 后面的代码 异步
}
async function bar() {
// do something...
}
foo();
其中 await 前面的代码是同步的,调用此函数时会直接执行;
而 await bar();这句可以被转换成Promise.resolve(bar());await 后面的代码 则会被放到 Promise 的 then() 方法里。
当遇到await时,会阻塞函数内部处于它后面的代码,去执行该函数外部的同步代码,当外部同步代码执行完毕,再回到该函数内部执行剩余的代码, 并且当await执行完毕之后,会先处理微任务队列的代码
转化为:
function foo() {
// await 前面的代码
Promise.resolve(bar()).then(() => {
// await 后面的代码
});
}
function bar() {
// do something...
}
foo();
function async1() {
console.log('async1 start'); // 2
Promise.resolve(async2()).then(() => {
console.log('async1 end'); // 6
});
}
function async2() {
console.log('async2'); // 3
}
console.log('script start'); // 1
setTimeout(function() {
console.log('settimeout'); // 8
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1'); // 4
resolve();
}).then(function() {
console.log('promise2'); // 7
});
console.log('script end'); // 5
总结
由于javascript是一种单线程语言,为了防止主线程阻塞,javascript就有了同步和异步的概念。
同步:
如果在一个函数返回的时候,调用者就能够得到预期结果,那么这个函数就是同步的。
异步:
如果在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。
JavaScript的执行过程是单线程的,所有的任务可以看做存放在两个队列中:执行队列和事件队列。
执行队列里面是所有同步代码的任务
,事件队列里面是所有异步代码的宏任务,而我们的微任务,是处在两个队列之间。
当JavaScript执行时,优先执行完所有同步代码,遇到对应的异步代码,就会根据其任务类型存到对应队列(宏任务放入事件队列,微任务放入执行队列之后,事件队列之前);当执行完同步代码之后,就会执行位于执行队列和事件队列之间的微任务,然后再执行事件队列中的宏任务。
易忘点
setTimeout(fn,0):将回调函数fn立刻插入消息队列,等待执行,而不是立即执行。
setTimeout(fn,0) 是指当主线程任务完成、所有微任务也完成的情况下就会立即执行。但是如果队列中还有setTimeout((){}),就先执行setTimeout((){}),后执行setTimeout((){},0)
如果promise中并没有resolve或reject。promise.then就不会执行,它只有在被改变了状态之后才会执行。