手写Promise—实现Promise/A+规范

最近在看手写API的面试题,发现了一个手写Promise,之前学长讲课的时候讲过,不过当时我还是刚刚接触前端,还没有学那么多,所以学长讲的,也没有听懂,时隔一年多,我已经大三了,也该研究一下这么写了。
网上挺多手写promise的文章,但是我试了几个没有多少完全通过官方promises-aplus-tests测试库的全部案例的,而且大部分代码完全一样,没有讲解为什么这样做,这就导致在看的时候稀里糊涂的。这就来总结一下

实现目标

要想手写promise,我们先要知道原生promise的一些方法,由此来照猫画虎,实现我们自己的一个promise方法,另外我们实现promise是按照Promise A+的规范来写的,具体可以看Promise/A+文档

  1. 函数的基本使用
  2. resolve、resject方法
  3. then方法
  4. catch方法
  5. finally方法
  6. MyPromise.resolve和MyPromise.reject静态方法
  7. MyPromise.all和MyPromise.race静态方法
  8. MyPromise.allSettled和MyPromise.all静态方法
  9. 使用promises-aplus-tests测试并通过官方案例

MyPromise实现

定义状态

我们都知道原生的promise有三种状态pending(等待)、fulfilled(已实现)、rejected(已失败),并且一旦改变状态之后不可再次改变

const status = {
    PENDING: 'pending',
    FULFILLED: 'fulfilled',
    REJECTED: 'rejected'
}

完成类基本构造

因为我们平常在用promise的时候经常会用到new Promise,这么我们就可以看出来promise实际上是一个构造函数或者类,为了方便,我们这里使用类来写,我们在new的一般会传入一个回调函数,该函数接收两个回调函数作为参数,一个是resolve,一个是reject,接下来我们就来实现这些

const status = {
  PENDING: 'pending',
  FULFILLED: 'fulfilled',
  REJECTED: 'rejected'
}

class MyPromise {
  constructor(executor) {
    // 初始化类的基本操作
    this.init()
    // 定义resolve和reject函数
    const resolve = (successValue) => {
      // 当且仅当状态为padding时才会触发
      if (this.status !== status.PENDING) {
        return
      }
      this.status = status.FULFILLED
      this.value = successValue
      this.successFns.forEach(callBack => callBack())
    }
    const reject = (failValue) => {
      // 当且仅当状态为padding时才会触发
      if (this.status !== status.PENDING) {
        return
      }
      this.status = status.REJECTED
      this.reason = failValue
      this.failFns.forEach(callBack => callBack())
    }

    // 执行传入的函数,当该函数抛出移除或者出错时,直接失败
    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }

  init() {
    // 初始化状态
    this.status = status.PENDING

    // 用于存放成功的值
    this.value = null
    // 用于存放成功的回到函数
    this.successFns = []

    // 用于存放出错的值
    this.reason = null
    // 用于存放失败的回调函数
    this.failFns = []
  }
}

then方法

then方法,也是接收两个函数,一个是成功的回调,另一个是失败的回调,只不过我们平常为了方便,不会传入第二个函数

then(successFn, failFn) {
  // 这里需要判断当前状态,如果是pedding时我们,就需要把回调函数压入对应的数组,供之后成功的时候执行
  if (this.status === status.PENDING) {
    // 压入函数我们还需要注意一点就是为了模仿异步性,我们使用setTimeout来代替
    this.successFns.push(() => {
      setTimeout(() => {
        // 这里需要注意以下this的指向,因为我们用的是箭头函数,所以this的指向仍任是实例
        successFn(this.successRes)
      })
    })
    this.failFns.push(() => {
      setTimeout(() => {
        failFn(this.failRes)
      })
    })
  }
  // 如果当前状态已经是成功的状态了,就直接执行(仍需要保证执行的异步性)
  if (this.status === status.FULFILLED) {
    setTimeout(() => {
      successFn(this.successRes)
    })
  }
  // 如果当前状态已经是失败的状态,就直接执行
  if (this.status === status.REJECTED) {
    setTimeout(() => {
      failFn(this.failRes)
    })
  }
}

另外需要注意一下,我们上边实现的then方法,仍有不足,就是我们一般调用then方法时,我们可以链式调用,这就需要我们返回一个promise方法

修改then方法

