折磨人的Promise——你还能给我整什么新花样

本文详细解读了Promise在异步任务中的执行机制,通过实例分析了微任务、宏任务和同步任务的执行顺序,帮助理解Promise的then回调处理过程和状态转换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写在前面

可以说现在是promise遍天下,谁还没写过promise呢!

我们都知道JS分为同步任务和异步任务,而异步任务又分为宏任务和微任务。

其中promise就是我们所说的微任务中的一种。本文只探讨promise的执行机制,希望你不再为它的一些行为所困扰。

先通过几个小案例来看一下,它们是否符合你的预期呢?

经典案例

案例一:

setTimeout(() => {
  console.log(1)
})
new Promise((rs, rj) => {
  console.log(2)
  rs()
}).then(() => {
  console.log(3)
}).then(() => {
  console.log(4)
})
Promise.resolve()
.then(() => {
  console.log(5)
})
console.log(6)

输出结果顺序:2,6,3,5,4,1

案例二:

let p1 = new Promise((rs, rj) => {
  console.log(1)
  setTimeout(() => {
    rs()
  })
}).then(() => {
  console.log(2)
}).then(() => {
  console.log(3)
})
setTimeout(() => {
  console.log(4)
})
Promise.resolve()
.then(() => {
  console.log(5)
})
p1.then(() => {
  console.log(6)
})
console.log(7)

输出结果顺序:1,7,5,2,3,6,4

案例三:

Promise.resolve(1)
.then(2)
.then(
  Promise.resolve(3)
)
.then(
  Promise.resolve(4, console.log(5))
  .then(r1 => {
    console.log(r1)
  })
)
.then((r2 => {
  console.log(r2)
})())
.then(console.log)
console.log(6)

输出结果顺序:5,undefined,6,4,1

结果说明

由案例一能够看出先执行同步任务2,6,再执行微任务3,5,4,最后执行宏任务1。

其实js中所有代码的执行,都遵循这个顺序,同步任务 -> 微任务 -> 宏任务。只不过在实际运行过程中将任务推入执行队列的顺序不同,而产生了各种不同的效果。

由案例二能够看出1,7都是同步任务先执行,再执行微任务5,然后开启下次事件循环,执行第一个宏任务,然后在该次事件循环结束之前执行微任务2,3,6,最后开启下次事件循环执行宏任务4。

其实js中所有的事件循环都可以看成是一次次的宏任务,也就是说宏任务总是在下次事件循环的开始后执行,微任务总是在本次事件循环阶段的结束前执行,如果在本次事件循环完成之前一直追加微任务,那么就会不断的执行微任务队列,直到清空,而本次事件循环添加的微任务,会在之后的事件循环中执行。

由案例三能够看出Promise.resolve属于同步任务,then中的参数会作为表达式进行求值之后再放入微任务队列或者缓存中,对于then参数为非函数的表达式不会作为结果传递给下一个then。

其实js中函数在进行参数传递的时候,会先进行求值,然后再以值的形式传入调用的函数。因此会先执行同步任务的5,undefined(第5行代码),6,然后再执行微任务4,1(第6行代码)。

原理分析

由于Promise存在三种状态pending、fulfilled和rejected。其中pending为等待状态,将来可能发生改变,而以微任务的执行顺序来说fulfilled和rejected都是同样的机制,因此下文均以fulfilled状态来讨论。

抛开同步任务最先按顺序执行不说,代码每执行到异步任务(宏任务和微任务)的时候,都是按照入栈出栈的操作,即按照先入先出的规则来调配的。

我们现在清楚几点概念:

  1. new Promise是同步任务
  2. Promise.resolve是同步任务
  3. then参数的表达式求值是同步任务
  4. then参数为函数才会被推入微任务队列
  5. 只有前一个then执行完毕,下一个then才会被推入微任务队列,等待执行
  6. then中如果返回值为promise,那么会分为两次推入微任务队列(可以理解为返回实际结果之前又执行了两个子微任务。第一次子微任务把return后面的结果以微任务形式推入执行队列,第二次把第一次子微任务得到的结果用then再次推入执行队列以供将来把值返回给主微任务)

