面试官:手写下promise和then的源码
前言
promise在我们学习javaScript的过程中也是一个非常重要的知识点。在面试时promise也是一个常考的热点问题,基本上百分之百会被面试官问到,而且,面试官还会让你手写一个promise方法的源码,promise身上有许多方法,你都需要彻底搞明白。
在了解promise之前我们还需要来了解一下异步同步的概念
异步和同步
在JavaScript中,异步(Asynchronous)指的是不阻塞程序执行的操作。也就是说,当一个操作开始后,程序可以继续执行后续代码,而不必等待该操作完成。异步编程的主要目的是提高程序的效率和响应速度,特别是在处理耗时操作(如网络请求、文件读写、定时器等)时。
异步指的是同时执行代码,其实就是指的并发。它的反义词是同步,对于V8引擎来说,同步并不是我们我们想当然的同时执行代码,同步是指同步原来的顺序,每一行代码会按照它们出现的顺序依次执行,前一行代码必须完全执行完毕并返回控制权给调用者之后,下一行代码才会开始执行。
来举个例子:像我们常见的定时器就是一个异步函数
function a(){
setTimeout(()=>{
console.log('a is ok')
},1000)
}
function b(){
console.log('b is ok')
}
a()
b()
对于V8引擎来说,执行代码肯定是要优先考虑效率的,它不可能会等你1s执行完a函数再去执行b函数的,这就是V8引擎执行代码的情况,这种需要耗时执行的代码也叫做异步执行代码
异步的缺点
尽管JavaScript的异步编程极大地提高了Web应用的性能和用户体验,但它也带来了一些挑战和潜在的缺点。以下是一些主要的异步代码缺点:
- 复杂的控制流:异步代码往往比同步代码更难理解和调试,特别是当涉及到多个异步操作和嵌套的回调函数时。这种现象被称为“回调地狱”,会导致代码难以阅读和维护。
- 错误处理困难:在传统的异步回调模式中,错误处理通常较为繁琐,因为错误需要在每个回调中显式地捕获和处理。这可能导致错误处理代码分散在各个地方,增加了出错的可能性和调试的难度。
- 资源管理问题:在某些情况下,资源(如文件句柄、数据库连接)的生命周期管理和释放可能会变得更加复杂。如果异步操作没有正确处理资源释放,可能会导致资源泄露。
- 学习曲线:对于初学者来说,理解异步编程的概念和模式(如事件循环、Promise链、async/await语法)可能需要更多的时间和精力。
- 调试困难:由于异步代码的非线性执行特性,传统的断点调试技术可能无法直接应用于异步代码,使得调试过程更加复杂。
- 性能开销:虽然异步可以避免阻塞,但过多的异步调用和过度分割的任务也可能导致额外的性能开销,如上下文切换、事件调度等。
异步好就好在效率好,但是要是有的时候我就是希望能够先执行上面的a函数,再执行b函数。又或者我执行的那个函数需要在另一个函数之前执行完毕,那怎么办嘞
解决异步问题
像咱们最开始举的那个例子,我希望能够先打印出“a is ok”,一个想法,如果我把b函数增加一个时间,改成1.5s后执行,那这样是不是就可以改变顺序了…… 以后要是真这样解决异步的,那很可能就会挨老板一顿骂,人家好端端的函数你给人家添加一个时间,等待执行,又或者说,万一函数多了起来,这样设置岂不是很麻烦,因此不建议这样做。
法一、回调
那咱们又该如何解决这个问题呢?突然萌生这样一个想法我能否给个开关变量,执行完一个函数后,改变其布尔值,然后通过if判断,如果变了,我就再执行,想法不错,不妨试试看。这里咱们换个场景,正常逻辑先输出我结婚啦,后输出我生娃啦
且看下面函数,按道理异步执行得话会是先输出我生娃啦,后输出我结婚啦,显然是不符合逻辑的
let flag = false
// 开关变量
function a(){
setTimeout(()=>{
console.log('我结婚啦');
flag = true
},1000)
}
function b(){
setTimeout(()=>{
console.log('我生娃啦')
},500)
}
a()
if(flag){
b()
}
想法很不错,但是为什么就是行不通呢?我生娃啦甚至没有输出,也就是说b
这个函数根本就没有执行,代码并没有进入if体内,实际上这是因为,v8执行a()
的时候a
函数需要1s
开始执行,在这1s
内,v8
会继续往下走,并不会走a函数里面的东西,此时的flag
还是false
,因此没有进入if
体内,等你1s
后,flag
变了,你不可能让v8
掉头再来执行if
,显然这样是不行的!
那咱就是说把b
函数放到a
函数里面去执行总可以了吧
function a(){
setTimeout(()=>{
console.log('我结婚啦');
b()
},1000)
}
function b(){
setTimeout(()=>{
console.log('我生娃啦')
},500)
}
a()
// 我结婚啦 我生娃啦
嗯~不错!这个方法行得通,把b函数放到a函数里面的定时器里去,这个方法其实确实行得通,感觉还是不错的,但是如果函数一多起来,函数嵌套函数,代码会非常糟糕,在es6之前,咱们们解决异步问题就是用这个法。
回调地狱
刚刚咱们再异步代码的缺点中提到回调地狱,回调地狱指得是代码维护将会非常恐怖!一旦你用回调去嵌套个几十个函数,动其中一行代码,就会导致全部出问题,甚至有时候你根本无从得知哪个函数的锅,你需要一个一个去看。
法二、promise
es6
问世之前也就是2015年以前,大家都是用回调去解决异步的,es6
就新增了一个promise
方法,专门用来解决回调地狱的,或者说解决异步更优雅
promise
Promise
是es6
新增的构造函数,下面我看下它的写法:
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("a is ok");
resolve(1)
//reject(1)
}, 1000);
});
}
function b() {
console.log("b is ok");
}
a().then(
(res) => {
//console.log(res, "+++");
b();
},
(err) => {
console.log(err, "---");
}
);
我来解释一下这段代码
function a()
定义了一个异步函数,它返回一个Promise对象。在这个Promise的executor函数中,setTimeout
被用来模拟一个耗时1秒的异步操作。当这个操作完成时,它会通过调用resolve
函数来解析Promise,传入值1作为结果。function b()
是一个简单的同步函数,打印出"b is ok"。- 在
a().then(...)
中,我们注册了两个回调函数:一个是成功的回调,另一个是失败的回调。由于在a
函数中调用了resolve
而不是reject
,所以只有成功的回调会被调用。 - 成功的回调函数接收Promise解析后的值(这里是1),然后,它调用了
b
函数,从而打印出"b is ok"。
promise和.then处理异步的方法我相信你已经了解了,你也知道peomise的强大了,我这里主要说的其实是他们的源码,他们是如何打造的,下面咱们来自己手搓一下promise和.then的源码。
源码
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
,.then里面的第二个回调就等同于.catch 所以,.then里面一定会有两个形参 resolve
的调用会触发then
的第一个回调,因此我需要把then
的第一个参数挂在this
上,让resolve
函数去调用这个then
的第一个回调,原型上的then
里面的this
指向的实例对象,共用一个this
,并且then
里面的回调里面可能会有多个函数,需要遍历他逐个进行调用
为什么是多个回调,一个promise
实例后面是可以接受多个then
的,只要promise
的状态为fulfilled
,那么所有的then
回调都会执行,执行的参数此时就发挥作用了,给到then
的回调,catch
同理
class MyPromise {
constructor(executor) {
this.onFulfilledCallbacks = [] //装then里面的回调
this.onRejectedCallbacks = [] //装catch里面的回调
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
需要等待前面的promise
状态变更后才能执行then
里面的回调,因此一定是个异步
then
then的源码在这里是最难的,也往往是面试官考查的最多的一个知识点
1、默认返回一���promise
对象,状态为fulfilled
2、当then前面的promise
状态为fulfilled
,then
中的回调直接执行,当then
前面的promise
状态为rejected
,then
中的第二个回调直接执行,当then
前面的promise
状态为pending
,then
中的回调需要被换存起来交给resolve
或者reject
。
一个promise
对象后面是可以接多个then
,因此then
一定返回一个promise
。既然保证了后面的then
执行,因此then
返回的promise
状态一定是fulfilled
另外,还需要考虑一种情况 就是then
里面接的参数是回调,但是以防万一往里面加入参数或者其他东西,不是函数体的话,
因此这里我就用typeof将两个参数先判断下,如果不是函数体,那就自己赋值一个没有意义的函数体,之后就返回一个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前面的那个哥们已经彻底执行完了 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(() => {
try {
const result = onFulfilled(value)
resolve(result)
} catch (error) {
reject(error)
}
})
})
this.onRejectedCallbacks.push((reason) => {
setTimeout(() => {
try {
const result = onFulfilled(reason)
resolve(result)
} catch (error) {
reject(error)
}
})
})
}
})
return newPromise
}
}
总结
好啦,今天的分享就到这里了,主要给大家分享的就是promise和then源码的实现,这是promise的底层原理逻辑,在面试中也是经常会被面试官问道的问题。代码我放到gitee仓库啦,大家有需要的话可以自行看看 如果觉得本文对你有帮助的话可以点个免费的小赞赞嘛。谢谢! gitee.com/Luo-zhao-fa…
转载自:https://juejin.cn/post/7392783748046192703