then(successFn, failFn) {
  // 如果不传处理函数,则使用默认处理函数
  successFn = typeof successFn === 'function' ? successFn : value => value;
  failFn = typeof failFn === 'function' ? failFn : err => { throw err };
  const promise2 = new MyPromise((resolve, reject) => {
    // 这里需要判断当前状态,如果是pedding时我们,就需要把回调函数压入对应的数组,供之后成功的时候执行
    if (this.status === status.PENDING) {
      // 压入函数我们还需要注意一点就是为了模仿异步性,我们使用setTimeout来代替
      this.successFns.push(() => {
        setTimeout(() => {
          try {
            // 这里需要注意以下this的指向,因为我们用的是箭头函数,所以this的指向仍任是实例
            successFn(this.value)
          } catch (error) {
            reject(error)
          }
        })
      })
      this.failFns.push(() => {
        setTimeout(() => {
          try {
            failFn(this.reason)
          } catch (error) {
            reject(error)
          }
        })
      })
    }
    // 如果当前状态已经是成功的状态了,就直接执行(仍需要保证执行的异步性)
    if (this.status === status.FULFILLED) {
      setTimeout(() => {
        try {
          successFn(this.value)
        } catch (error) {
          reject(error)
        }
      })
    }
    // 如果当前状态已经是失败的状态,就直接执行
    if (this.status === status.REJECTED) {
      setTimeout(() => {
        try {
          failFn(this.reason)
        } catch (error) {
          reject(error)
        }
      })
    }
  })

  return promise2
}

不过我们修改后,仍然不对,因为我们then传入的函数中仍然可以返回一个promise方法,所以我们需要拿到传入函数返回的promise的结果,然后作为promise2的返回结果,这时候可能有人会疑惑为什么不能返回自身而是新建一个promise,这个是因为自身的状态已经改变过了,就不能再改变了。另外还有一种疑惑就是我们为什么不判断传入函数的执行结果,然后判断它是否返回一个promise函数,然后再返回,这是因为传入函数的执行是异步的,而then的链式调用是同步的,所以我们需要立马返回一个promise,并拿到返回的结果值(包括promise的返回结果)然后作为我们创建的promise2的返回值,以此达到链式调用的功能

链式调用

