前言
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,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决
另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请”点赞+评论+收藏“一键三连,感谢支持!