likes
comments
collection
share

面试官:手写下promise和then的源码

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

前言

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

面试官:手写下promise和then的源码

对于V8引擎来说,执行代码肯定是要优先考虑效率的,它不可能会等你1s执行完a函数再去执行b函数的,这就是V8引擎执行代码的情况,这种需要耗时执行的代码也叫做异步执行代码

异步的缺点

尽管JavaScript的异步编程极大地提高了Web应用的性能和用户体验,但它也带来了一些挑战和潜在的缺点。以下是一些主要的异步代码缺点:

  1. 复杂的控制流:异步代码往往比同步代码更难理解和调试,特别是当涉及到多个异步操作和嵌套的回调函数时。这种现象被称为“回调地狱”,会导致代码难以阅读和维护。
  2. 错误处理困难:在传统的异步回调模式中,错误处理通常较为繁琐,因为错误需要在每个回调中显式地捕获和处理。这可能导致错误处理代码分散在各个地方,增加了出错的可能性和调试的难度。
  3. 资源管理问题:在某些情况下,资源(如文件句柄、数据库连接)的生命周期管理和释放可能会变得更加复杂。如果异步操作没有正确处理资源释放,可能会导致资源泄露。
  4. 学习曲线:对于初学者来说,理解异步编程的概念和模式(如事件循环、Promise链、async/await语法)可能需要更多的时间和精力。
  5. 调试困难:由于异步代码的非线性执行特性,传统的断点调试技术可能无法直接应用于异步代码,使得调试过程更加复杂。
  6. 性能开销:虽然异步可以避免阻塞,但过多的异步调用和过度分割的任务也可能导致额外的性能开销,如上下文切换、事件调度等。

异步好就好在效率好,但是要是有的时候我就是希望能够先执行上面的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()
}

面试官:手写下promise和then的源码

想法很不错,但是为什么就是行不通呢?我生娃啦甚至没有输出,也就是说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()
// 我结婚啦 我生娃啦

面试官:手写下promise和then的源码

嗯~不错!这个方法行得通,把b函数放到a函数里面的定时器里去,这个方法其实确实行得通,感觉还是不错的,但是如果函数一多起来,函数嵌套函数,代码会非常糟糕,在es6之前,咱们们解决异步问题就是用这个法。

回调地狱

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

法二、promise

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

promise

Promisees6新增的构造函数,下面我看下它的写法:

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, "---");
  }
);

面试官:手写下promise和then的源码

我来解释一下这段代码

  1. function a() 定义了一个异步函数,它返回一个Promise对象。在这个Promise的executor函数中,setTimeout被用来模拟一个耗时1秒的异步操作。当这个操作完成时,它会通过调用resolve函数来解析Promise,传入值1作为结果。
  2. function b() 是一个简单的同步函数,打印出"b is ok"。
  3. a().then(...)中,我们注册了两个回调函数:一个是成功的回调,另一个是失败的回调。由于在a函数中调用了resolve而不是reject,所以只有成功的回调会被调用。
  4. 成功的回调函数接收Promise解析后的值(这里是1),然后,它调用了b函数,从而打印出"b is ok"。

promise和.then处理异步的方法我相信你已经了解了,你也知道peomise的强大了,我这里主要说的其实是他们的源码,他们是如何打造的,下面咱们来自己手搓一下promise和.then的源码。

源码

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的第二个参数

并且,还有一种情况,就是假设你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)
	}
}

这样就保证了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,其实第二个就是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状态为fulfilledthen中的回调直接执行,当then前面的promise状态为rejectedthen中的第二个回调直接执行,当then前面的promise状态为pendingthen中的回调需要被换存起来交给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是微任务啊,所以这个地方咱们需要模拟一下异步执行,但是模拟不了微任务,现在和面试的时候咱们就写个定时器宏任务代替下

并且执行的回调携带的参数就是上一个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(() => { 
						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
评论
请登录