then(successFn, failFn) {
  // 如果不传处理函数,则使用默认处理函数
  successFn = typeof successFn === 'function' ? successFn : value => value;
  failFn = typeof failFn === 'function' ? failFn : err => { throw err };

  const promise2 = new MyPromise((resolve, reject) => {
    // 这里需要判断当前状态,如果是pedding时我们,就需要把回调函数压入对应的数组,供之后成功的时候执行
    if (this.status === status.PENDING) {
      // 压入函数我们还需要注意一点就是为了模仿异步性,我们使用setTimeout来代替
      this.successFns.push(() => {
        setTimeout(() => {
          try {
            // 这里需要注意以下this的指向,因为我们用的是箭头函数,所以this的指向仍任是实例
            const x = successFn(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        })
      })
      this.failFns.push(() => {
        setTimeout(() => {
          try {
            const x = failFn(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        })
      })
    }
    // 如果当前状态已经是成功的状态了,就直接执行(仍需要保证执行的异步性)
    if (this.status === status.FULFILLED) {
      setTimeout(() => {
        try {
          const x = successFn(this.value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (error) {
          reject(error)
        }
      })
    }
    // 如果当前状态已经是失败的状态,就直接执行
    if (this.status === status.REJECTED) {
      setTimeout(() => {
        try {
          const x = failFn(this.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (error) {
          reject(error)
        }
      })
    }
  })

  return promise2
}

这里我们定义了一个resolvePromise用来递归的拿到返回的结果的值

function resolvePromise(promise2, x, resolve, reject) {
  // 避免出现自己等待自己的情况
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  // 多次调用resolve或reject以第一次为主,忽略后边的
  let called = false
  // 判断传入的x是否是一个包含then方法的对象,如果有,就认为resolve返回的值是一个promise
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      const then = x.then
      if (typeof then === 'function') {
        // 认为是一个promise
        then.call(
          x,
          y => {
            if (called) {
              return
            }
            called = true
            // 递归执行,避免resolve是一个promise值
            resolvePromise(promise2, y, resolve, reject)
          },
          reason => {
            if (called) {
              return
            }
            called = true
            reject(reason)
          })
      } else {
        resolve(x)
      }
    } catch (error) {
      if (called) {
        return
      }
      called = true
      reject(error)
    }
  } else {
    // 其他值,可以直接返回
    resolve(x)
  }
}

由此我们完成了手写promise中最复杂的一个功能,另外需要注意一点的是,可能有人不明白既然我们调用了resolve那么它的状态就会改变,为什么还需要called变量来过滤,其实很多博客说的都是避免重复调用,但是我看这个的时候比较迷,就是成功或者失败的函数的执行时机只能是statues为pedding的时候,怎么可能会重复调用呢,其实这个说法不太准确,准确的应该按Promise/A+规范中2.3.3.3中说的

If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
如果同时调用 resolvePromise 和 rejectPromise ,或者对同一参数进行多次调用,则第一个调用优先,并且忽略任何后续调用。

即:我们重复调用resolve或者reject应该以第一次的为准,而忽略后续的,我们可以看这种情况理解一下

const p = new MyPromise(resolve => {
  resolve()
})
const thenable1 = {
  then(reslove) {
    setTimeout(() => {
      reslove(2)
    }, 0)
  },
}
const thenable2 = {
  then(resolve) {
    resolve(thenable1)
    resolve(1)
  },
}

p.then(() => {
  return thenable2
})
  .then(res => {
    console.log(res);
  })

按上边规范所说的情况,我们应该最终得到的结果是2,但是如果没有called的情况,我们会得到1
这个很好理解,我们应该以第一个resolve为主,但是第一个resolve的值是一个带resolve函数的对象,并且用一个宏任务setTimeout来包裹,所以其执行时机会比resolve(2)要晚一步,那么就会错误的拿到1,这时候我们需要忽略后续的调用而采用第一次调用

完成其他方法

static resolve(value) {
  // 传入的是一个promise
  if (value instanceof MyPromise) {
    return value
  }
  return new MyPromise((resolve, reject) => {
    resolve(value)
  })
}

static reject(err) {
  return new MyPromise((resolve, reject) => {
    reject(err)
  })
}

catch(failFn) {
  return this.then(null, failFn)
}

finally(callback) {
  // 调用then方法,传入两个相同的处理函数
  return this.then(
    value => {
      // 创建一个新的Promise实例,确保异步执行callback
      return MyPromise.resolve(callback()).then(() => value);
    },
    reason => {
      // 创建一个新的Promise实例,确保异步执行callback
      return MyPromise.resolve(callback()).then(() => { throw reason; });
    }
  );
}

static all(promises) {
  return new MyPromise((resolve, reject) => {
    const res = []
    let conunt = 0
    promises.forEach((promise, index) => {
      MyPromise.resolve(promise).then(value => {
        res[index] = value
        conunt++
        if (conunt === promises.length) {
          resolve(res)
        }
      }, err => {
        reject(err)
      })
    })
  })
}

static race(promises) {
  return new MyPromise((resolve, reject) => {
    promises.forEach(promise => {
      MyPromise.resolve(promise).then(value => {
        resolve(value)
      }, err => {
        reject(err)
      })
    })
  })
}

static allSettled(promises) {
  const result = [];
  let settledCount = 0;
  promises.forEach((promise, index) => {
    MyPromise.resolve(promise).then(
      value => {
        result[index] = { status: 'fulfilled', value };
        settledCount++;
        if (settledCount === promises.length) {
          resolve(result);
        }
      },
      reason => {
        result[index] = { status: 'rejected', reason };
        settledCount++;
        if (settledCount === promises.length) {
          resolve(result);
        }
      }
    );
  });
}

static any(promises) {
  return new MyPromise((resolve, reject) => {
    const errors = [];
    let rejectedCount = 0;
    promises.forEach((promise, index) => {
      MyPromise.resolve(promise).then(
        value => {
          resolve(value);
        },
        reason => {
          errors[index] = reason;
          rejectedCount++;
          if (rejectedCount === promises.length) {
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        }
      );
    });
  });
}

完整代码

由此我们就完成了手写promise,并且完成了其中的一些方法

const status = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}
function resolvePromise(promise2, x, resolve, reject) {
// 避免出现自己等待自己的情况
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
// 多次调用resolve或reject以第一次为主,忽略后边的
let called = false
// 判断传入的x是否是一个包含then方法的对象,如果有,就认为resolve返回的值是一个promise
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
const then = x.then
if (typeof then === 'function') {
// 认为是一个promise
then.call(
x,
y => {
if (called) {
return
}
called = true
// 递归执行,避免resolve是一个promise值
resolvePromise(promise2, y, resolve, reject)
},
reason => {
if (called) {
return
}
called = true
reject(reason)
})
} else {
resolve(x)
}
} catch (error) {
if (called) {
return
}
called = true
reject(error)
}
} else {
// 其他值,可以直接返回
resolve(x)
}
}
class MyPromise {
constructor(executor) {
// 初始化类的基本操作
this.init()
// 定义resolve和reject函数
const resolve = (successValue) => {
// 当且仅当状态为padding时才会触发
if (this.status !== status.PENDING) {
return
}
this.status = status.FULFILLED
this.value = successValue
this.successFns.forEach(callBack => callBack())
}
const reject = (failValue) => {
// 当且仅当状态为padding时才会触发
if (this.status !== status.PENDING) {
return
}
this.status = status.REJECTED
this.reason = failValue
this.failFns.forEach(callBack => callBack())
}
// 执行传入的函数,当该函数抛出移除或者出错时,直接失败
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
init() {
// 初始化状态
this.status = status.PENDING
// 用于存放成功的值
this.value = null
// 用于存放成功的回到函数
this.successFns = []
// 用于存放出错的值
this.reason = null
// 用于存放失败的回调函数
this.failFns = []
}
then(successFn, failFn) {
// 如果不传处理函数,则使用默认处理函数
successFn = typeof successFn === 'function' ? successFn : value => value;
failFn = typeof failFn === 'function' ? failFn : err => { throw err };
const promise2 = new MyPromise((resolve, reject) => {
// 这里需要判断当前状态,如果是pedding时我们,就需要把回调函数压入对应的数组,供之后成功的时候执行
if (this.status === status.PENDING) {
// 压入函数我们还需要注意一点就是为了模仿异步性,我们使用setTimeout来代替
this.successFns.push(() => {
setTimeout(() => {
try {
// 这里需要注意以下this的指向,因为我们用的是箭头函数,所以this的指向仍任是实例
const x = successFn(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
})
this.failFns.push(() => {
setTimeout(() => {
try {
const x = failFn(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
})
}
// 如果当前状态已经是成功的状态了,就直接执行(仍需要保证执行的异步性)
if (this.status === status.FULFILLED) {
setTimeout(() => {
try {
const x = successFn(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
}
// 如果当前状态已经是失败的状态,就直接执行
if (this.status === status.REJECTED) {
setTimeout(() => {
try {
const x = failFn(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (error) {
reject(error)
}
})
}
})
return promise2
}
static resolve(value) {
// 传入的是一个promise
if (value instanceof MyPromise) {
return value
}
return new MyPromise((resolve, reject) => {
resolve(value)
})
}
static reject(err) {
return new MyPromise((resolve, reject) => {
reject(err)
})
}
catch(failFn) {
return this.then(null, failFn)
}
finally(callback) {
// 调用then方法,传入两个相同的处理函数
return this.then(
value => {
// 创建一个新的Promise实例,确保异步执行callback
return MyPromise.resolve(callback()).then(() => value);
},
reason => {
// 创建一个新的Promise实例,确保异步执行callback
return MyPromise.resolve(callback()).then(() => { throw reason; });
}
);
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const res = []
let conunt = 0
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(value => {
res[index] = value
conunt++
if (conunt === promises.length) {
resolve(res)
}
}, err => {
reject(err)
})
})
})
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
MyPromise.resolve(promise).then(value => {
resolve(value)
}, err => {
reject(err)
})
})
})
}
static allSettled(promises) {
const result = [];
let settledCount = 0;
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
result[index] = { status: 'fulfilled', value };
settledCount++;
if (settledCount === promises.length) {
resolve(result);
}
},
reason => {
result[index] = { status: 'rejected', reason };
settledCount++;
if (settledCount === promises.length) {
resolve(result);
}
}
);
});
}
static any(promises) {
return new MyPromise((resolve, reject) => {
const errors = [];
let rejectedCount = 0;
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
resolve(value);
},
reason => {
errors[index] = reason;
rejectedCount++;
if (rejectedCount === promises.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
}
);
});
});
}
}
module.exports = {
MyPromise
}

测试

写完了代码我们还需要测试我们写的代码是否符合promise/A+规范,我们可以使用promises-aplus-tests来测试

  1. 初始化项目
npm init --y
  1. 安装依赖
yarn add promises-aplus-tests -D
  1. 新建adapter.js
const { MyPromise } = require('./MyPromise')
// 暴露适配器对象
module.exports = {
resolved: MyPromise.resolve,
rejected: MyPromise.reject,
deferred() {
const result = {};
result.promise = new MyPromise((resolve, reject) => {
result.resolve = resolve;
result.reject = reject;
});
return result;
}
};
  1. 新建test.js
const promisesAplusTests = require('promises-aplus-tests');
const adapter = require('./adapter');
promisesAplusTests(adapter, function (err) {
if (err) {
console.error('Promises/A+ 测试失败:');
console.error(err);
} else {
console.log('Promises/A+ 测试通过');
}
});
  1. 执行测试
node test.js

这样我们就查看我们的代码是否符合promise/A+的规范了

总结

我们完成了测试,就表示我们写的一个符合Promise/A+规范的promise,另外我们也补充了一些promise的一些基本方法,虽然有些人说手写API没啥用,不过我不这样认为,因为我们手写API的过程中,不仅能更多的了解到原生API的一些方法的实现原理,这样可以帮助我们遇到问题时更快速的定位,另外也给我们一个思路,手写promise的过程中我感觉它有点像一个发布订阅模式,同时还要考虑一些异步的问题,我这里是用setTimeout来简单模拟的,还可以用一个微任务队列去模拟,这个更多的是体会思想吧。真的不得不服知道标准的哪些人。

原文链接:https://juejin.cn/post/7315260397371129871 作者:BUG不加糖

(0)
上一篇 2023年12月23日 上午10:10
下一篇 2023年12月23日 上午10:21

相关推荐

发表回复

登录后才能评论