前言
promise
也是JavaScript
中一座大山
promise
是个面试热点问题,基本上百分百会被问到,并且是你每天都需要用上的方法,面试中,面试官会让你手写一个promise
方法源码,promise
身上所有的方法你都需要彻底搞明白
在介绍promise
之前我们需要先认识下异步
想看源码的小伙伴直接跳到后面源码那里
异步
异步指的是同时执行代码,异步的反义词是同步,每门语言都有异步的概念,对于v8引擎来说,同步并不是我们想当然的同时执行代码,同步是同步原来的顺序。其实异步就是指的并发
并发的意思就是同一时间干多件事情,像是我们的淘宝,每到双十一,它的并发就会达到上亿的量,这个时候的程序员一般都要加班,防止后端服务器崩溃
像我们常见的定时器就是一个异步函数
function a(){
setTimeout(function(){
console.log('菜做完了,上菜');
},1000)
}
// b函数对于v8来说,不耗时,太快了忽略不计
function b(){
console.log('我在看B站')
}
a()
b()
// 我在看B站
// 菜做完了,上菜
对于v8来说,执行代码是优先考虑效率的,不可能等你1s执行完函数a再去执行b,这就是一个默认情况,我们管这个叫做异步执行代码
异步缺点
异步好在效率很好,但是有时候我们就是希望能够先执行完上面的a函数,再执行b函数。又或者我执行的那个函数需要在另一个函数之前执行完毕,比如我谈恋爱,我肯定是谈恋爱->结婚->生子,这个顺序一定是这样的。
解决异步问题
像是最开始给到的那个例子,我希望能够先打印出“菜做完了,上菜”,我把它的时间给改了,改成1.5s,这样就可以改变顺序了……你以后要是真这样解决异步的,你可能马上被leader骂走,人家好端端的函数你给人家添加一个时间,等待执行,又或者说,万一函数多了起来,这样设置会很麻烦,因此这个方法不要考虑!
法一---回调
那我如何解决呢?我能否给个开关变量,执行完一个函数后,改变其布尔值,然后if判断,如果变了,我就再执行,不妨试试看。这里我们换个场景,正常逻辑先输出个人信息,后输出家人信息
且看下面函数,按道理异步执行得话会是先输出家人信息,后输出个人信息,显然是不符合逻辑的
let flag = false
// 开关变量
function a(){
setTimeout(()=>{
let age = 18
console.log('个人信息');
flag = true
},1000)
}
function b(){
setTimeout(()=>{
console.log('家人信息')
},500)
}
a()
if(flag){
b()
}
// 个人信息
想法不错,但是为何行不通?家人信息甚至没有输出,也就是说b
这个函数没有执行,代码并没有进入if体内,实际上这是因为,v8执行a()
的时候a
函数需要1s
开始执行,在这1s
内,v8
会继续往下走,此时的flag
还是false
,因此没有进入if
体内,等你1s
后,flag
变了,你不可能让v8
掉头再来执行if
,显然这样不行!
那我把b
函数放到a
函数里面去执行总可以了吧
function a(fn){
setTimeout(()=>{
let age = 18
console.log('个人信息');
fn()
},1000)
}
function b(){
setTimeout(()=>{
console.log('家人信息')
},500)
}
a(b)
// 个人信息 家人信息
嗯~不错!这个方法行得通,让a函数接收一个函数作为参数,然后再执行传进来的函数,感觉不错哈!这个方法其实确实行得通,但是如果函数一多起来,函数嵌套函数,代码会非常糟糕,在es6之前,我们解决异步就是这个方法,这个方法也有个名字叫做回调callback除了麻烦你们说这个方法还有啥缺点么,会导致内存泄漏吗,内存泄漏是闭包的缺点,这里可不是闭包噢,只是一个正常的执行代码过程,闭包长啥样啊,不懂闭包得话,请移步闭包。
回调地狱
回调地狱指得是代码维护将会非常恐怖!一旦你用回调去嵌套个几十个函数,动其中一行代码,就会导致全部出问题,有时候你根本无从得知哪个函数的锅,你需要一个一个去看。
法二---Promise
前面说到,es6
问世之前也就是2015年以前,大家都是用回调去解决异步的,es6
就新增了一个promise
方法,专门用来解决回调地狱的,或者说解决异步更优雅
Promise
Promise
是es6
新增的构造函数,下面我看下它的写法,这次我换个例子进行讲解
function xq() {
return new Promise((resolve,reject) => {
setTimeout(()=>{
console.log('我要相亲了!')
resolve('相亲成功')
},2000)
})
}
function marry(){
return new Promise((resolve, reject) => {
setTimeout(()=>{
console.log('我要结婚了!')
resolve('生不生?')
},1000)
})
}
function baby(){
setTimeout(() => {
console.log('生娃!')
},500)
}
xq().then((res) => {
marry().then((res) => {
console.log(res)
baby()
})
console.log(res)
})
// 输出如下
我要相亲了!
相亲成功
我要结婚了!
生不生?
生娃!
从这个例子就可以看出,每个除了最后一个,每个函数都需要返回出一个Promise
实例对象,并且函数体都写在了Promise
里面。最后输出时让then
去接收回调,then
也会返回一个Promise
,因此我们需要用后面函数返回的东西去覆盖它。这里你肯定发现了,这么写跟我用普通回调有啥不同,也是无限套娃!好吧,其实Promise
的正确写法是这样的
xq()
.then((res) => {
console.log(res)
return marry()
})
.then((res) => {
console.log(res)
baby()
})
好了,你已经知道Promise
很强大了,这个时候你肯定会很好奇怎么打造的,下面开始自己手搓一个promise
其实promise
源码并不难,难主要是难在then
,但是前几年面试官都喜欢问then
源码,导致大家都会then
源码了,现在面试官又喜欢问你race
,all
,finally
的源码……
源码
Promise
- 维护一个状态,
state
:pending
,fulfilled
,rejected
,目的是让promise
的状态一经改变无法修改,并且then
和catch
无法同时触发 - 内部的
resolve
函数会修改state
为0
,并触发then
中的回调,reject
同理
从用法上我们清楚,promise
接受一个回调,回调里面有两个参数,这个回调需要自己触发,所以要进行调用,并且这两个形参是个函数,这里我们用es6
的class
来写
class MyPromise {
constructor(executor) {
const resolve = () => {
}
const reject = () => {
}
executor(resolve, reject)
}
}
promise
里面内置了捕获错误的机制,用的catch或者then的第二个参数处理
promise
自身维护了一个状态state
,值分别为:pending
,fulfilled
,rejected
,默认值就是pending
,如果我们resolve
了,那么就会触发then
的第一个参数,如果reject
了,那么就是触发catch
或者then
的第二个参数
并且,假设你
resolve
和reject
都调用了,谁先谁执行,后面的不执行
class MyPromise {
constructor(executor) {
this.state = 'pending'
const resolve = () => {
if (this.state === 'pending') {
this.state = 'fulfilled'
}
}
const reject = () => {
if (this.state === 'pending') {
this.state = 'rejected'
}
}
executor(resolve, reject)
}
}
这样就保证了resolve
,reject
同时存在时谁先谁执行,必须先是默认状态开始,然后才能改状态,resolve
对应的fulfilled
,reject
对象rejected
,并且状态只能变更一次
并且,resolve
和reject
函数是可以传值的,分别传给then
和catch
的形参,参数都给个初始值undefined
,调用的时候进行赋值
class MyPromise {
constructor(executor) {
this.state = 'pending' // promise的默认状态
this.value = undefined // resolve的参数
this.reason = undefined // reject的参数
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
}
}
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
}
}
executor(resolve, reject)
}
}
另外,我们要清楚一点,无论我们是否resolve
,如果有then()
,那么then
一定会进行调用的,都写成执行的样子了,一定会调用,有无resolve
影响的是then
里面的回调,resolve
了,那么then
的第一个回调一定是执行的,catch
同理
好了,因此,resolve
后需要把then
的回调触发掉,reject
后需要把catch
的回调触发掉,既然需要动用then
,就先把then
的雏形先写下
then
是Promise
原型身上的方法,用class
写的话,就写在constructor
外面
class MyPromise {
constructor(executor) {
……
}
then() {
}
}
另外,then
是走两个回调的,一个对应的resolve
,一个对应的reject
,其实第二个就是catch
,resolve
的调用会触发then
的第一个回调,因此我需要把then
的第一个参数挂在this
上,让resolve
函数去调用这个then
的第一个回调,原型上的then
里面的this
指向的实例对象,共用一个this
,并且then
里面的回调里面可能会有多个函数,需要遍历他逐个进行调用
为何多个回调,一个
promise
实例后面是可以接受多个then
的,只要promise
的状态为fulfilled
,那么所有的then
回调都会执行,执行的参数此时就发挥作用了,给到then
的回调,catch
同理
class MyPromise {
constructor(executor) {
this.onFulfilledCallbacks = [] // 多个回调用数组来装
this.onRejectedCallbacks = []
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
this.onFulfilledCallbacks.forEach(cb => cb(value))
}
}
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reason
this.onRejectedCallbacks.forEach(cb => cb(reason))
}
}
}
then(onFulfilled, onRejected) {
// 两个参数回调都需要存起来分别供resolve和reject去调用
}
}
如何理解
then
是个异步微任务,首先then
需要等待前面的promise
状态变更后才能执行then
里面的回调,因此一定是个异步,可能不好解释为何是微任务,但是可以说一定不是宏任务,若是宏任务,then
的执行就会放到后面了
then
then
源码是最难的
- 默认返回一个
promise
对象,状态为fulfilled
- 当then前面的
promise
状态为fulfilled
,then
中的回调直接执行,当then
前面的promise
状态为rejected
,then
中的第二个回调直接执行,当then
前面的promise
状态为pending
,then
中的回调需要被换存起来交给resolve
或者reject
一个promise
对象后面是可以接多个then
,因此then
一定返回一个promise
。既然保证了后面的then
执行,因此then
返回的promise
状态一定是fulfilled
另外,then
里面接的参数是回调,但是以防万一有人往里面填参数,我们在源码中需要规避掉
因此这里我就将两个参数先判断下,如果不是函数体,那就自己赋值一个没有意义的函数体,之后就返回一个promise
class MyPromise {
……
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
const newPromise = new MyPromise((resolve, reject) => {
})
return newPromise
}
}
另外,then
里面的回调是可以继续返回一个promise
对象的,这个时候的promise
状态就可以把默认的fulfilled
状态覆盖掉。因此要进行一个状态判断,三种状态就是三种可能
如果状态为fulfilled
,那么就说明then
前面的promise
对象的状态瞬间变更完成了,需要立即执行then
里面的回调
按道理
promise
里面写的异步代码,需要把then
自己的回调存起来,等fulfilled
后再执行,但是这里一上来就是fulfilled
,就说明人家promise
里面写了个同步代码,就没必要把自己的回调存起来后再调用了另外你也不能直接执行回调,否则就是同步执行了,
then
是微任务啊~,其实官方这里的微任务很复杂,但是面试的时候完全可以写个定时器宏任务代替下并且执行的回调携带的参数就是上一个
promise
的resolve
的参数,为保证之后一连串的then
能正常执行,执行完后还需要resolve
下
如果状态为rejected
,那么就调用第二个回调,状态rejected
并不能影响后面then
的执行,因此还是resolve
如果状态为pending
,那么就说明前面的promise
实例因为异步的原因还没有转变状态,因此要把两种状态给存起来
class MyPromise {
……
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
const newPromise = new MyPromise((resolve, reject) => {
if (this.state == 'fulfilled') {
setTimeout(() => { // 模拟异步微任务
try {
const result = onFulfilled(this.value)
resolve(result) // 应该放result中的resolve中的参数
} catch (error) {
reject(error)
}
})
}
if (this.state === 'rejected') {
setTimeout(() => {
try {
const result = onRejected(this.reason)
resolve(result)
} catch (error) {
reject(error)
}
})
}
if (this.state === 'pending') { // 缓存then中的回调
this.onFulfilledCallbacks.push((value) => {
setTimeout(() => { // 保障将来onFulfilled在resolve中被调用时是个异步函数
try {
const result = onFulfilled(value)
resolve(result)
} catch (error) {
reject(error)
}
})
})
this.onRejectedCallbacks.push((reason) => {
setTimeout(() => { // 保障将来onFulfilled在resolve中被调用时是个异步函数
try {
const result = onFulfilled(reason)
resolve(result)
} catch (error) {
reject(error)
}
})
})
}
})
return newPromise
}
}
all
,race
其实都差不多
race
的用法如下
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('a');
resolve('OK')
}, 1000)
})
}
function b() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('b');
resolve('ok2')
}, 500)
})
}
Promise.race([a(), b()]).then((res) => {
console.log(res); // b ok2 a
})
race
接收数组,放的都是promise
对象,然后看这些promise
对象谁先resolve
出值,返回第一个resolve
的值给到后面的then
很好理解,race
就是比赛的意思,看谁先~
race
race
接在构造函数Promise
后面,因此他不是原型身上的方法,而是构造函数身上的方法,不被实例对象拿到,就需要用个static
关键字
class MyPromise {
……
static race(promises) { // 接收数组
return new MyPromise((resolve, reject) => {
// 判断数组中谁的状态先变更
for (let promise of promises) {
promise.then( // 能走这个逻辑一定是个promise对象,因此不用判断
(value) => {
resolve(value)
},
(reason) => { // 谁先reject,就用谁
reject(reason)
}
)
}
})
}
}
接着看下all
,其实then
和all
是考察得最多的
all
的逻辑是最后都执行完了再去返回一个promise
出来,同样的,all
也是挂在Promise
构造函数身上,接收的promise
是数组
all
对于all
来说,大家都是resolve
,那么最终就是fulfilled
,但凡有一个reject
,那么all
返回的就是rejected
,都好才好
并且all
返回的resolve
的参数,是数组,里面是所有的resolve
参数
class MyPromise {
……
static all(promises) {
return new MyPromise((resolve, reject) => {
let count = 0, arr = []
for (let i = 0; i < promises.length; i++) {
promises[i].then(
(value) => {
count++
arr[i] = value
if (count === promises.length) {
resolve(arr)
}
},
(reason) => { // 但凡有一个走这个回调,那么all就rejected
reject(reason)
}
)
}
})
}
}
any
相较于all
,它是反着来的,all
是只要有个坏的,那么就是坏的,any
只要有个好的,就是好的
any
class MyPromise {
……
static any(promises) {
return new MyPromise((resolve, reject) => {
let count = 0, errors = []
for (let i = 0; i < promises.length; i++) {
promises[i].then(
(value) => {
resolve(value)
},
(reason) => {
count++
errors[i] = reason
if (count === promises.length) {
reject(new AggregateError(errors))
}
}
)
}
})
}
}
finally
这个方法同then
和catch
一样,挂在原型身上,因此可以接在promise
实例后面,finally
不管前面的promise
实例返回成功失败,都会执行finally
里面的回调,有个很经典的面试题红绿灯就是用这个实现的
class MyPromise {
……
finally(cb) {
return this.then(
(value) => {
return Promise.resolve(cb()).then(() => value)
},
(reason) => {
return Promise.resolve(cb()).then(() => {
throw reason
})
}
)
}
}
allSettled
这个方法同样接收一个数组,里面所有的promise
都会返回一个结果,无论成功与否都会给到allSettled
,因此这个方法非常适合解决并发问题,无论哪个请求失败与否都不会造成阻塞,用all
得话,有一个失败就全部失败了
class MyPromise {
……
static allSettled(promises) {
return new MyPromise((resolve) => {
let res = []
let count = 0
function checkSettled () {
if (count === promises.length) {
resolve(res)
}
}
for(let i = 0; i < promises.lenght; i++) {
promises[i]
.then((value) => {
res[i] = { status: 'fulfilled', value }
})
.catch((reason) => {
res[i] = { status: 'rejected', reason }
})
.finally(() => {
count++
checkSettled()
})
}
})
}
}
最后
Promise
方法中,只有then
,catch
,finally
是挂在原型身上的,所以可以被实例对象拿到,其余的方法都是static
,挂在Promise
构造函数身上
这么多方法,其实也就是then
和all
考察的最多,看到最近的面经,all
被考的最多
如果你对春招感兴趣,可以加我的个人微信:
Dolphin_Fung
,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决
另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请”点赞+评论+收藏“一键三连,感谢支持!