我们用几个案例来说明上述情况:

//new Promise中的回调函数以同步方式执行
//只有状态为fulfilled才会将接下来的任务推入微任务队列
new Promise((rs, rj) => {
  console.log(1)
  rs()
})
console.log(2)
//输出1,2
//Promise.resolve中的参数表达式是以同步的方式进行求值
//只不过该状态的完成永远为fulfilled
//而Promise.reject的完成状态永远为rejected
let a = 1
Promise.resolve(a = 1 + 2)
console.log(a)
//输出3
/* 可以把 Promise.resolve(value)
等价看成是 new Promise((rs,rj) => {
  rs(value)
}) */
//会以同步的方式扫描每一个then的参数,并将表达式求值
//只有上一个状态为fulfilled才会将该then的回调函数放入微任务队列
//否则将保存调用关系到缓存堆中,以供将来执行
new Promise((rs, rj) => {
  rs(1)
}).then(() => {
  return 2
}).then(((r) => {
  console.log(r)
  return 3
})(4))
console.log(5)
//输出4,5
//只要then的参数表达式求值结果为函数,那么就会被当做待执行的回调
let a = null
new Promise((rs, rj) => {
  rs(5)
}).then((a = 1,(r) => {
  console.log(r)
  return 2
})).then((() => {
  return () => {
    console.log(3)
  }
})())
console.log(a)
//输出1,5,3
//第8行并不是打印一个Promise函数
//而是等待上一个then返回值的状态完成之后resolve回来的值
new Promise((rs, rj) => {
  rs(1)
}).then(() => {
  return myP()
}).then((r) => {
  console.log(r)
})
console.log(2)
function myP() {
  return new Promise((rs, rj) => {
    console.log(3)
    setTimeout(() => {
      rs(4)
    },1000)
  })
}
//输出2,3,4(其中4是一秒之后才打印的)
//当then回调中遇到return一个Promise时
//会执行两次子微任务,然后再执行后面的then回调
new Promise((rs, rj) => {
  rs(1)
}).then(() => {
  console.log(2)
  //输出同步执行的8之后,会把结果第一次以子微任务的形式(记为P1)推入微任务队列
  //输出3之后,会执行P1这个微任务,然后再把结果用then(记为T1)推入微任务队列
  //输出4之后,会执行T1这个微任务,再把后面的主微任务的then推入微任务队列
  //这样输出5之后就会输出9
  return myP()
}).then((r) => {
  console.log(r)
})
new Promise((rs, rj) => {
  rs(3)
}).then((r) => {
  console.log(r)
  return 4
}).then((r) => {
  console.log(r)
  return 5
}).then((r) => {
  console.log(r)
  return 6
}).then((r) => {
  console.log(r)
})
console.log(7)
function myP() {
  return new Promise((rs, rj) => {
    console.log(8)
    rs(9)
  })
}
//输出7,2,8,3,4,5,9,6

因此,我们现在可以对上面的三个案例进行分析,很容易得出它们的执行结果:(我们约定宏任务队列用macroTask来表示,微任务队列用microTask来表示,缓存堆用cacheStack来表示。)

在这里插入图片描述

第一段属于宏任务,直接推入宏任务队列,因此macroTask:[第一段]

第二段属于同步任务,直接执行输出2,遇到rs状态变为fulfilled,将第三段推入微任务队列,因此microTask:[第三段]

由于第三段还未执行,处于pending状态,所以将第四段与第三段建立关系之后,推入缓存堆

第五段属于同步任务,并且直接处于fulfilled状态,将第六段推入微任务队列,因此microTask:[第三段,第六段]

第七段属于同步任务,直接输出6,到此同步任务执行结束

执行微任务队列,执行第三段输出3,状态变为fulfilled,将第四段推入微任务队列,因此microTask:[第六段,第四段]

