Promise源码实现
前言
自从ECMAScript 2015(es6)问世以来javascript就不仅仅是一个简单脚本语言了真正的成为了一种可以用于企业级开发的计算机高级程序设计语言,es6新增了很多东西,可以算的上对整个javascript语言进行了重构。其中最重要的之一当属promsie。此文章由于笔者水平有限,依然存在很多不足。只是实现了promise的基本功能,也没有完全按照Promise/A+规范来实现。但是可以为我们提供一个程序设计的思路,开拓我们的思维。在阅读此文章之前希望您对promise的基本用法有所了解,并且掌握发布订阅设计模式,递归,以及函数式编程等思想。闲言少序让我们开始吧!
代码实现
1.基本架构
//1. 3种状态 pending fulfilled rejected 三种状态//2.链式调用 then//3.executon 执行器函数 这个函数是同步执行的//此时此刻executon函数是实际上是实际参数,我们在实例化promise的时候传进去的resolve和reject才是形式参数 :重点//上述是promise的核心三大概念//我们需要明确的一点是 new Promsie 一定是通过构造函数或者 class关键字去定义一个类,在这里我们使用类的形式去写classMyPromise{
//executon函数就是我们实例化promsie传入的回调函数,作为主体函数
constructor(executon){
this.state = 'pending'//这个用于保存我们promise的三种状态 默认是pending即等待状态
this.value = https://www.it610.com/article/undefined//这是我们最终成功返回的一个promise执行结果的值
this.reason = undefined//这是我们执行错误最终返回的一个值//在执行成功的函数时候我们首先可以做的就是改变promise的状态,接收一个参数,并把这个value赋值给当前类的value
let resolve = (value)=>{
if(this.state === 'pending'){
this.state='fullFilled'
this.value = https://www.it610.com/article/value
}
}
//同理在执行失败的函数中我们也要改变promise的状态并对错误进行赋值
let reject=(reason)=>{
if(this.state === 'pending'){
this.state = 'rejected'
this.reason = reason
}}
//主体函数
executon(resolve,reject)}
//并且这两个参数都是回调函数,javascript强大之处体现的一点就在于支持函数式编程所谓的函数式编程感兴趣的同学可以下去研究一下,这里对应着就是then方法里面成功的回调和失败的回调
then(onFullFilled,onRejected){
//这里呢我们也需要判断当前的状态
//当我们的状态变成成功的时候我们需要执行then里面成功的回调函数
if(this.state === 'fullFilled'){
onFullFilled(this.value)}
//同理当状态变成失败的时候我们也要执行失败的回调函数,并把失败的值穿进去
if(this.state === 'rejected'){
onRejected(this.reason)
}}
}
1.1测试用例1
//此时基础架构以及搭建好了我们来试试
//我们可以在这里分析一下整个执行过程//1.当我们实例化这个myPromise的时候我们使用了resolve去吧状态改成fullFilled所以我们then里面成功的函数则会执行
//2.resolve此时是一个形式参数来接收executon函数内部执行结果 executon执行完毕才会执行我们的resolve所有大家可以看到
//3.尽管resolve先执行但是本身这是一个回调函数所以依然是先打印1再打印10从一种不严谨的角度出发
//这里可以理解成微任务
new MyPromise((resolve, reject) => {
resolve(10)
conssole.log(1)//1
}).then((res) => {
console.log(res)//10
})
上述代码以及测试用例都能通过了,下一步我们将处理一下错误捕获,promise内部出现任何错误都会被捕获到,从而不会阻塞代码的执行,下面直接上代码
classMyPromise{
constructor(executon){
this.state = 'pending'
this.value = https://www.it610.com/article/undefined
this.reason = undefined
let resolve = (value)=>{
if(this.state === 'pending'){
this.state='fullFilled'
this.value = https://www.it610.com/article/value
}
}let reject=(reason)=>{
if(this.state === 'pending'){
this.state = 'rejected'
this.reason = reason
}}
//捕获错误主要是在这里捕获
try {
executon(resolve,reject)
} catch (error) {
//抛出错误
reject(error)
}
}
then(onFullFilled,onRejected){
if(this.state === 'fullFilled'){
onFullFilled(this.value)
}
if(this.state === 'rejected'){
onRejected(this.reason)
}}
}
1.2测试用例2
new MyPromise((resolve, reject) => {
console.log(a)//一个未定义的a必然出错
reject(10)}).then((res) => {
console.log(res)//这里没有打印
}, (reason) => {
console.log(reason, 'error')//这里捕获到了错误
})
2处理异步任务
现在我们的myPromise已经有了基本的架构,但是并不具备异步的处理能力,promise的诞生就是为了解决javascript传统的使用回调函数去接受一个异步任务处理结果的缺陷,这种缺陷就是我们老生常谈的回调地狱。
那么promise内部究竟是怎么做的可以避免陷入回调地狱呢?答案是采用发布订阅者设计模式,这种设计模式的本质就是存在一种一对多的依赖关系,不了解这种设计模式的同学,可以查一查资料尝试写一写。下面我们直接上代码
classMyPromise{
constructor(executon){
this.state = 'pending'
this.value = https://www.it610.com/article/undefined
this.reason = undefined//利用发布订阅设计模式在合适的时机去执行注册的回调函数即在异步的回调函数执行完成我们再执行then里面的代码
//既然是发布订阅那么我们则需要两个缓存列表(缓存对应的回调函数)一个成功的列表 一个失败的列表
this.onResolveCallback = []//成功
this.onRejectedCallback = []//失败let resolve = (value)=>{
if(this.state === 'pending'){
this.state='fullFilled'
this.value = https://www.it610.com/article/value
//重写resolve方法我们需要在合适的时机去执行异步对应的回调函数
//这里可以理解为发布订阅里面的订阅者
this.onResolveCallback.forEach(fn=>{fn()})
}
}let reject=(reason)=>{
if(this.state === 'pending'){
this.state = 'rejected'
this.reason = reason
//同理reject也是需要去执行对应的函数
this.onRejectedCallback.forEach(fn=>{fn()})
}
}
try {
executon(resolve,reject)
} catch (error) {reject(error)
}}
then(onFullFilled,onRejected){
if(this.state === 'fullFilled'){
onFullFilled(this.value)
}
if(this.state === 'rejected'){
onRejected(this.reason)
}
//如果我们在实例化的时候向executon函数加上一个异步任务那么此时我们的状态将永远是等待状态
//因为,异步会后于同步代码执行,我们的成功回调也就是resolve还没执行,then方法里面的判断成功也不会执行,那么状态将永远是等待状态所有我们需要在这里判断一下如果出现异步的情况,那么我们的状态将永远是等待状态
if(this.state === 'pending'){
//如果是等待状态我们需要向我们缓存列表里面注册成功回调函数,在未来的某个时刻去执行他们
//这里也可以理解为发布带订阅里面的发布者
this.onResolveCallback.push(()=>{
onFullFilled(this.value)//传入成功的
})
//同理如果是失败的情况也要注册一个失败的回调函数
this.onRejectedCallback.push(()=>{
onRejected(this.reason)
})
}}
}
2.1 测试用例3 上述代码为我们的myPromise添加了处理异步任务的能力,那么就要补上我们的测试
let p1 = new MyPromise((resolve, reject) => {
//请注意 setTimeout众所周知是异步的而且是一个宏任务是我们的的浏览器提供的api。setTimeout会在所有的同步代码后执行
//我么可以分析一下此时代码的执行过程,当我们实例化的时候遇到了异步,那么我们状态就不会得到改变,这时我们在myPromise的then方法里面回去注册成功的回调函数,当1s过后我们的 resolve函数执行,那么状态就会发生改变,此时我们缓存列表对应的回调也会执行,这里非常绕,很难理解,手动写一写将会有助于理解
setTimeout(() => {
resolve(100)
},1000)
})p1.then((res) => {
console.log(res + 1)//1s后 打印101测试通过
})
2.2测试用例4 由于我们使用了缓存列表所有每一个then都能捕捉到异步都执行同样的,我们也补上测试用例
let p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(100)
}.1000)
})p1.then((res) => {
console.log(res + 1)//1s 打印101
})p1.then((res) => {
console.log(res + 2)//1s后 打印102
})
3.实现链式调用
现在最复杂,最绕,最硬核,最重要的链式调用要来了,链式调用是promsie的核心,重中之重。这一段非常难以理解,笔者写的也不是很清楚,还请多多包涵,如果想要弄得这里还是那句话得动手写一写。上代码
class MyPromise {
constructor(executon) {
this.state = 'pending'
this.value = https://www.it610.com/article/undefined
this.reason = undefinedthis.onResolveCallback = []
this.onRejectedCallback = []let resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fullFilled'
this.value = https://www.it610.com/article/valuethis.onResolveCallback.forEach(fn => { fn() })
}
}let reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = reasonthis.onRejectedCallback.forEach(fn => { fn() })
}}try {
executon(resolve, reject)
} catch (error) {reject(error)
}}//链式调用 :重点
//这里实际上也是接收的形式参数 onFullFilled onRejected
//并且这两个参数都是回调函数,javascript强大之处体现的一点就在于支持函数式编程所谓的函数式编程感兴趣的同学可以下去研究一下
then(onFullFilled, onRejected) {//实现链式调用我们的then里面要默认返回一个新的promsie//注意思考一下这里为什么不能直接return this? 因为这里的this实际上是当前的proise 实例
//所以要返回一个新的promsie 这里非常的绕啊 我给出结论是 我们then多少次那么这里的 代码就会执行多少次//所以这里实际上是一个递归//并且我门需要处理如果promise实例里面的then也返回的是一个新的promise实例应该怎么做?
//本质上就是根据x的值来处理不同的结果 定义一个resolvePromsie函数来执行下一步let p2 =new MyPromise((resolve, reject) => {
let x;
//记录一下 我们可以想一想这个x到底是什么东西?其实就是一个用于记录then里面返回的结果,非常难以理解啊这里//这里呢我们也需要判断当前的状态if (this.state === 'fullFilled') {
//并且传入当前的value值,很重要的一点是 此时的this指向的myclass这个类即指向构造函数,这里的this.value是一个实际参数//并且这里一定要走异步否自状态永远是pending状态因为then里面也可能是实例化了一个新的promise并且在里面做了一个异步任务setTimeout(()=>{
x = onFullFilled(this.value)
resolvePromsie(p2,x,resolve,reject)
})
}if (this.state === 'rejected') {
x=onRejected(this.reason)
resolvePromsie(p2,x,resolve,reject)
}if (this.state === 'pending') {
this.onResolveCallback.push(() => {
x=onFullFilled(this.value)
resolvePromsie(p2,x,resolve,reject)
})
this.onRejectedCallback.push(() => {
x=onRejected(this.reason)
resolvePromsie(p2,x,resolve,reject)
})
}
})//return这个实例出去
return p2}}//定义一个执行下一步操作的函数这里主要是接收了四个参数
//p2 一个promise的实例,在规范里面then方法会默认返回一个新的promise实例 注意这里是新的function resolvePromsie(p2,x,resolve,reject){
//请注意p2和x不能是同一个值至于为什么可以思考一下 或者根据promise A+标准去理解
if(x === p2){
return new Error('引用错误')
}
//为了兼容其他promise的库我们这里做了一些兼容 x可以是一个新的实例也可以是一个函数
if(typeof x === 'object' && x!== null || typeof x=== 'function'){
//错误捕获
try {
//x如果是一个新的promise实例那么它必然有then方法
let then = x.then
if(typeof then === 'function'){
//如果存在then直接让x调用
then.call(x,y=>{
//成功的回调
//resolve(y) 注意这里如果仅仅是调用resolve(y)那么只是判断了第二层的回调函数第二层往下便执行不了//所以这里必须要递归执行resolvePromsie 无论有多少promise嵌套多少实例都能监听到
resolvePromsie(p2,y,resolve,reject)
},err=>{
//失败的回调 失败就直接抛出错误无需递归
reject(err)
})
}
} catch (error) {
reject(err)
}
}else{
//如果x是一个普通值则直接调用成功即可
resolve(x)
}}
3.1 测试用例5 上述代码已经为我们补上了 链式调用已经如果我们在then里面重新实例化一个promse的处理结果 同意我们补上测试用例
let p1 = new MyPromise((resolve, reject) => {
resolve(100)
})p1.then((res) => {
console.log(res + 1)//101
return res+4
}).then((res)=>{
console.log(res)//102
})
3.2测试用例 6 异步链式调用
let p1 = new MyPromise((resolve, reject) => {
setTimeout(()=>{
resolve(100)
})
})p1.then((res) => {
console.log(res + 1)//101
return res+4}).then((res)=>{
console.log(res)//104
})
3.3 测试用例7 then里面返回一个新的promise实例
let p1 = new MyPromise((resolve, reject) => {
setTimeout(()=>{
resolve(100)
})})p1.then((res) => {
console.log(res)//100
return new MyPromise((resolve,reject)=>{
resolve(200)
})}).then((res)=>{
console.log(res)//200
})
3.4 测试用例8 顺序执行异步任务 这也是promise的核心 即解决回调地狱
let p1 = new MyPromise((resolve, reject) => {
setTimeout(()=>{
console.log(1)// 3s后打印1
resolve()
},3000)})p1.then((res) => {
return new MyPromise((resolve,reject)=>{
setTimeout(()=>{
console.log(2)// 函数体里面的异步执行完成后 等2s 打印2
resolve()
},2000)})}).then((res)=>{
console.log(3)//最后是3
})//这里是整个promise的核心 同步的方式去执行异步任务
结语
【简单的promsie源码实现】手写一个promise并非一件很容易的事,此案例还有很多方法没有实现,感兴趣的同学可以再次基础之上实现一下all,race,finally等方法,程序本身就是一件很抽象的事,只能在一次又一次的实践中锻炼自己的思维。作者实力有限,还望指正。
推荐阅读
- 谷歌(新手不推荐 选它就对了)
- uniapp|微信小程序获取当前城市名称--逆地址解析
- 校招前端二面高频面试题合集
- 美团前端面试题(附答案)
- 滴滴前端二面面试题
- 阿里前端一面面试题(附答案)
- 滴滴前端一面必会面试题
- 高级前端二面高频面试题合集
- 社招前端二面常见面试题