Skip to content

手写 Promise 实现思路

观察 Promise 的使用方式

js
let pro = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);

    // or call reject(0)
  }, 1000);
});

pro
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.error(err);
  });

可以分析 Promise 的内部实现大致如下:

  1. Promise 类在实例化的时候接收一个函数作为参数,计作:cbcb 能够接收两个参数:resolve 方法和 reject 方法;
  2. cb 内部逻辑执行结果成功,调用了 resolve 方法,则传递给.then 的方法会被执行;
  3. cb 内部逻辑执行结果失败,则调用 reject 方法,则传递给.catch 的方法会被执行;
  4. Promise共有三种状态(pending、fulfilled、rejected),并且初始值为pending,并在最后从pending变为fulfilled或者从pending变为rejected
  5. Promise支持链式调用。

实现 Promise 就要做到

  • 接收一个立即执行的 cb、内部控制 Promise 生命周期的状态 status,初始值为 pending、存储使用者传递给 then 的参数 thenCb、存储使用者传递给 catch 的参数 catchCb
  • Promise 实例化的时候立即执行传递个它的 cb 函数,cb 内部根据执行结果判断是执行 resolve() 还是 reject() 函数;
  • 如果执行了 resolve,则将 status 变为 fulfilled,并执行传递给 then 的回调函数;
  • 如果执行了 reject,则将 status 变为 rejected,并执行传递给 catch 的回调函数;
  • 因为要支持链式调用,所以各个内部方法都需要返回 this

那么 MyPromise 内部结构应该大致如下:

js
const PENDING = "pending"; //等待状态
const FULFILLED = "fulfilled"; //成功
const REJECTED = "rejected"; //失败

class MyPromise {
  constructor(cb) {
    this.status = PENDING;

    this.thenCb = () => {};
    this.catchCb = () => {};

    if (cb && typeof cb == "function") {
      // 函数体立即执行,但是resolve和reject是加入到微任务队列后续执行的
      cb(this.resolve.bind(this), this.reject.bind(this));
    } else {
      throw new Error("param must be a function.");
    }
  }

  // 改变status为fulfilled,代表cb执行成功,并执行thenCb
  resolve(result) {
    return this;
  }

  // 改变status为rejected,代表cb执行失败,并执行catchCb
  reject(error) {
    return this;
  }

  // 调用.then记录then内部回调函数,在resolve里触发;
  then(thenCb) {
    return this;
  }

  // 调用.catch记录catch内部回调函数,在reject里触发;
  catch(catchCb) {
    return this;
  }
}

然后根据分析,完善内部具体逻辑,代码如下:

js
const PENDING = "pending"; //等待状态
const FULFILLED = "fulfilled"; //成功
const REJECTED = "rejected"; //失败

class MyPromise {
  constructor(cb) {
    this.status = PENDING;

    this.thenCb = () => {};
    this.catchCb = () => {};

    if (cb && typeof cb == "function") {
      cb(this.resolve.bind(this), this.reject.bind(this));
    } else {
      throw new Error("param must be a function.");
    }
  }

  // 将status从pending变为fulfilled,代表cb执行成功,并执行thenCb
  resolve(result) {
    if (this.status == PENDING) {
      this.status = FULFILLED;
      this.thenCb(result);
      return this;
    }
  }

  // 将status从pending变为rejected,代表cb执行失败,并执行catchCb
  reject(error) {
    if (this.status == PENDING) {
      this.status = REJECTED;
      this.catchCb(error);
      return this;
    }
  }

  // 调用.then,如果status=pending,则记录then内部回调函数,在resolve里触发;
  then(thenCb) {
    if (this.status == PENDING) {
      this.thenCb = thenCb;
    }
    return this;
  }

  // 调用.catch,如果status=pending,则记录catch内部回调函数,在reject里触发;
  catch(catchCb) {
    if (this.status == PENDING) {
      this.catchCb = catchCb;
    }
    return this;
  }
}

测试 MyPromise,并对比原生 Promise

js
let isOk = Math.random() > 0.5;
function asyncCb(resolve, reject, type) {
  setTimeout(() => {
    if (isOk) {
      resolve(`${type}: Succeed.`);
    } else {
      reject(`${type}: Failed.`);
    }
  }, 1000);
}

// 测试自定义promise
let myPro = new MyPromise((resolve, reject) =>
  asyncCb(resolve, reject, "手写")
);

myPro
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  });

// 测试原生promise
let pro = new Promise((resolve, reject) => asyncCb(resolve, reject, "原生"));

pro
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  });

🧯 异常出现了!

当在 cb 里同步调用 resolve 或者 reject,那么.then 或.catch 将无返回结果,如下代码:

function syncCb(resolve, reject, type) {
  resolve('同步返回succeed结果')
}

