Event loop

 

 

 在上图中可以看到,setTimeout这类异步接口实际上不在JS引擎中,而是由浏览器中的Web(图中的V8是chrome中的JS引擎,safari、firefox则是各自的引擎,参考《主流浏览器内核及JS引擎》)

setTimeout(() => 
  console.log(1)
}, 0)

console.log(0)

执行的过程:

  • 然后「JS引擎线程」执行了console.log(0),控制台中输出了0

  • 浏览器中的「定时器线程」接管该定时任务,当定时任务超时后,将回调函数即console.log(1)塞入到宏任务队列中,等待调度,如图中「2」

  • 「event loop线程」检查JS执行栈是否为空,如果是,则将宏任务队列中的任务塞入JS执行栈中,如图中「8」

  • 「JS引擎线程」处理执行栈中的console.log(1)代码,控制台中输出1

从这个流程可以看出,无论超时设置的多短,setTimeout中的回调函数必须等到下一次事件循环才能被处理,这就不难理解为什么0会在1之前输出了

 

promise.then函数

Promise.resolve().then(()=>{
   console.log(2) 
})

console.log(0)

执行的过程:

  • 「JS引擎线程」运行JS执行栈中执行如上代码,由于Promise首先执行了resolve函数变为终结状态,因此立即执行promise.then方法,此方法实际上是创建了一个微任务,并将console.log(2)塞入微任务队列中,如图「5」所示。

  • 然后「JS引擎线程」执行了console.log(0),控制台中输出了0

  • 「event loop线程」检查JS执行栈是否为空,如果是,则将微任务队列中的任务塞入JS执行栈中,如图中「8」

  • 「JS引擎线程」处理执行栈中的console.log(2)代码,控制台中输出2

任务分类

JS事件循环中的任务分为两大类

  • 宏任务,即task

  • 微任务,即microTask,也称job

对于浏览器环境和Node环境,提供的接口会有些许差别

宏任务接口

接口浏览器Node
I/O操作
setTimeout
setInterval
setImmediate
requestAnimationFrame

微任务接口

接口浏览器Node
process.nextTick
promise.then/catch/finally
MutationObserver

任务优先级

  • 整体上,微任务的处理优先于宏任务。准确的说是微任务会在本此事件循环结束前处理,宏任务会在下次事件循环开始时处理。

  • 在Node环境中,process.nextTick产生的微任务优先级最高,会被插到微任务队列的最前端优先处理;而setImmediate产生的宏任务优先级最高,会在所有宏任务之前执行

 

链式调用

通过上面的例子,Promise的基本执行顺序应该能清楚了,下面再看一个更复杂的问题,即当Promise.then链式调用时,代码是如何运行的。

问题来源于这片文章:《从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节》

Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
}).then((res) => {
    console.log(res)
})

Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
})

在看这个复杂的问题前,先看两个关于Promise.resolve和Promise.then的简单例子

Promise.resolve()

Promise.resolve是一个静态方法,它实际上是创建了一个立即resolve的Promise对象,以下两段代码是等同的

Promise.resolve(0)

// 等同于
new Promise((resolve, reject) => {
  resolve(0)
})

所以,

Promise.resolve(0).then(data => console.log(data))

这段代码的执行过程:

  • 创建一个Promise对象,立即将其状态改变为fulfilled

  • 执行promise.then方法,由于此时的promise状态是fulfilled,会直接将其后的成功回调函数塞入微任务队列,等待调度

Promise.then()

 我们知道,then方法会返回一个新的Promise对象,从而提供了链式调用的特性。来看这个例子:

Promise.resolve(0).then(data => {
  console.log(data)
  return 1
}).then(data => {
  console.log(data)
})

我们省略细节,简单描述这个事件循环的过程

  • 第一个then创建了一个微任务

  • 执行第一个微任务,控制台输出0,并且返回1,返回的1将作为下一次then的成功回调函数的入参

  • 第二个then创建一个新的微任务

  • 执行第二个微任务,控制台输出1

