likes
comments
collection
share

Promise从0到手写

作者站长头像
站长
· 阅读数 2

前言

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)
// 个人信息 家人信息

回调地狱

回调地狱指得是代码维护将会非常恐怖!一旦你用回调去嵌套个几十个函数,动其中一行代码,就会导致全部出问题,有时候你根本无从得知哪个函数的锅,你需要一个一个去看。

法二---Promise

前面说到,es6问世之前也就是2015年以前,大家都是用回调去解决异步的,es6就新增了一个promise方法,专门用来解决回调地狱的,或者说解决异步更优雅

Promise

Promisees6新增的构造函数,下面我看下它的写法,这次我换个例子进行讲解

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()
})

参考:ECMAScript 6 入门------阮一峰

好了,你已经知道Promise很强大了,这个时候你肯定会很好奇怎么打造的,下面开始自己手搓一个promise

其实promise源码并不难,难主要是难在then,但是前几年面试官都喜欢问then源码,导致大家都会then源码了,现在面试官又喜欢问你raceallfinally的源码……

源码

Promise

  1. 维护一个状态,statependingfulfilledrejected,目的是让promise的状态一经改变无法修改,并且thencatch无法同时触发
  2. 内部的resolve函数会修改state0,并触发then中的回调,reject同理

从用法上我们清楚,promise接受一个回调,回调里面有两个参数,这个回调需要自己触发,所以要进行调用,并且这两个形参是个函数,这里我们用es6class来写

class MyPromise {
	constructor(executor) {
		const resolve = () => {
		
		}
		const reject = () => {
		
		}
		executor(resolve, reject)
	}
}

promise里面内置了捕获错误的机制,用的catch或者then的第二个参数处理

promise自身维护了一个状态state,值分别为:pendingfulfilledrejected,默认值就是pending,如果我们resolve了,那么就会触发then的第一个参数,如果reject了,那么就是触发catch或者then的第二个参数

并且,假设你resolvereject都调用了,谁先谁执行,后面的不执行

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)
	}
}

这样就保证了resolvereject同时存在时谁先谁执行,必须先是默认状态开始,然后才能改状态,resolve对应的fulfilledreject对象rejected,并且状态只能变更一次

并且,resolvereject函数是可以传值的,分别传给thencatch的形参,参数都给个初始值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的雏形先写下

thenPromise原型身上的方法,用class写的话,就写在constructor外面

class MyPromise {
	constructor(executor) {
		……
	}
	then() {
	
	}
}

另外,then是走两个回调的,一个对应的resolve,一个对应的reject,其实第二个就是catchresolve的调用会触发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源码是最难的

  1. 默认返回一个promise对象,状态为fulfilled
  2. 当then前面的promise状态为fulfilledthen中的回调直接执行,当then前面的promise状态为rejectedthen中的第二个回调直接执行,当then前面的promise状态为pendingthen中的回调需要被换存起来交给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是微任务啊~,其实官方这里的微任务很复杂,但是面试的时候完全可以写个定时器宏任务代替下

并且执行的回调携带的参数就是上一个promiseresolve的参数,为保证之后一连串的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
	}
}

allrace其实都差不多

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,其实thenall是考察得最多的

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

这个方法同thencatch一样,挂在原型身上,因此可以接在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方法中,只有thencatchfinally是挂在原型身上的,所以可以被实例对象拿到,其余的方法都是static,挂在Promise构造函数身上

这么多方法,其实也就是thenall考察的最多,看到最近的面经,all被考的最多

如果你对春招感兴趣,可以加我的个人微信:Dolphin_Fung,我和我的小伙伴们有个面试群,可以进群讨论你面试过程中遇到的问题,我们一起解决

另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请”点赞+评论+收藏“一键三连,感谢支持!