// 测试自定义promise
let myPro = new MyPromise((resolve, reject) =>
  syncCb(resolve, reject, "手写")
);

myPro
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  });

分析原因不难发现,是因为 resolve 的同步执行,导致 status 立即被改为 fulfilled,调用 this.thenCb 无效,因为此时 thenCb 还并未被.then 赋值。 要兼容这种情况,需要处理几个地方:

  1. 在 resolve 时,存储 result 值;在 reject 时,存储 error 值;
  2. 在调用.then 时,判断如果 status=fulfilled,说明 cb 里同步执行了 resolve,那么这里就需要手动调用 thenCb(result);
  3. 在调用.catch 时,判断如果 status=rejected,说明 cb 里同步执行了 reject,那么这里就需要手动调用 catchCb(error);

代码做如下改造:

js
const PENDING = "pending"; //等待状态
const FULFILLED = "fulfilled"; //成功
const REJECTED = "rejected"; //失败

class MyPromise {
  constructor(cb) {
    this.status = PENDING;
    this.result = null;
    this.error = null;

    this.thenCb = (e) => e;
    this.catchCb = (e) => e;

    if (cb && typeof cb == "function") {
      cb(this.resolve.bind(this), this.reject.bind(this));
    } else {
      throw new Error("param must be a function.");
    }
  }

  // 将status从pending变为fulfilled,代表cb执行成功,并执行thenCb
  resolve(result) {
    if (this.status == PENDING) {
      this.status = FULFILLED;
      this.result = result;
      this.thenCb(result);
      return this;
    }
  }

  // 将status从pending变为rejected,代表cb执行失败,并执行catchCb
  reject(error) {
    if (this.status == PENDING) {
      this.status = REJECTED;
      this.error = error;
      this.catchCb(error);
      return this;
    }
  }

  // 调用.then,如果status=pending,则记录then内部回调函数,在resolve里触发;
  then(thenCb) {
    if (this.status == PENDING) {
      this.thenCb = thenCb;
    }

    // 特殊情况:cb里直接执行了resolve,这里要手动执行thenCb
    if (this.status == FULFILLED) {
      thenCb(this.result);
    }
    return this;
  }

  // 调用.catch,如果status=pending,则记录catch内部回调函数,在reject里触发;
  catch(catchCb) {
    if (this.status == PENDING) {
      this.catchCb = catchCb;
    }

    // 特殊情况:cb里直接执行了reject,这里要手动执行catchCb
    if (this.status == REJECTED) {
      catchCb(this.error);
    }
    return this;
  }
}

这么一来,在传递给 Promise 的函数里同步执行 resolve 或者 reject 也是没问题的了,可以测试下效果:

js
function syncCb(resolve, reject) {
  resolve("同步返回succeed结果");
}

// 测试自定义promise
let mySyncPro = new MyPromise((resolve, reject) => syncCb(resolve, reject));

mySyncPro
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  });

// 立即返回结果:同步返回succeed结果

用原生 Promise.all 来测试我们的 MyPromise 实例:

js
let myPro1 = new MyPromise((resolve) => resolve("myPro1"));

let myPro2 = new MyPromise((resolve) => resolve("myPro2"));

Promise.all([myPro1, myPro2])
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  });

// 立即返回结果:[ 'myPro1', 'myPro2' ]

实现 promise.all

关键点:1.保证按照执行顺序返回结果的顺序;2.结束条件判断;

js
function delay(ms = 1000) {
  return new Promise((resolve) => resolve(ms), ms);
}

MyPromise.all = function (promises) {
  let res = [];
  let len = promises.length;
  let resoleCounts = 0;
  return new Promise((resolve, reject) => {
    promises.map((item, index) => {
      item
        .then((data) => {
          // res.push(data);// !!!这里不能push,没法保证顺序;
          res[index] = data;
          resoleCounts++;
          // if (res.length == len) // !!!这里不能用结果的length,因为加入res[3] = 4,的到res=[,,,4],length也是4
          if (resoleCounts == len) {
            resolve(res);
          }
        })
        .catch((err) => {
          reject(err);
        });
    });
  });
};

let p1 = delay(2000);
let p2 = delay(1000);
MyPromise.all([p1, p2]).then((ress) => {
  console.log(ress);
});

One More Thing

由此可见,.then.catch 都是同步执行的,目的是为了在状态改变之前把它们的参数值存储到 Promise 内部的 thenCbcatchCb,这样在 cb 里调用 resolvereject 时就能够找到对应要执行的回调函数了。

即便用户在cb里同步调用了resolve()reject()方法,导致用户传递给.then.catch的回调函数来不及被收集,也可以在 Promise 的.then.catch里判断this.status == FULFILLED || REJECTED来手动执行thenCbcatchCb

参考链接

Promise 进阶实战与控制反转

Released under the MIT License.