继续执行微任务队列,执行第六段输出5,因此microTask:[第四段]

继续执行微任务队列,执行第四段输出4,因此microTask:[]

微任务执行完毕,本次事件循环结束,进入下一个事件循环

执行宏任务,执行第一段输出1,因此macroTask:[]

到此所有任务队列执行完毕。

在这里插入图片描述

第一段属于同步任务,直接执行输出1

第二段属于宏任务,直接推入宏任务队列,因此macroTask:[第二段]

由于未执行rs,当前promise处于pending状态,所以将第三段与第一段建立关系之后,推入缓存堆,因此cacheStack:{第一段→第三段}

同样由于第三段还未执行,处于pending状态,所以将第四段与第三段建立关系之后,推入缓存堆,因此cacheStack:{第一段→第三段,第三段→第四段}

第五段属于宏任务,直接推入宏任务队列,因此macroTask:[第二段,第五段]

第六段属于同步任务,并且直接处于fulfilled状态,将第七段推入微任务队列,因此microTask:[第七段]

第八段属于微任务,由于p1处于pending状态,所以将第八段与第四段建立关系之后,推入缓存堆,因此cacheStack:{第一段→第三段,第三段→第四段,第四段→第八段}

第九段属于同步任务,直接执行输出7,到此同步任务执行结束

执行微任务队列,执行第七段输出5,状态变为fulfilled,因此microTask:[]

微任务执行完毕,本次事件循环结束,进入下一个事件循环

执行宏任务,执行第二段,因此macroTask:[第五段],并且将第一段状态变为fulfilled,将第三段从缓存堆中取出并推入微任务队列,因此microTask:[第三段]

执行微任务,执行第三段输出2,状态变为fulfilled,将第四段从缓存堆中取出并推入微任务队列,因此microTask:[第四段]

执行微任务,执行第四段输出3,状态变为fulfilled,将第八段从缓存堆中取出并推入微任务队列,因此microTask:[第八段]

执行微任务,执行第八段输出6,因此microTask:[]

微任务执行完毕,本次事件循环结束,进入下一个事件循环

执行宏任务,执行第五段输出4,因此macroTask:[]

到此所有任务队列执行完毕。

在这里插入图片描述

第一段属于同步任务,并且直接处于fulfilled状态

第二段中then的参数为数值2,不是函数,因此只进行求值

第三段中then的参数为Promise对象,不是函数,因此只进行求值

直接执行第四段的同步任务代码

第五段中then的参数同样为Promise对象,不是函数,因为只进行求值

直接执行第六段代码,由于Promise.resolve属于同步任务,并且会对参数先求值再进行传递,因此直接输出5,状态变为fulfilled,将第七段推入微任务队列,因此microTask:[第七段]

第八段中then的参数求值之后为立即执行函数返回的undefined,不是函数,由于没有传递参数r2,因此直接输出undefined

第九段由于前面的第一段状态为fulfilled,所以直接推入微任务队列,因此microTask:[第七段,第九段]

第十段属于同步任务,因此直接执行输出6,到此同步任务执行结束

执行微任务队列,执行第七段,由于第六段返回的值为4,所以输出4,因此microTask:[第九段]

继续执行微任务队列,执行第九段,由于第一段返回的值为1,所以输出1,因此microTask:[]

到此所有任务队列执行完毕。

好了,上面的三个案例已经全部讲解完毕了,只要跟着读一遍,就会感到非常清晰了。

在这里插入图片描述

写在最后

Promise的出现为大家带来了方便,解决了很多的麻烦事,但是也为大家带来了一些困惑,尤其是执行时机上面的疑问。不过我们在日常开发中不必过于纠结,除了同步任务之外,我们尽量不要在宏任务或者微任务上面有任何执行顺序方面的假设,都把他们当做是异步任务就可以了,尤其generator与await的出现,我们一般都是多种相结合来实现功能需求的。

希望对你有所帮助,做一个快乐的码农~[吐舌]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值