前端必须学会的经典面试题场景之一 —— 手写Promise

本文详细介绍了如何手写一个基本的Promise实现,包括构造函数、resolve和reject的使用,状态管理以及then方法的实现。通过逐步解析,展示了Promise如何处理异步操作,实现状态转变以及链式调用。文中还探讨了如何优化then方法以支持多次调用和链式调用,并提到了在实际面试中可能需要考虑的错误处理和类型判断等进阶问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Promise作为JS中异步编程最为强大的解决方案之一,ES6将其写进了语言标准,统一了用法。
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。
  • 下面我们自己实现一个Promise:

Promis的手写实现我们用到用到ES6中的Class类,当然用构造函数也是一样的,基本骨架如下:

class ZhouPromise {
  constructor(executor) {
    const resolve = (res) => {
      console.log(res);
    };
    const reject = (err) => {};
    executor && executor(resolve, reject);
  }
}

const promise = new ZhouPromise((resolve, reject) => {
  resolve('zhoubaba');
})

当我们new ZhouPromise的时候,执行类的constructor构造器,接收一个函数执行体,同时并且 执行将自定义的resolve,reject传递进去当做resolve,reject的参数变量接收。

  • resolve作用是将Promise对象状态由“未完成”变为“成功”,也就是Pending -> Fulfilled,在异步操作成功时调用,并将异步操作的结果作为参数传递出去;而reject函数则是将Promise对象状态由“未完成”变为“失败”,也就是Pending -> Rejected,在异步操作失败时调用,并将异步操作的结果作为参数传递出去。
const PROMISE_STATUS_PENDING = "pending";
const PROMISE_STATUS_FULFILLED = "fulfilled";
const PROMISE_STATUS_REJECTED = "rejected";

constructor构造器内部状态,默认为pending

this.status = PROMISE_STATUS_PENDING;
this.value = undefined;
this.reason = undefined;

改变resolve和rejected,当执行的时候修改status状态

const resolve = (value) => {
  if (this.status === PROMISE_STATUS_PENDING) 
   {      
    this.status = PROMISE_STATUS_FULFILLED;
        this.value = value;
       console.log("status", this.status, "val  ue", this.value);
      }
    };
const reject = (reason) => {
  if (this.status === PROMISE_STATUS_PENDING)    {
      this.status = PROMISE_STATUS_REJECTED;
      this.reason = reason;
        console.log("status", this.status, "reason", this.reason);
      }
   }

const p = new ZhouPromise((resolve, reject) => {
     resolve("fulfilled状态"); // status fulfilled value fulfilled状态
     //reject("rejected状态"); // status rejectedvalue rejected状态
});
then方法的实现:
  • Promise实例生成后,可用then方法分别指定两种状态回调参数。then 方法可以接受两个回调函数作为参数:
  1. Promise对象状态改为Resolved时调用 (必选)
  2. Promise对象状态改为Rejected时调用 (可选)

简单实现:

constructor(executor) {
    ...
    this.onFulfilled = undefined; // !新增
    this.onRejected = undefined; // !新增
    const resolve = (value) => {
        ...
        this.onFulfilled(this.value); // !新增
      }
    };
    const reject = (reason) => {
        ...
        this.onRejected(this.reason); // !新增
      }
    };
  }

 then(onFulfilled, onRejected) { // !新增
    this.onFulfilled = onFulfilled;
    this.onRejected = onRejected;
  }
  
 ...
 p.then(
  (res) => {
    console.log(res);
  },
  (err) => {
    console.log(err);
  }
);

如上代码所示(部分重复代码用…展示),constructor添加两个onFulfilled,onRejected用来存储then函数的回调。定义类的then方法,接收两个回调,同时赋值。
在这里插入图片描述

会发现resolve执行后会报错,那是因为new promise里的回调函数是同步执行的,当resolve的时候,p.then还没有执行,所以onFulfilled 为undefined。我们需要给resolve内部添加setTimeout或者queueMicrotask异步任务。异步任务不会阻塞代码,会进入到宏任务中。等到同步代码p.then执行完后再去执行resolve。
所以如下修改resolve和reject函数:
 const resolve = (value) => {
      if (this.status === PROMISE_STATUS_PENDI    NG) {
        this.status = PROMISE_STATUS_FULFILLED;
        queueMicrotask(() => { // !新增
          this.value = value;
          this.onFulfilled(this.value);
        });
      }
    };


  const reject = (reason) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        this.status = PROMISE_STATUS_REJECTED;
        queueMicrotask(() => { // !新增
          this.reason = reason;
          this.onRejected(this.reason);
        });
      }
    };
至此,一个then回调完成了。

这就是promise简单的基本手写思路。在面试中写到这里已经基本差不多了。但是可以更加完全接近promise的所有功能。

优化 .then

