JS Event loop(事件循环) Promise、执行顺序、代码题

1,868 阅读6分钟

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中,用on来监听事件,on来监听事件,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就不会执行,它只有在被改变了状态之后才会执行。