可以看到,当then返回一个普通对象时,会简单的将此对象作为入参交给下一次then调用。但是,如果当then返回一个Promise对象时会发生什么?

Promise.resolve(0).then(data => {
  console.log(data)
  return Promise.resolve(1)
}).then(data => {
  console.log(data)
})

在第一个then中,我们返回了一个fulfilled状态的Promise对象,如果还是按照普通对象的处理逻辑,直接把该Promise对象作为入参交给下一个then函数,那么上述代码的输出会变成

0
Promise {<fulfilled>: 1}

显然,这不是我们预期的结果。我们希望的是将Promise.resolve(1)执行完毕后,将其resolve的结果(例子里就是1)交给下一个then来使用。所以,实际当then方法中返回一个Promise对象时,会先执行这个Promise对象,然后再处理其后的then方法。为了理解这个机制,可以简单的理解为,在两个then函数中,插入了一个then函数

// 简化理解代码执行顺序,实际处理过程中会多塞一次微任务队列,后面会谈到
Promise.resolve(0).then(data => {
  console.log(data)
}).then(data => {
    return 1
}).then(data => {
  console.log(data)
})

例子中代码的执行结果是

0
1

实际上,为了处理then方法中返回的Promise对象,机制要比上面的简化写法复杂。

  • V8首先创建了一个 NewPromiseResolveThenableJobTask类型的微任务,然后将此任务放入微任务队列中等待处理(微任务+1)

  • 当上面的微任务被调度,处理NewPromiseResolveThenableJobTask时,实际就是调用了Promise.then方法,此时又创建了一个微任务(微任务+1)

  • 所以,实际上需要两次微任务才能把这个Promise对象转变为fulfilled状态

综合分析

有了以上基础,我们再回到那个复杂的问题

// PromiseA
Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
}).then((res) => {
    console.log(res)
})

// PromiseB
Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
})

 

// 1.创建一个状态为fulfilled的Promise
const PromiseA = Promise.resolve()

// 2.由于PromiseA的状态为fulfilled,其后then函数中的成功回调会直接放入微任务中等待处理,PromiseA1的状态为pending
const PromiseA1 = PromiseA.then(() => {
    console.log(0);
    return Promise.resolve(4);
})

// 3.由于PromiseA1的状态为pending,其后then函数会被放入成功回调队列中,等待PromiseA1的状态变为fulfilled时再处理
const PromiseA2 = PromiseA1.then((res) => {
    console.log(res)
})

// 4.创建一个状态为fulfilled的Promise
const PromiseB = Promise.resolve()

// 5.由于PromiseB的状态为fulfilled,其后then函数中的成功回调会直接放入微任务中等待处理,PromiseB1的状态为pending
const PromiseB1 = PromiseB.then(() => {
    console.log(1);
})

// 6.由于PromiseB1的状态为pending,其后then函数会被放入成功回调队列中,等待PromiseB1的状态变为fulfilled时再处理
const PromiseB2 = PromiseB1.then(() => {
    console.log(2);
})

// 7.同上,放入PromiseB2的成功回调队列中
const PromiseB3 = PromiseB2.then(() => {
    console.log(3);
})

// 8.同上,放入PromiseB3的成功回调队列中
const PromiseB4 = PromiseB3.then(() => {
    console.log(5);
})

上述代码会先按顺序同步执行,可以看到,此时的微任务队列中只有第2步和第5步创建的两个微任务任务

这两个任务按顺序依次执行,产生的结果为

  • 控制台依次输出0, 1

  • 由于PromiseA的成功回调中返回了一个Promise对象,根据我们之前的讨论,此操作需要两轮微任务才能将此PromiseA1的状态改变为fulfilled,所以PromiseA2需要两轮后才能执行

  • PromiseB1状态变为fulfilled,其后的then方法的成功回调会继续放入微任务中处理

 

所以,控制台的输出结果是

0
1
2
3
4
5
posted on 2021-05-27 11:45  京鸿一瞥  阅读(580)  评论(0)    收藏  举报