原生Promise的then方法可以被多次调用,这有点像Vue内的响应式收集。当我们调用多个then方法,会被收集到桶里面。
例如:

p.then(
  (res) => {
    console.log("res1", res);
  },
  (err) => {
    console.log("err1", err);
  }
);
p.then(
  (res) => {
    console.log("res2", res);
  },
  (err) => {
    console.log("err2", err);
  }
);
setTimeout(() => {
  p.then(
    (res3) => {
      console.log(res3 + "res3");
    },
    (err3) => {
      console.log(err3 + "err3");
    }
  );
}, 1000);

所以收集then回调的得是个可迭代的对象或者数组,在这里用数组代替。在resolve或者reject执行时对收集的数组内遍历函数。

  this.onFulfilled = [];//修改
  this.onRejected = [];//修改

----------------------------
  const resolve = (value) => {
      if (this.status === PROMISE_STATUS_PENDING) {
       //this.status = PROMISE_STATUS_FULFILLED; // !变动到微任务内
        queueMicrotask(() => {
          this.status = PROMISE_STATUS_FULFILLED;
          this.value = value;
          this.onFulfilled.forEach((fn) => { // !修改
            fn && fn(this.value);
          });
        });
      }
    };
    
  const reject = (reason) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        //this.status = PROMISE_STATUS_REJECTED; // !变动到微任务内
        queueMicrotask(() => {
          this.status = PROMISE_STATUS_REJECTED;
          this.reason = reason;
          this.onRejected.forEach((fn) => { // !修改
            fn && fn(this.reason);
          });
        });
      }
    };

同时then方法内也要修改,防止在全局异步下添加的then函数没有被执行,如果当前状态已经被修改了,说明queueMicrotask已经执行了,是全局setTimeout添加的then方法,就直接执行函数;如果仍然是pending,说明仍然在收集then回调。

 then(onFulfilled, onRejected) {
    if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {//新增
      onFulfilled(this.value);
      return
    }
    if (this.status === PROMISE_STATUS_REJECTED && onRejected) {//新增
      onRejected(this.reason);
      return
    }
    if (this.status === PROMISE_STATUS_PENDING) {
      this.onFulfilled.push(onFulfilled);
      this.onRejected.push(onRejected);
    }

这里特别注意一点: resolve和reject函数内的queueMicrotask微任务前的this.status状态改变需要移入queueMicrotask内部判断;不然会出现如下undefined效果:
在这里插入图片描述
原因是在新增的then方法内做了状态判断,在改变状态后执行为微任务队列,p.then方法会直接执行当前回调,所以this.value是undefined,我们需要将状态改变放到微任务队列中。

  • 如何实现then的链式调用?我们知道then会返回一个promise对象,所以then需要return一个出去
  then(onFulfilled, onRejected) {
    return new MjcPromise((resolve, reject) => {
      if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
        onFulfilled(this.value);
        return;
      }
      if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
        onRejected(this.reason);
        return;
      }
      if (this.status === PROMISE_STATUS_PENDING) {
        this.onFulfilled.push(onFulfilled);
        this.onRejected.push(onRejected);
      }
    });
  }

最麻烦的问题来了,当前return 出去的promise是需要resolve或者reject的,而我们必须得拿到上一次结果的返回值,在新promise内resolve()出去。我们需要改变this.onFulfilled//onRejected的push对象,拿到返回值在这里resolve。

 then(onFulfilled, onRejected) {
    return new ZhouPromise((resolve, reject) => {
      ...
      if (this.status === PROMISE_STATUS_PENDING) {
        this.onFulfilled.push(() => {
          let value = onFulfilled(this.value);
          resolve(value);
        });
        this.onRejected.push(() => {
          let value = onRejected(this.reason);
          resolve(value);
        });
      }
    });
  }

当用箭头函数的作用可以通过this的不绑定原则能拿到实例上的值。

p.then(
  (res) => {
    console.log("res1", res);
    return "mjc";
  },
  (err) => {
    console.log("err1", err);
    return "mjcerr";
  }
).then(
  (res) => {
    console.log("res2", res);
  },
  (err) => {
    console.log("err2", err);
  }
);

一开始这种处理确实难理解,因为没看懂this和resolve代表哪个promise。我们只要清楚this指向就知道如何执行的。如图:
在这里插入图片描述
我们在promise内传递的函数是箭头函数,那就意味着this指向上下文,也就是说this是指向上一个promise实例,而resolve的调用是处理当前new ZhouPromise对象的状态。
在这里插入图片描述

至此已经基本实现链式调用了。事实上剩下的是错误不断处理,判断类型正确,因为篇幅有限不展开了。 以上部分思路参考promiseA+以及coderwhy大神技术参考。

本篇文章内部分示例引用:
作者:麦当当
链接:https://juejin.cn/post/7102703565566640136
来源:稀土掘金

感谢阅读,有什么问题可以在评论区或后台留言讨论~
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值