手写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(已失败),并且一旦改变状态之后不可再次改变

javascript 复制代码
const status = {
    PENDING: 'pending',
    FULFILLED: 'fulfilled',
    REJECTED: 'rejected'
}

完成类基本构造

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

javascript 复制代码
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方法,也是接收两个函数,一个是成功的回调,另一个是失败的回调,只不过我们平常为了方便,不会传入第二个函数

javascript 复制代码
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方法

javascript 复制代码
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的返回值,以此达到链式调用的功能

链式调用

javascript 复制代码
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用来递归的拿到返回的结果的值

javascript 复制代码
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应该以第一次的为准,而忽略后续的,我们可以看这种情况理解一下

javascript 复制代码
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,这时候我们需要忽略后续的调用而采用第一次调用

完成其他方法

javascript 复制代码
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,并且完成了其中的一些方法

javascript 复制代码
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. 初始化项目
bash 复制代码
npm init --y
  1. 安装依赖
bash 复制代码
yarn add promises-aplus-tests -D
  1. 新建adapter.js
javascript 复制代码
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
javascript 复制代码
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. 执行测试
javascript 复制代码
node test.js

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

总结

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

相关推荐
老赵的博客7 分钟前
QSS 设置bug
前端·bug·音视频
Chikaoya8 分钟前
项目中用户数据获取遇到bug
前端·typescript·vue·bug
南城夏季8 分钟前
蓝领招聘二期笔记
前端·javascript·笔记
NoloveisGod34 分钟前
Vue的基础使用
前端·javascript·vue.js
GISer_Jing36 分钟前
前端系统设计面试题(二)Javascript\Vue
前端·javascript·vue.js
海上彼尚1 小时前
实现3D热力图
前端·javascript·3d
杨过姑父1 小时前
org.springframework.context.support.ApplicationListenerDetector 详细介绍
java·前端·spring
理想不理想v1 小时前
使用JS实现文件流转换excel?
java·前端·javascript·css·vue.js·spring·面试
惜.己2 小时前
Jmeter中的配置原件(四)
java·前端·功能测试·jmeter·1024程序员节
EasyNTS2 小时前
无插件H5播放器EasyPlayer.js网页web无插件播放器vue和react详细介绍
前端·javascript·vue.js